标签 链接器 下的文章

Go程序员拥抱C语言简明指南

本文永久链接 – https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher

本文是为于航老师的极客时间专栏《深入C语言和程序运行原理》写的加餐文章《Tony Bai:Go程序员拥抱C语言简明指南》,这里分享给大家,尤其是那些想学习C语言的Gopher们。


你好,我是Tony Bai。

也许有同学对我比较熟悉,看过我在极客时间上的专栏《Tony Bai ·Go语言第一课》,或者是关注了我的博客。那么,作为一个Gopher,我怎么跑到这个C语言专栏做分享了呢?其实,在学习Go语言并成为一名Go程序员之前,我也曾是一名地地道道的C语言程序员。

大学毕业后,我就开始从事C语言后端服务开发工作,在电信增值领域摸爬滚打了十多年。不信的话,你可以去翻翻我的博客,数一数我发的C语言相关文章是不是比关于Go的还多。一直到近几年,我才将工作中的主力语言从C切换到了Go。不过这并不是C语言的问题,主要原因是我转换赛道了。我目前在智能网联汽车领域从事面向云原生平台的先行研发,而在云原生方面,新生代的Go语言有着更好的生态。

不过作为资深C程序员,C语言已经在我身上打下了深深的烙印。虽然Go是我现在工作中的主力语言,但我仍然会每天阅读一些C开源项目的源码,每周还会写下数百行的C代码。在一些工作场景中,特别是在我参与先行研发一些车端中间件时,C语言有着资源占用小、性能高的优势,这一点是Go目前还无法匹敌的。

正因为我有着C程序员和Go程序员的双重身份,接到这个加餐邀请时,我就想到了一个很适合聊的话题——在 Gopher(泛指Go程序员)与C语言之间“牵线搭桥”。在这门课的评论区里,我看到一些同学说,“正是因为学了Go,所以我想学好C”。如果你也对Go比较熟悉,那么恭喜你,这篇加餐简直是为你量身定制的:一个熟悉Go的程序员在学习C时需要注意的问题,还有可能会遇到的坑,我都替你总结好了。

当然,我知道还有一些对Go了解不多的同学,看到这里也别急着退出去。因为C和Go这两门语言的比较,本身就是一个很有意思的话题。今天的加餐,会涉及这两门语言的异同点,通过对C与Go语言特性的比较,你就能更好地理解“C 语言为什么设计成现在这样”。

一. C语言是现代IT工业的根基

在比较C和Go之前,先说说我推荐Gopher学C的最重要原因吧:用一句话总结,C语言在IT工业中的根基地位,是Go和其他语言目前都无法动摇的

C语言是由美国贝尔实验室的丹尼斯·里奇(Dennis Ritchie)以Unix发明人肯·汤普森(Ken Thompson)设计的B语言为基础而创建的高级编程语言。诞生于上个世纪(精确来说是1972年)的它,到今年(2022年)已到了“知天命”的半百年纪。 年纪大、设计久远一直是“C语言过时论”兴起的根源,但如果你相信这一论断,那就大错特错了。下面,我来为你分析下个中缘由。

首先,我们说说C语言本身:C语言一直在演进,从未停下过脚步

虽然C语言之父丹尼斯·里奇不幸于2011年永远地离开了我们,但C语言早已成为ANSI(美国国家标准学会)标准以及ISO/IEC(国际标准化组织和国际电工委员会)标准,因此其演进也早已由标准委员会负责。我们来简单回顾一下C语言标准的演进过程:

  • 1989年,ANSI发布了首个C语言标准,被称为C89,又称ANSI C。次年,ISO和IEC把ANSI C89标准定为C语言的国际标准(ISO/IEC 9899:1990),又称C90,它也是C语言的第一个官方版本;
  • 1999年,ISO和IEC发布了C99标准(ISO/IEC 9899:1999),它是C语言的第二个官方版本;
  • 2011年,ISO和IEC发布了C11标准(ISO/IEC 9899:2011),它是C语言的第三个官方版本;
  • 2018年,ISO和IEC发布了C18标准(ISO/IEC 9899:2018),它是C语言的第四个官方版本。
    目前,ISO/IEC标准化委员会正在致力于C2x标准的改进与制定,预计它会在2023年发布。

其次,时至今日,C语言的流行度仍然非常高

著名编程语言排行榜TIOBE的数据显示,各大编程语言年度平均排名的总位次,C语言多年来高居第一,如下图(图片来自TIOBE)所示:

这说明,无论是在过去还是现在,C语言都是一门被广泛应用的工业级编程语言。

最后,也是最重要的一点是:C语言是现代IT工业的根基,我们说C永远不会退出IT行业舞台也不为过。

如今,无论是普通消费者端的Windows、macOS、Android、苹果iOS,还是服务器端的Linux、Unix等操作系统,亦或是各个工业嵌入式领域的操作系统,其内核实现语言都是C语言。互联网时代所使用的主流Web服务器,比如 Nginx、Apache,以及主流数据库,比如MySQL、Oracle、PostgreSQL等,也都是使用C语言开发的杰作。可以说,现代人类每天都在跟由C语言实现的系统亲密接触,并且已经离不开这些系统了。回到我们程序员的日常,Git、SVN等我们时刻在用的源码版本控制软件也都是由C语言实现的。

可以说,C语言在IT工业中的根基地位,不光Go语言替代不了,C++、Rust等系统编程语言也无法动摇,而且不仅短期如此,长期来看也是如此。

总之,C语言具有紧凑、高效、移植性好、对内存的精细控制等优秀特性,这使得我们在任何时候学习它都不会过时。不过,我在这里推荐Gopher去了解和系统学习C语言,其实还有另一个原因。我们继续往下看。

二. C与Go的相通之处:Gopher拥抱C语言的“先天优势”

众所周知,Go 是在C语言的基础上衍生而来的,二者之间有很多相通之处,因此 Gopher 在学习C语言时是有“先天优势”的。接下来,我们具体看看C和Go的相通之处有哪些。

1. 简单且语法同源

Go语言以简单著称,而作为Go先祖的C语言,入门门槛同样不高:Go有25个关键字,C有32个关键字(C89标准),简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言,这与C的简单也不无关系。

和Go不同的是,C语言是一个小内核、大外延的编程语言,其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库,我们可以快速掌握它。但需要注意的是,与Go语言“开箱即用、内容丰富”的标准库不同,C标准库非常小(在C11标准之前甚至连thread库都不包含),所以掌握“小内核”后,在LeetCode平台上刷题是没有任何问题的,但要写出某一领域的工业级生产程序,我们还有很多外延知识技能要学习,比如并发原语、操作系统的系统调用,以及进程间通信等。

C语言的这种简单很容易获得Gopher们的认同感。当年Go语言之父们在设计Go语言时,也是主要借鉴了C语言的语法。当然,这与他们深厚的C语言背景不无关系:肯·汤普森(Ken Thompson)是Unix之父,与丹尼斯·里奇共同设计了C语言;罗博·派克(Rob Pike)是贝尔实验室的资深研究员,参与了Unix系统的演进、Plan9操作系统的开发,还是UTF-8编码的发明人;罗伯特·格瑞史莫(Robert Griesemer)也是用C语言手写Java虚拟机的大神级人物。

Go的第一版编译器就是由肯·汤普森(Ken Thompson)用C语言实现的。并且,Go语言的早期版本中,C代码的比例还不小。以Go语言发布的第一个版本,Go 1.0版本为例,我们通过loccount工具对其进行分析,会得到下面的结果:

$loccount .
all          SLOC=460992  (100.00%) LLOC=193045  in 2746 files
Go           SLOC=256321  (55.60%)  LLOC=109763  in 1983 files
C            SLOC=148001  (32.10%)  LLOC=73458   in 368 files
HTML         SLOC=25080   (5.44%)   LLOC=0       in 57 files
asm          SLOC=10109   (2.19%)   LLOC=0       in 133 files
... ...

这里我们看到,在1.0版本中,C语言代码行数占据了32.10%的份额,这一份额直至Go 1.5版本实现自举后,才下降为不到1%。

我当初对Go“一见钟情”,其中一个主要原因就是Go与C语言的语法同源。相对应地,相信这种同源的语法也会让Gopher们喜欢上C语言。

2. 静态编译且基础范式相同

除了语法同源,C语言与Go语言的另一个相同点是,它们都是静态编译型语言。这意味着它们都有如下的语法特性:

  • 变量与函数都要先声明后才能使用;
  • 所有分配的内存块都要有对应的类型信息,并且在确定其类型信息后才能操作;
  • 源码需要先编译链接后才能运行。

相似的编程逻辑与构建过程,让学习C语言的Gopher可以做到无缝衔接。

除此之外,Go 和C的基础编程范式都是命令式编程(imperative programming),即面向算法过程,由程序员通过编程告诉计算机应采取的动作。然后,计算机按程序指令执行一系列流程,生成特定的结果,就像菜谱指定了厨师做蛋糕时应遵循的一系列步骤一样。

从Go看 C,没有面向对象,没有函数式编程,没有泛型(Go 1.18已加入),满眼都是类型与函数,可以说是相当亲切了。

3. 错误处理机制如出一辙

对于后端编程语言来说,错误处理机制十分重要。如果两种语言的错误处理机制不同,那么这两种语言的代码整体语法风格很可能大不相同。

在C语言中,我们通常用一个类型为整型的函数返回值作为错误状态标识,函数调用者基于值比较的方式,对这一代表错误状态的返回值进行检视。通常,当这个返回值为0时,代表函数调用成功;当这个返回值为其他值时,代表函数调用出现错误。函数调用者需根据该返回值所代表的错误状态,来决定后续执行哪条错误处理路径上的代码。

C语言这种简单的基于错误值比较的错误处理机制,让每个开发人员必须显式地去关注和处理每个错误。经过显式错误处理的代码会更为健壮,也会让开发人员对这些代码更有信心。另外,这些错误就是普通的值,我们不需要额外的语言机制去处理它们,只需利用已有的语言机制,像处理其他普通类型值那样去处理错误就可以了。这让代码更容易调试,我们也更容易针对每个错误处理的决策分支进行测试覆盖。

C语言错误处理机制的这种简单与显式,跟Go语言的设计哲学十分契合,于是Go语言设计者决定继承这种错误处理机制。因此,当Gopher们来到C语言的世界时,无需对自己的错误处理思维做出很大的改变,就可以很容易地适应C语言的风格。

三. 知己知彼,来看看C与Go的差异

虽说 Gopher 学习C语言有“先天优势”,但是不经过脚踏实地的学习与实践就想掌握和精通C语言,也是不可能的。而且,C 和Go还是有很大差异的,Gopher 们只有清楚这些差异,做到“知己知彼”,才能在学习过程中分清轻重,有的放矢。俗话说,“磨刀不误砍柴功”,下面我们就一起看看C与Go有哪些不同。

1. 设计哲学

在人类自然语言学界,有一个很著名的假说——“萨丕尔-沃夫假说”。这个假说的内容是这样的:语言影响或决定人类的思维方式。对我来说,编程语言也不仅仅是一门工具,它还影响着程序员的思维方式。每次开始学习一门新的编程语言时,我都会先了解这门编程语言的设计哲学。

每种编程语言都有自己的设计哲学,即便这门语言的设计者没有将其显式地总结出来,它也真真切切地存在,并影响着这门语言的后续演进,以及这门语言程序员的思维方式。我在《Tony Bai · Go语言第一课》专栏里,将Go语言的设计哲学总结成了5点,分别是简单、显式、组合、并发和面向工程

那么C语言的设计哲学又是什么呢?从表面上看,简单紧凑、性能至上、极致资源、全面移植,这些都可以作为C的设计哲学,但我倾向于一种更有人文气息的说法:满足和相信程序员

在这样的设计哲学下,一方面,C语言提供了几乎所有可以帮助程序员表达自己意图的语法手段,比如宏、指针与指针运算、位操作、pragma指示符、goto语句,以及跳转能力更为强大的longjmp等;另一方面,C语言对程序员的行为并没有做特别严格的限定与约束,C程序员可以利用语言提供的这些语法手段,进行天马行空的发挥:访问硬件、利用指针访问内存中的任一字节、操控任意字节中的每个位(bit)等。总之,C语言假定程序员知道他们在做什么,并选择相信程序员。

C语言给了程序员足够的自由,可以说,在C语言世界,你几乎可以“为所欲为”。但这种哲学也是有代价的,那就是你可能会犯一些莫名其妙的错误,比如悬挂指针,而这些错误很少或不可能在其他语言中出现。

这里再用一个比喻来更为形象地表达下:从Go世界到C世界,就好比在动物园中饲养已久的动物被放归到野生自然保护区,有了更多自由,但周围也暗藏着很多未曾遇到过的危险。因此,学习C语言的Gopher们要有足够的心理准备。

2. 内存管理

接下来我们来看C与Go在内存管理方面的不同。我把这一点放在第二位,是因为这两种语言在内存管理上有很大的差异,而且这一差异会给程序员的日常编码带来巨大影响。

我们知道,Go是带有垃圾回收机制(俗称GC)的静态编程语言。使用Go编程时,内存申请与释放,在栈上还是在堆上分配,以及新内存块的清零等等,这一切都是自动的,且对程序员透明。

但在C语言中,上面说的这些都是程序员的责任。手工内存管理在带来灵活性的同时,也带来了极大的风险,其中最常见的就是内存泄露(memory leak)与悬挂指针(dangling pointer)问题。

内存泄露主要指的是程序员手工在堆上分配的内存在使用后没有被释放(free),进而导致的堆内存持续增加。而悬挂指针的意思是指针指向了非法的内存地址,未初始化的指针、指针所指对象已经被释放等,都是导致悬挂指针的主要原因。针对悬挂指针进行解引用(dereference)操作将会导致运行时错误,从而导致程序异常退出的严重后果。

Go语言带有GC,而C语言不带GC,这都是由各自语言设计哲学所决定的。GC是不符合C语言的设计哲学的,因为一旦有了GC,程序员就远离了机器,程序员直面机器的需求就无法得到满足了。并且,一旦有了GC,无论是在性能上还是在资源占用上,都不可能做到极致了。

在C中,手工管理内存到底是一种什么感觉呢?作为一名有着十多年C开发经验的资深C程序员,我只能告诉你:与内存斗,其乐无穷!这是在带GC的编程语言中无法体会到的。

3. 语法形式

虽然C语言是Go的先祖,并且Go也继承了很多C语言的语法元素,但在变量/函数声明、行尾分号、代码块是否用括号括起、标识符作用域,以及控制语句语义等方面,二者仍有较大差异。因此,对Go已经很熟悉的程序员在初学C时,受之前编码习惯的影响,往往会踩一些“坑”。基于此,我总结了Gopher学习C语言时需要特别注意的几点,接下来我们具体看看。

第一,注意声明变量时类型与变量名的顺序

前面说过,Go与C都是静态编译型语言,这就要求我们在使用任何变量之前,需要先声明这个变量。但Go采用的变量声明语法颇似Pascal语言,即变量名在前,变量类型在后,这与C语言恰好相反,如下所示:

Go:

var a, b int
var p, q *int

vs.

C:
int a, b;
int *p, *q;

此外,Go支持短变量声明,并且由于短变量声明更短小,无需显式提供变量类型,Go编译器会根据赋值操作符后面的初始化表达式的结果,自动为变量赋予适当类型。因此,它成为了Gopher们喜爱和重度使用的语法。但短声明在C中却不是合法的语法元素:

int main() {
    a := 5; //  error: expected expression
    printf("a = %d\n", a);
}

不过,和上面的变量类型与变量名声明的顺序问题一样,C编译器会发现并告知我们这个问题,并不会给程序带来实质性的伤害。

第二,注意函数声明无需关键字前缀

无论是C语言还是Go语言,函数都是基本功能逻辑单元,我们也可以说C程序就是一组函数的集合。实际上,我们日常的C代码编写大多集中在实现某个函数上。

和变量一样,函数在两种语言中都需要先声明才能使用。Go语言使用func关键字作为函数声明的前缀,并且函数返回值列表放在函数声明的最后。但在C语言中,函数声明无需任何关键字作为前缀,函数只支持单一返回值,并且返回值类型放在函数名的前面,如下所示:

Go:
func Add(a, b int) int {
    return a+b
}

vs.

C:
int Add(int a, int b) {
    return a+b;
}

第三,记得加上代码行结尾的分号

我们日常编写Go代码时,极少手写分号。这是因为,Go设计者当初为了简化代码编写,提高代码可读性,选择了由编译器在词法分析阶段自动在适当位置插入分号的技术路线。如果你是一个被Go编译器惯坏了的Gopher,来到C语言的世界后,一定不要忘记代码行尾的分号。比如上面例子中的C语言Add函数实现,在return语句后面记得要手动加上分号。

第四,补上“省略”的括号

同样是出于简化代码、增加可读性的考虑,Go设计者最初就取消掉了条件分支语句(if)、选择分支语句(switch)和循环控制语句(for)中条件表达式外围的小括号:

// Go代码
func f() int {
    return 5
}
func main() {
    a := 1
    if a == 1 { // 无需小括号包裹条件表达式
        fmt.Println(a)
    }

    switch b := f(); b { // 无需小括号包裹条件表达式
    case 4:
        fmt.Println("b = 4")
    case 5:
        fmt.Println("b = 5")
    default:
        fmt.Println("b = n/a")
    }

    for i := 1; i < 10; i++ { // 无需小括号包裹循环语句的循环表达式
        a += i
    }
    fmt.Println(a)
}

这一点恰恰与C语言“背道而驰”。因此,我们在使用C语言编写代码时,务必要想着补上这些括号:

// C代码
int f() {
        return 5;
}

int main() {
    int a = 1;
    if (a == 1) { // 需用小括号包裹条件表达式
        printf("%d\n", a);
    }

    int b = f();
    switch (b) { // 需用小括号包裹条件表达式
    case 4:
        printf("b = 4\n");
        break;
    case 5:
        printf("b = 5\n");
        break;
    default:
        printf("b = n/a\n");
    }

    int i = 0;
    for (i = 1; i < 10; i++) { // 需用小括号包裹循环语句的循环表达式
        a += i;
    }
    printf("%d\n", a);
}

第五,留意C与Go导出符号的不同机制

C语言通过头文件来声明对外可见的符号,所以我们不用管符号是不是首字母大写的。但在Go中,只有首字母大写的包级变量、常量、类型、函数、方法才是可导出的,即对外部包可见。反之,首字母小写的则为包私有的,仅在包内使用。Gopher一旦习惯了这样的规则,在切换到C语言时,就会产生“心理后遗症”:遇到在其他头文件中定义的首字母小写的函数时,总以为不能直接使用。

第六,记得在switch case语句中添加break

C 语言与Go语言在选择分支语句的语义方面有所不同:C语言的 case 语句中,如果没有显式加入break语句,那么代码将向下自动掉落执行。而Go在最初设计时就重新规定了switch case的语义,默认不自动掉落(fallthrough),除非开发者显式使用fallthrough关键字。

适应了Go的switch case语句的语义后再回来写C代码,就会存在潜在的“风险”。我们来看一个例子:

// C代码:
int main() {
    int a = 1;
    switch(a) {
        case 1:printf("a = 1\n");
        case 2:printf("a = 2\n");
        case 3:printf("a = 3\n");
        default:printf("a = ?\n");
    }
}

这段代码是按Go语义编写的switch case,编译运行后得到的结果如下:

a = 1
a = 2
a = 3
a = ?

这显然不符合我们输出“a = 1”的预期。对于初学C的Gopher而言,这个问题影响还是蛮大的,因为这样编写的代码在C编译器眼中是完全合法的,但所代表的语义却完全不是开发人员想要的。这样的程序一旦流入到生产环境,其缺陷可能会引发生产故障。

一些Clint 工具可以检测出这样的问题,因此对于写C代码的Gopher,我建议在提交代码前使用lint工具对代码做一下检查。

4. 构建机制

Go与C都是静态编译型语言,它们的源码需要经过编译器和链接器处理,这个过程称为构建(build),构建后得到的可执行文件才是最终交付给用户的成果物。

和Go语言略有不同的是,C语言的构建还有一个预处理(pre-processing)阶段,预处理环节的输出才是C编译器的真正输入。C语言中的宏就是在预处理阶段展开的。不过,Go没有预处理阶段。

C语言的编译单元是一个C源文件(.c),每个编译单元在编译过程中会对应生成一个目标文件(.o/.obj),最后链接器将这些目标文件链接在一起,形成可执行文件。

而Go则是以一个包(package)为编译单元的,每个包内的源文件生成一个.o文件,一个包的所有.o文件聚合(archive)成一个.a文件,链接器将这些目标文件链接在一起形成可执行文件。

Go语言提供了统一的Go命令行工具链,且Go编译器原生支持增量构建,源码构建过程不需要Gopher手工做什么配置。但在C语言的世界中,用于构建C程序的工具有很多,主流的包括gcc/clang,以及微软平台的C编译器。这些编译器原生不支持增量构建,为了提升工程级构建的效率,避免每次都进行全量构建,我们通常会使用第三方的构建管理工具,比如make(Makefile)或CMake。考虑移植性时,我们还会使用到configure文件,用于在目标机器上收集和设置编译器所需的环境信息。

5. 依赖管理

我在前面提过,C语言仅提供了一个“小内核”。像依赖管理这类的事情,C语言本身并没有提供跟Go中的Go Module类似的,统一且相对完善的解决方案。在C语言的世界中,我们依然要靠外部工具(比如CMake)来管理第三方的依赖。

C语言的第三方依赖通常以静态库(.a)或动态共享库(.so)的形式存在。如果你的应用要使用静态链接,那就必须在系统中为C编译器提供第三方依赖的静态库文件。但在实际工作中,完全采用静态链接有时是会遇到麻烦的。这是因为,很多操作系统在默认安装时是不带开发包的,也就是说,像 libc、libpthread 这样的系统库只提供了动态共享库版本(如/lib下提供了libc的共享库libc.so.6),其静态库版本是需要自行下载、编译和安装的(如libc的静态库libc.a在安装后是放在/usr/lib下面的)。所以多数情况下,我们是将****静态、动态****两种链接方式混合在一起使用的,比如像libc这样的系统库多采用动态链接。

动态共享库通常是有版本的,并且按照一定规则安装到系统中。举个例子,一个名为libfoo的动态共享库,在安装的目录下文件集合通常是这样:

2022-03-10 12:28 libfoo.so -> libfoo.so.0.0.0*
2022-03-10 12:28 libfoo.so.0 -> libfoo.so.0.0.0*
2022-03-10 12:28 libfoo.so.0.0.0*

按惯例,每个动态共享库都有多个名字属性,包括real name、soname和linker name。下面我们来分别看下。

  • real name:实际包含共享库代码的那个文件的名字(如上面例子中的libfoo.so.0.0.0)。动态共享库的真实版本信息就在real name中,显然real name中的版本号符合语义版本规范,即major.minor.patch。当两个版本的major号一致,说明是向后兼容的两个版本;
  • soname:shared object name的缩写,也是这三个名字中最重要的一个。无论是在编译阶段还是在运行阶段,系统链接器都是通过动态共享库的soname(如上面例子中的libfoo.so.0)来唯一识别共享库的。我们看到的soname实际上是仅包含major号的共享库名字;
  • linker name:编译阶段提供给编译器的名字(如上面例子中的libfoo.so)。如果你构建的共享库的real name跟上面例子中libfoo.so.0.0.0类似,带有版本号,那么你在编译器命令中直接使用-L path -lfoo是无法让链接器找到对应的共享库文件的,除非你为libfoo.so.0.0.0提供了一个linker name(如libfoo.so,一个指向libfoo.so.0.0.0的符号链接)。linker name一般在共享库安装时手工创建。
    动态共享库有了这三个名称属性,依赖管理就有了依据。但由于在链接的时候使用的是linker name,而linker name并不带有版本号,真实版本与主机环境有关,因此要实现C应用的可重现构建还是比较难。在实践中,我们通常会使用专门的构建主机,项目组将该主机上的依赖管理起来,进而保证每次构建所使用的依赖版本是可控的。同时,应用部署的目标主机上的依赖版本也应该得到管理,避免运行时出现动态共享库版本不匹配的问题。

6. 代码风格

Go语言是历史上首次实现了代码风格全社区统一的编程语言。它基本上消除了开发人员在代码风格上的无休止的、始终无法达成一致的争论,以及不同代码风格带来的阅读、维护他人代码时的低效。gofmt工具格式化出来的代码风格已经成为Go开发者的一种共识,融入到Go语言的开发文化当中了。所以,如果你让某个Go开发者说说gofmt后的代码风格是什么样的,多数Go开发者可能说不出,因为代码会被gofmt自动变成那种风格,大家已经不再关心风格了。

而在C语言的世界,代码风格仍存争议。但经过多年的演进,以及像Go这样新兴语言的不断“教育”,C社区也在尝试进行这方面的改进,涌现出了像clang-format这样的工具。目前,虽然还没有在全社区达成一致的代码风格(由于历史原因,这很难做到),但已经可以减少很多不必要的争论。

对于正在学习C语言,并进行C编码实践的Gopher,我的建议是:不要拘泥于使用什么代码风格,先用clang-format,并确定一套风格模板就好

四. 小结

作为一名对Go跟随和研究了近十年的程序员,我深刻体会到,Go的简单性、性能和生产力使它成为了创建面向用户的应用程序和服务的理想语言。快速的迭代让团队能够快速地作出反应,以满足用户不断变化的需求,让团队可以将更多精力集中在保持灵活性上。

但Go也有缺点,比如缺少对内存以及一些低级操作的精确控制,而C语言恰好可以弥补这个缺陷。C 语言提供的更精细的控制允许更多的精确性,使得C成为低级操作的理想语言。这些低级操作不太可能发生变化,并且C相比Go还提高了性能。所以,如果你是一个有性能与低级操作需求的 Gopher ,就有充分的理由来学习C语言。

C 的优势体现在最接近底层机器的地方,而Go的优势在离用户较近的地方能得到最大发挥。当然,这并不是说两者都不能在对方的空间里工作,但这样做会增加“摩擦”。当你的需求从追求灵活性转变为注重效率时,用C重写库或服务的理由就更充分了。

总之,虽然Go和C的设计有很大的不同,但它们也有很多相似性,具备发挥兼容优势的基础。并且,当我们同时使用这二者时,就可以既有很大的灵活性,又有很好的性能,可以说是相得益彰!

五. 写在最后

今天的加餐中,我主要是基于C与Go的比较来讲解的,对于Go语言的特性并没有作详细展开。如果你还想进一步了解Go语言的设计哲学、语法特性、程序设计相关知识,欢迎来学习我在极客时间上的专栏《Tony Bai ·Go语言第一课》。在这门课里,我会用我十年Gopher的经验,带给你一条系统、完整的Go语言入门路径。

感谢你看到这里,如果今天的内容让你有所收获,欢迎把它分享给你的朋友。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go 1.16中值得关注的几个变化

img{512x368}

辛丑牛年初七开工大吉的日子(2021.2.18),Go核心开发团队为中国Gopher们献上了大礼 – Go 1.16版本正式发布了!国内Gopher可以在Go中国官网上下载到Go 1.16在各个平台的安装包:

img{512x368}

2020年双12,Go 1.16进入freeze状态,即不再接受新feature,仅fix bug、编写文档和接受安全更新等,那时我曾写过一篇名为《Go 1.16新功能特性不完全前瞻》的文章。当时Go 1.16的发布说明尚处于早期草稿阶段,要了解Go 1.16功能特性都有哪些变化,只能结合当时的release note以及从Go 1.16里程碑中的issue列表中挖掘。

如今Go 1.16版本正式发布了,和当时相比,Go 1.16又有哪些变化呢?在这篇文章中,我们就来一起详细分析一下Go 1.16中那些值得关注的重要变化!

一. 语言规范

如果你是Go语言新手,想必你一定很期待一个大版本的发布会带来许多让人激动人心的语言特性。但是Go语言在这方面肯定会让你“失望”的。伴随着Go 1.0版本一起发布的Go1兼容性承诺给Go语言的规范加了一个“框框”,从Go 1.0到Go 1.15版本,Go语言对语言规范的变更屈指可数,因此资深Gopher在阅读Go版本的release notes时总是很自然的略过这一章节,因为这一章节通常都是如下面这样的描述:

img{512x368}

这就是Go的设计哲学:简单!绝不轻易向语言中添加新语法元素增加语言的复杂性。除非是那些社区呼声很高并且是Go核心团队认可的。我们也可以将Go从1.0到Go 1.16这段时间称为“Go憋大招”的阶段,因为就在Go团队发布1.16版本之前不久,Go泛型提案正式被Go核心团队接受(Accepted):

img{512x368}

这意味着什么呢?这意味着在2022年2月份(Go 1.18),Gopher们将迎来Go有史以来最大一次语言语法变更并且这种变更依然是符合Go1兼容性承诺的,这将避免Go社区出现Python3给Python社区带去的那种“割裂”。不过就像《“能力越大,责任越大” – Go语言之父详解将于Go 1.18发布的Go泛型》一文中Go语言之父Robert Griesemer所说的那样:泛型引入了抽象,但滥用抽象而没有解决实际问题将带来不必要的复杂性,请三思而后行! 离泛型的落地还有一年时间,就让我们耐心等待吧!

二. Go对各平台/OS支持的变更

Go语言具有良好的可移植性,对各主流平台和OS的支持十分全面和及时,Go官博曾发布过一篇文章,简要列出了自Go1以来对各主流平台和OS的支持情况:

  • Go1(2012年3月)支持原始系统(译注:上面提到的两种操作系统和三种架构)以及64位和32位x86上的FreeBSD、NetBSD和OpenBSD,以及32位x86上的Plan9。
  • Go 1.3(2014年6月)增加了对64位x86上Solaris的支持。
  • Go 1.4(2014年12月)增加了对32位ARM上Android和64位x86上Plan9的支持。
  • Go 1.5(2015年8月)增加了对64位ARM和64位PowerPC上的Linux以及32位和64位ARM上的iOS的支持。
  • Go 1.6(2016年2月)增加了对64位MIPS上的Linux,以及32位x86上的Android的支持。它还增加了32位ARM上的Linux官方二进制下载,主要用于RaspberryPi系统。
  • Go 1.7(2016年8月)增加了对的z系统(S390x)上Linux和32位x86上Plan9的支持。
  • Go 1.8(2017年2月)增加了对32位MIPS上Linux的支持,并且它增加了64位PowerPC和z系统上Linux的官方二进制下载。
  • Go 1.9(2017年8月)增加了对64位ARM上Linux的官方二进制下载。
  • Go 1.12(2018年2月)增加了对32位ARM上Windows10 IoT Core的支持,如RaspberryPi3。它还增加了对64位PowerPC上AIX的支持。
  • Go 1.14(2019年2月)增加了对64位RISC-V上Linux的支持。

Go 1.7版本中新增的go tool dist list命令还可以帮助我们快速了解各个版本究竟支持哪些平台以及OS的组合。下面是Go 1.16版本该命令的输出:

$go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm

通常我不太会过多关注每次Go版本发布时关于可移植性方面的内容,这次将可移植性单独作为章节主要是因为Go 1.16发布之前的Apple M1芯片事件

img{512x368}

苹果公司再次放弃Intel x86芯片而改用自造的基于Arm64的M1芯片引发业界激烈争论。但现实是搭载Arm64 M1芯片的苹果笔记本已经大量上市,对于编程语言开发团队来说,能做的只有尽快支持这一平台。因此,Go团队给出了在Go 1.16版本中增加对Mac M1的原生支持。

在Go 1.16版本之前,Go也支持darwin/arm64的组合,但那更多是为了构建在iOS上运行的Go应用(利用gomobile)。

Go 1.16做了进一步的细分:将darwin/arm64组合改为apple M1专用;而构建在iOS上运行的Go应用则使用ios/arm64。同时,Go 1.16还增加了ios/amd64组合用于支持在MacOS(amd64)上运行的iOS模拟器中运行Go应用

另外还值得一提的是在OpenBSD上,Go应用的系统调用需要通过libc发起,而不能再绕过libc而直接使用汇编指令了,这是出于对未来OpenBSD的一些兼容性要求考虑才做出的决定。

三. Go module-aware模式成为默认!

在泛型落地前,Go module依旧是这些年Go语言改进的重点(虽不是语言规范特性)。在Go 1.16版本中,Go module-aware模式成为了默认模式(另一种则是传统的gopath模式)。module-aware模式成为默认意味着什么呢?意味着GO111MODULE的值默认为on了。

自从Go 1.11加入go module,不同go版本在GO111MODULE为不同值的情况下开启的构建模式几经变化,上一次go module-aware模式的行为有较大变更还是在Go 1.13版本中。这里将Go 1.13版本之前、Go 1.13版本以及Go 1.16版本在GO111MODULE为不同值的情况下的行为做一下对比,这样我们可以更好的理解go 1.16中module-aware模式下的行为特性,下面我们就来做一下比对:

GO111MODULE < Go 1.13 Go 1.13 Go 1.16
on 任何路径下都开启module-aware模式 任何路径下都开启module-aware模式 【默认值】:任何路径下都开启module-aware模式
auto 【默认值】:使用GOPATH mode还是module-aware mode,取决于要构建的源码目录所在位置以及是否包含go.mod文件。如果要构建的源码目录不在以GOPATH/src为根的目录体系下,且包含go.mod文件(两个条件缺一不可),那么使用module-aware mode;否则使用传统的GOPATH mode。 【默认值】:只要当前目录或父目录下有go.mod文件时,就开启module-aware模式,无论源码目录是否在GOPATH外面 只有当前目录或父目录下有go.mod文件时,就开启module-aware模式,无论源码目录是否在GOPATH外面
off gopath模式 gopath模式 gopath模式

我们看到在Go 1.16模式下,依然可以回归到gopath模式。但Go核心团队已经决定拒绝“继续保留GOPATH mode”的提案,并计划在Go 1.17版本中彻底取消gopath mode,仅保留go module-aware mode:

img{512x368}

虽然目前仍有项目没有转换到go module下,但根据调查,大多数项目已经选择拥抱go module并完成了转换工作,因此笔者认为即便Go 1.17真的取消了GOPATH mode,对整个Go社区的影响也不会太大了。

Go 1.16中,go module机制还有其他几个变化,这里逐一来看一下:

1. go build/run命令不再自动更新go.mod和go.sum了

为了能更清晰看出Go 1.16与之前版本的差异,我们准备了一个小程序:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld/go.mod
module github.com/bigwhite/helloworld

go 1.16

// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld/helloworld.go
package main

import "github.com/sirupsen/logrus"

func main() {
    logrus.Println("Hello, World")
}

我们使用go 1.15版本构建一下该程序:

$go build
go: finding module for package github.com/sirupsen/logrus
go: downloading github.com/sirupsen/logrus v1.8.0
go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.8.0

$cat go.mod
module github.com/bigwhite/helloworld

go 1.16

require github.com/sirupsen/logrus v1.8.0

$cat go.sum
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

在Go 1.15版本中,go build会自动分析源码中的依赖,如果go.mod中没有对该依赖的require,则会自动添加require,同时会将go.sum中将相关包(特定版本)的校验信息写入。

我们将上述helloworld恢复到初始状态,再用go 1.16来build一次:

$go build
helloworld.go:3:8: no required module provides package github.com/sirupsen/logrus; to add it:
    go get github.com/sirupsen/logrus

我们看到go build没有成功,而是给出错误:go.mod中没有对logrus的require,并给出添加对logrus的require的方法(go get github.com/sirupsen/logrus)。

我们就按照go build给出的提示执行go get:

$go get github.com/sirupsen/logrus
go: downloading github.com/magefile/mage v1.10.0
go get: added github.com/sirupsen/logrus v1.8.0

$cat go.mod
module github.com/bigwhite/helloworld

go 1.16

require github.com/sirupsen/logrus v1.8.0 // indirect

$cat go.sum
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

$go build
//ok

我们看到go build并不会向go 1.15及之前版本那样做出有“副作用”的动作:自动修改go.mod和go.sum,而是提示开发人员显式通过go get来添加缺少的包/module,即便是依赖包major版本升级亦是如此。

从自动更新go.mod,到通过提供-mod=readonly选项来避免自动更新go.mod,再到Go 1.16的禁止自动更新go.mod,笔者认为这个变化是Go不喜“隐式转型”的一种延续,即尽量不支持任何可能让开发者产生疑惑或surprise的隐式行为(就像隐式转型),取而代之的是要用一种显式的方式去完成(就像必须显式转型那样)。

我们也看到在go 1.16中,添加或更新go.mod中的依赖,只有显式使用go get。go mod tidy依旧会执行对go.mod的清理,即也可以修改go.mod。

2. 推荐使用go install安装Go可执行文件

在gopath mode下,go install基本“隐身”了,它能做的事情基本都被go get“越俎代庖”了。在go module时代初期,go install更是没有了地位。但Go团队现在想逐步恢复go install的角色:安装Go可执行文件!在Go 1.16中,当go install后面的包携带特定版本号时,go install将忽略当前go.mod中的依赖信息而直接编译安装可执行文件:

// go install回将gopls v0.6.5安装到GOBIN下
$go install golang.org/x/tools/gopls@v0.6.5

并且后续,Go团队会让go get将专注于分析依赖,并获取go包/module,更新go.mod/go.sum,而不再具有安装可执行Go程序的行为能力,这样go get和go install就会各司其职,Gopher们也不会再被两者的重叠行为所迷惑了。现在如果不想go get编译安装,可使用go get -d。

3. 作废module的特定版本

《如何作废一个已发布的Go module版本,我来告诉你!》一文中,我曾详细探讨了Go引入module后如何作废一个已发布的go module版本。当时已经知晓Go 1.16会在go.mod中增加retract指示符,因此也给出了在Go 1.16下retract一个module版本的原理和例子(基于当时的go tip)。

Go 1.16正式版在工具的输出提示方面做了进一步的优化,让开发人员体验更为友好。我们还是以一个简单的例子来看看在Go 1.16中作废一个module版本的过程吧。

在我的bitbucket账户下有一个名为m2的Go module(https://bitbucket.org/bigwhite/m2/),当前它的版本为v1.0.0:

// bitbucket.org/bigwhite/m2
$cat go.mod
module bitbucket.org/bigwhite/m2

go 1.15

$cat m2.go
package m2

import "fmt"

func M2() {
    fmt.Println("This is m2.M2 - v1.0.0")
}

我们在本地建立一个m2的消费者:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/retract

$cat go.mod
module github.com/bigwhite/retractdemo

go 1.16

$cat main.go
package main

import "bitbucket.org/bigwhite/m2"

func main() {
    m2.M2()
}

运行这个消费者:

$go run main.go
main.go:3:8: no required module provides package bitbucket.org/bigwhite/m2; to add it:
    go get bitbucket.org/bigwhite/m2

由于上面提到的原因,go run不会隐式修改go.mod,因此我们需要手工go get m2:

$go get bitbucket.org/bigwhite/m2
go: downloading bitbucket.org/bigwhite/m2 v1.0.0
go get: added bitbucket.org/bigwhite/m2 v1.0.0

再来运行消费者,我们将看到以下运行成功的结果:

$go run main.go
This is m2.M2 - v1.0.0

现在m2的作者对m2打了小补丁,版本升级到了v1.0.1。这时消费者通过go list命令可以看到m2的最新版本(前提:go proxy server上已经cache了最新的v1.0.1):

$go list -m -u all
github.com/bigwhite/retractdemo
bitbucket.org/bigwhite/m2 v1.0.0 [v1.0.1]

消费者可以通过go get将对m2的依赖升级到最新的v1.0.1:

$go get bitbucket.org/bigwhite/m2@v1.0.1

go get: upgraded bitbucket.org/bigwhite/m2 v1.0.0 => v1.0.1
$go run main.go
This is m2.M2 - v1.0.1

m2作者收到issue,有人指出v1.0.1版本有安全漏洞,m2作者确认了该漏洞,但此时v1.0.1版已经发布并被缓存到各大go proxy server上,已经无法撤回。m2作者便想到了Go 1.16中引入的retract指示符,于是它在m2的go.mod用retract指示符做了如下更新:

$cat go.mod
module bitbucket.org/bigwhite/m2

// 存在安全漏洞
retract v1.0.1

go 1.15

并将此次更新作为v1.0.2发布了出去!

之后,当消费者使用go list查看m2是否有最新更新时,便会看到retract提示:(前提:go proxy server上已经cache了最新的v1.0.2)

$go list -m -u all
github.com/bigwhite/retractdemo
bitbucket.org/bigwhite/m2 v1.0.1 (retracted) [v1.0.2]

执行go get会收到带有更详尽信息的retract提示和问题解决建议:

$go get .
go: warning: bitbucket.org/bigwhite/m2@v1.0.1: retracted by module author: 存在安全漏洞
go: to switch to the latest unretracted version, run:
    go get bitbucket.org/bigwhite/m2@latest

于是消费者按照提示执行go get bitbucket.org/bigwhite/m2@latest:

$go get bitbucket.org/bigwhite/m2@latest
go get: upgraded bitbucket.org/bigwhite/m2 v1.0.1 => v1.0.2

$cat go.mod
module github.com/bigwhite/retractdemo

go 1.16

require bitbucket.org/bigwhite/m2 v1.0.2

$go run main.go
This is m2.M2 - v1.0.2

到此,retract的使命终于完成了!

4. 引入GOVCS环境变量,控制module源码获取所使用的版本控制工具

出于安全考虑,Go 1.16引入GOVCS环境变量,用于在go命令直接从代码托管站点获取源码时对所使用的版本控制工具进行约束,如果是从go proxy server获取源码,那么GOVCS将不起作用,因为go工具与go proxy server之间使用的是GOPROXY协议

GOVCS的默认值为public:git|hg,private:all,即对所有公共module允许采用git或hg获取源码,而对私有module则不限制版本控制工具的使用。

如果要允许使用所有工具,可像下面这样设置GOVCS:

GOVCS=*:all

如果要禁止使用任何版本控制工具去直接获取源码(不通过go proxy),那么可以像下面这样设置GOVCS:

GOVCS=*:off

5. 有关go module的文档更新

自打Go 1.14版本宣布go module生产可用后,Go核心团队在说服和帮助Go社区全面拥抱go module的方面不可谓不努力。在文档方面亦是如此,最初有关go module的文档仅局限于go build命令相关以及有关go module的wiki。随着go module日益成熟,go.mod格式的日益稳定,Go团队在1.16版本中还将go module相关文档升级到go reference的层次,与go language ref等并列:

img{512x368}

我们看到有关go module的ref文档包括:

官方还编写了详细的Go module日常开发时的使用方法,包括:开发与发布module、module发布与版本管理工作流、升级major号等。

img{512x368}

建议每个gopher都要将这些文档仔细阅读一遍,以更为深入了解和使用go module

四. 编译器与运行时

1. runtime/metrics包

《Go 1.16新功能特性不完全前瞻》一文中,我们提到过:Go 1.16 新增了runtime/metrics包,以替代runtime.ReadMemStats和debug.ReadGCStats输出runtime的各种度量数据,这个包更通用稳定,性能也更好。限于篇幅这里不展开,后续可能会以单独的文章讲解这个新包。

2. GODEBUG环境变量支持跟踪包init函数的消耗

GODEBUG=inittrace=1这个特性也保留在了Go 1.16正式版当中了。当GODEBUG环境变量包含inittrace=1时,Go运行时将会报告各个源代码文件中的init函数的执行时间和内存开辟消耗情况。我们用上面的helloworld示例(github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld)来看看该特性的效果:

$go build
$GODEBUG=inittrace=1 ./helloworld
init internal/bytealg @0.006 ms, 0 ms clock, 0 bytes, 0 allocs
init runtime @0.037 ms, 0.031 ms clock, 0 bytes, 0 allocs
init errors @0.29 ms, 0.005 ms clock, 0 bytes, 0 allocs
init math @0.31 ms, 0 ms clock, 0 bytes, 0 allocs
init strconv @0.33 ms, 0.002 ms clock, 32 bytes, 2 allocs
init sync @0.35 ms, 0.003 ms clock, 16 bytes, 1 allocs
init unicode @0.37 ms, 0.10 ms clock, 24568 bytes, 30 allocs
init reflect @0.49 ms, 0.002 ms clock, 0 bytes, 0 allocs
init io @0.51 ms, 0.003 ms clock, 144 bytes, 9 allocs
init internal/oserror @0.53 ms, 0 ms clock, 80 bytes, 5 allocs
init syscall @0.55 ms, 0.010 ms clock, 752 bytes, 2 allocs
init time @0.58 ms, 0.010 ms clock, 384 bytes, 8 allocs
init path @0.60 ms, 0 ms clock, 16 bytes, 1 allocs
init io/fs @0.62 ms, 0.002 ms clock, 16 bytes, 1 allocs
init internal/poll @0.63 ms, 0.001 ms clock, 64 bytes, 4 allocs
init os @0.65 ms, 0.089 ms clock, 4472 bytes, 20 allocs
init fmt @0.77 ms, 0.006 ms clock, 32 bytes, 2 allocs
init bytes @0.84 ms, 0.004 ms clock, 48 bytes, 3 allocs
init context @0.87 ms, 0 ms clock, 128 bytes, 4 allocs
init encoding/binary @0.89 ms, 0.002 ms clock, 16 bytes, 1 allocs
init encoding/base64 @0.90 ms, 0.015 ms clock, 1408 bytes, 4 allocs
init encoding/json @0.93 ms, 0.002 ms clock, 32 bytes, 2 allocs
init log @0.95 ms, 0 ms clock, 80 bytes, 1 allocs
init golang.org/x/sys/unix @0.96 ms, 0.002 ms clock, 48 bytes, 1 allocs
init bufio @0.98 ms, 0 ms clock, 176 bytes, 11 allocs
init github.com/sirupsen/logrus @0.99 ms, 0.009 ms clock, 312 bytes, 5 allocs
INFO[0000] Hello, World

以下面这行为例:

init fmt @0.77 ms, 0.006 ms clock, 32 bytes, 2 allocs
  • 0.77ms表示的是自从程序启动后到fmt包init执行所过去的时间(以ms为单位)
  • 0.006 ms clock表示fmt包init函数执行的时间(以ms为单位)
  • 312 bytes表示fmt包init函数在heap上分配的内存大小;
  • 5 allocs表示的是fmt包init函数在heap上执行内存分配操作的次数。

3. Go runtime默认使用MADV_DONTNEED

Go 1.15版本时,我们可以通过GODEBUG=madvdontneed=1让Go runtime使用MADV_DONTNEED替代MADV_FREE达到更积极的将不用的内存释放给OS的效果(如果使用MADV_FREE,只有OS内存压力很大时,才会真正回收内存),这将使得通过top查看到的常驻系统内存(RSS或RES)指标更实时也更真实反映当前Go进程对os内存的实际占用情况(仅使用linux)。

在Go 1.16版本中,Go runtime将MADV_DONTNEED作为默认值了,我们可以用一个小例子来对比一下这种变化:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/runtime/memalloc.go
package main

import "time"

func allocMem() []byte {
    b := make([]byte, 1024*1024*1) //1M
    return b
}

func main() {
    for i := 0; i < 100000; i++ {
        _ = allocMem()
        time.Sleep(500 * time.Millisecond)
    }
}

我们在linux上使用go 1.16版本编译该程序,考虑到优化和inline的作用,我们在编译时关闭优化和内联:

$go build -gcflags "-l -N" memalloc.go

接下来,我们分两次运行该程序,并使用top监控其RES指标值:

$./memalloc
$ top -p 9273
  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 9273 root      20   0  704264   5840    856 S  0.0  0.3   0:00.03 memalloc
 9273 root      20   0  704264   3728    856 S  0.0  0.2   0:00.05 memalloc
 ... ...

$GODEBUG=madvdontneed=0 ./memalloc
$ top -p 9415

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 9415 root      20   0  704264   5624    856 S  0.0  0.3   0:00.03 memalloc
 9415 root      20   0  704264   5624    856 S  0.0  0.3   0:00.05 memalloc

我们看到默认运行的memalloc(开启MADV_DONTNEED),RES很积极的变化,当上一次显示5840,下一秒内存就被归还给OS,RES变为3728。而关闭MADV_DONTNEED(GODEBUG=madvdontneed=0)的memalloc,OS就会很lazy的回收内存,RES一直显示5624这个值。

4. Go链接器的进一步进行现代化改造

新一代Go链接器的更新计划从Go 1.15版本开始,在Go 1.15版本链接器的性能、资源占用、最终二进制文件大小等方面都有了一定幅度的优化提升。Go 1.16版本延续了这一势头:相比于Go 1.15,官方宣称(在linux上)性能有20%-25%的提升,资源占用下降5%-15%。更为直观的是编译出的二进制文件的size,我实测了一下文件大小下降10%以上:

-rwxr-xr-x   1 tonybai  staff    22M  2 21 23:03 my-large-app-demo*
-rwxr-xr-x   1 tonybai  staff    25M  2 21 23:02 my-large-app-demo-go1.15*

并且和Go 1.15的链接器优化仅针对amd64平台和基于ELF格式的OS不同,这次的链接器优化已经扩展到所有平台和os组合上

五. 标准库

1. io/fs包

Go 1.16标准库新增io/fs包,并定义了一个fs.File接口用于表示一个只读文件树(tree of file)的抽象。之所以要加入io/fs包并新增fs.File接口源于对嵌入静态资源文件(embed static asset)的实现需求。虽说实现embed功能特性是直接原因,但io/fs的加入也不是“临时起意”,早在很多年前的godoc实现时,对一个抽象的文件系统接口的需求就已经被提了出来并给出了实现:

最终这份实现以godoc工具的vfs包的形式一直长期存在着。虽然它的实现有些复杂,抽象程度不够,但却对io/fs包的设计有着重要的参考价值。同时也部分弥补了Rob Pike老爷子当年没有将os.File设计为interface的遗憾Ian Lance Taylor 2013年提出的增加VFS层的想法也一并得以实现。

io/fs包的两个最重要的接口如下:

// $GOROOT/src/io/fs/fs.go

// An FS provides access to a hierarchical file system.
//
// The FS interface is the minimum implementation required of the file system.
// A file system may implement additional interfaces,
// such as ReadFileFS, to provide additional or optimized functionality.
type FS interface {
        // Open opens the named file.
        //
        // When Open returns an error, it should be of type *PathError
        // with the Op field set to "open", the Path field set to name,
        // and the Err field describing the problem.
        //
        // Open should reject attempts to open names that do not satisfy
        // ValidPath(name), returning a *PathError with Err set to
        // ErrInvalid or ErrNotExist.
        Open(name string) (File, error)
}

// A File provides access to a single file.
// The File interface is the minimum implementation required of the file.
// A file may implement additional interfaces, such as
// ReadDirFile, ReaderAt, or Seeker, to provide additional or optimized functionality.
type File interface {
        Stat() (FileInfo, error)
        Read([]byte) (int, error)
        Close() error
}

FS接口代表虚拟文件系统的最小抽象,File接口则是虚拟文件的最小抽象,我们可以基于这两个接口进行扩展以及对接现有的一些实现。io/fs包也给出了一些扩展FS的“样例”:

这两个接口的设计也是“Go秉持定义小接口惯例”的延续(更多关于这方面的内容,可以参考我的专栏文章《定义小接口是Go惯例》)。

io/fs包的加入也契合了Go社区对vfs的需求,在Go团队决定加入io/fs并提交实现后,社区做出了积极的反应,在github上我们能看到好多为各类对象提供针对io/fs.FS接口实现的项目:

io/fs.FS和File接口在后续Go演进过程中会像io.Writer和io.Reader一样成为Gopher们在操作类文件树时最爱的接口。

2. embed包

《Go 1.16新功能特性不完全前瞻》一文中我们曾重点说了Go 1.16将支持在Go二进制文件中嵌入静态文件并给出了一个在webserver中嵌入文本文件的例子:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/webserver/hello.txt
hello, go 1.16

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/webserver/main.go
package main

import (
         _  "embed"
    "net/http"
)

//go:embed hello.txt
var s string

func main() {
    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(s))
    }))
    http.ListenAndServe(":8080", nil)
}

我们看到在这个例子,通过//go:embed hello.txt,我们可以轻易地将hello.txt的内容存储在包级变量s中,而s将作为每个http request的应答返回给客户端。

在Go二进制文件中嵌入静态资源文件是Go核心团队对社区广泛需求的积极回应。在go 1.16以前,Go社区开源的类嵌入静态文件的项目不下十多个,在Russ Cox关于embed的设计草案中,他就列了十多个:

  • github.com/jteeuwen/go-bindata(主流实现)
  • github.com/alecthomas/gobundle
  • github.com/GeertJohan/go.rice
  • github.com/go-playground/statics
  • github.com/gobuffalo/packr
  • github.com/knadh/stuffbin
  • github.com/mjibson/esc
  • github.com/omeid/go-resources
  • github.com/phogolabs/parcello
  • github.com/pyros2097/go-embed
  • github.com/rakyll/statik
  • github.com/shurcooL/vfsgen
  • github.com/UnnoTed/fileb0x
  • github.com/wlbr/templify
  • perkeep.org/pkg/fileembed

Go1.16原生支持嵌入并且给出一种开发者体验良好的实现方案,这对Go社区是一种极大的鼓励,也是Go团队重视社区声音的重要表现。

笔者认为embed机制是Go 1.16中玩法最多的一种机制,也是极具新玩法挖掘潜力的机制。在embed加入Go tip不久,很多Gopher就已经“脑洞大开”:

有通过embed嵌入版本号的:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/version/main.go
package main

import (
    _ "embed"
    "fmt"
    "strings"
)

var (
    Version string = strings.TrimSpace(version)
    //go:embed version.txt
    version string
)

func main() {
    fmt.Printf("Version %q\n", Version)
}

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/version/version.txt
v1.0.1

有通过embed打印自身源码的:

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/printself/main.go
package main

import (
        _ "embed"
        "fmt"
)

//go:embed main.go
var src string

func main() {
        fmt.Print(src)
}

更是有将一个完整的、复杂的带有js支持的web站点直接嵌入到go二进制文件中的示例,鉴于篇幅,这里就不一一列举了。

Go擅长于Web服务,而embed机制的引入粗略来看,可以大大简化web服务中资源文件的部署,估计这也是之前社区青睐各种静态资源文件嵌入项目的原因。embed估计也会成为Go 1.16中最被gopher们喜爱的功能特性。

不过embed机制的实现目前有如下一些局限:

  • 仅支持在包级变量前使用//go:embed指示符,还不支持在函数/方法内的局部变量上应用embed指示符(当然我们可以通过将包级变量赋值给局部变量来过渡一下);
  • 使用//go:embed指示符的包必须以空导入的方式导入embed包,二者是成对出现的,缺一不可;

3. net包的变化

在Go 1.16之前,我们检测在一个已关闭的网络上进行I/O操作或在I/O完成前网络被关闭的情况,只能通过匹配字符串”use of closed network connection”的方式来进行。之前的版本没有针对这个错误定义“哨兵错误变量”(更多关于哨兵错误变量的内容,可以参考我的专栏文章《别笑!这就是 Go 的错误处理哲学》),Go 1.16增加了ErrClosed这个“哨兵错误变量”,我们可以通过errors.Is(err, net.ErrClosed)来检测是否是上述错误情况。

六. 小结

从Go 1.16版本变更的功能特性中,我看到了Go团队更加重视社区的声音,这也是Go团队一直持续努力的目标。在最新的Go proposal review meeting的结论中,我们还看到了这样的一个proposal被accept:

要知道这个proposal的提议是将在Go 1.18才会落地的泛型实现分支merge到Go项目master分支,也就是说在Go 1.17中就会包含“不会发布的”泛型部分实现,这在之前是不可能实现的(之前,新proposal必须有原型实现的分支,实现并经过社区测试与Go核心委员会评估后才会在特定版本merge到master分支)。虽说泛型的开发有其特殊情况,但能被accept,这恰证明了Go社区的声音在Go核心团队日益受到重视。

如果你还没有升级到Go 1.16,那么现在正是时候

本文中涉及的代码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/go1.16-examples


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

考虑到部落尚处于推广期,这里仍然为大家准备了新人优惠券,虽然优惠幅度有所下降,但依然物超所值,早到早享哦!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订阅!目前该技术专栏正在新春促销!关注我的个人公众号“iamtonybai”,发送“go专栏活动”即可获取专栏专属优惠码,可在订阅专栏时抵扣20元哦(2021.2月末前有效)。

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats