You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

359 lines
30 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 大咖助阵Tony BaiGo 程序员拥抱 C 语言简明指南
> 你好,我是于航。这一讲是一期大咖加餐,我们邀请到了 Tony Bai 老师,来跟你聊聊 C 语言的一个优秀“后辈”Go 语言的故事。
> Go 在语法上跟 C 类似,但它却通过提供垃圾回收机制,从侧面解决了 C 程序容易发生内存泄露的问题进而使得程序的构建变得更加简单。除此之外Go 还提供了大量用于编写并发程序的内置工具和库,因此它被大量应用于构架需要满足高并发性能的软件中,比如你最熟悉的 Kubernetes。
> 通过这一讲加餐,你可以了解到 C 与 Go 这两种语言之间的相似性和区别,相信你一定能有所收获。
你好我是Tony Bai。
也许有同学对我比较熟悉,看过我在极客时间上的专栏[《Tony Bai · Go 语言第一课》](https://time.geekbang.org/column/intro/100093501?tab=catalog)或者是关注了我的博客。那么作为一个Gopher我怎么跑到这个C语言专栏做分享了呢其实在学习Go语言并成为一名Go程序员之前我也曾是一名地地道道的C语言程序员。
大学毕业后我就开始从事C语言后端服务开发工作在电信增值领域摸爬滚打了十多年。不信的话你可以去翻翻[我的博客](https://tonybai.com)数一数我发的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)](https://www.iso.org/standard/29237.html)它是C语言的第二个官方版本
* 2011年ISO和IEC发布了[C11标准(ISO/IEC 9899:2011)](https://www.iso.org/standard/57853.html)它是C语言的第三个官方版本
* 2018年ISO和IEC发布了[C18标准(ISO/IEC 9899:2018)](https://www.iso.org/standard/74528.html)它是C语言的第四个官方版本。
目前ISO/IEC标准化委员会正在致力于C2x标准的改进与制定预计它会在2023年发布。
其次,**时至今日C语言的流行度仍然非常高**。
著名编程语言排行榜TIOBE的数据显示各大编程语言年度平均排名的总位次C语言多年来高居第一如下图图片来自[TIOBE](https://www.tiobe.com/tiobe-index))所示:
![图片](https://static001.geekbang.org/resource/image/69/8f/696192949279c8bfde8554b77191be8f.png?wh=1920x797)
这说明无论是在过去还是现在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 的相通之处有哪些。
### 简单且语法同源
Go语言以简单著称而作为**Go先祖**的C语言入门门槛同样不高Go有25个关键字C有32个关键字C89标准简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言这与C的简单也不无关系。
和Go不同的是C语言是一个**小内核、大外延**的编程语言其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库我们可以快速掌握它。但需要注意的是与Go语言“开箱即用、内容丰富”的标准库不同[C标准库](https://en.wikipedia.org/wiki/C_standard_library)非常小在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版本](https://github.com/golang/go/releases/tag/go1)为例,我们通过[loccount工具](https://gitlab.com/esr/loccount)对其进行分析,会得到下面的结果:
```plain
$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语言。
### 静态编译且基础范式相同
除了语法同源C语言与Go语言的另一个相同点是它们都是静态编译型语言。这意味着它们都有如下的语法特性
* 变量与函数都要先声明后才能使用;
* 所有分配的内存块都要有对应的类型信息,并且在确定其类型信息后才能操作;
* 源码需要先编译链接后才能运行。
相似的编程逻辑与构建过程让学习C语言的Gopher可以做到无缝衔接。
除此之外Go 和 C 的基础编程范式都是命令式编程imperative programming即面向算法过程由程序员通过编程告诉计算机应采取的动作。然后计算机按程序指令执行一系列流程生成特定的结果就像菜谱指定了厨师做蛋糕时应遵循的一系列步骤一样。
从 Go 看 C没有面向对象没有函数式编程没有泛型Go 1.18已加入),满眼都是类型与函数,可以说是相当亲切了。
### 错误处理机制如出一辙
对于后端编程语言来说,错误处理机制十分重要。如果两种语言的错误处理机制不同,那么这两种语言的代码整体语法风格很可能大不相同。
在C语言中我们通常用一个类型为整型的函数返回值作为错误状态标识函数调用者基于值比较的方式对这一代表错误状态的返回值进行检视。通常当这个返回值为0时代表函数调用成功当这个返回值为其他值时代表函数调用出现错误。函数调用者需根据该返回值所代表的错误状态来决定后续执行哪条错误处理路径上的代码。
C语言这种简单的**基于错误值比较**的错误处理机制,让每个开发人员必须显式地去关注和处理每个错误。经过显式错误处理的代码会更为健壮,也会让开发人员对这些代码更有信心。另外,这些错误就是普通的值,我们不需要额外的语言机制去处理它们,只需利用已有的语言机制,像处理其他普通类型值那样去处理错误就可以了。这让代码更容易调试,我们也更容易针对每个错误处理的决策分支进行测试覆盖。
C语言错误处理机制的这种简单与显式跟Go语言的设计哲学十分契合于是Go语言设计者决定继承这种错误处理机制。因此当Gopher们来到C语言的世界时无需对自己的错误处理思维做出很大的改变就可以很容易地适应C语言的风格。
## 知己知彼,来看看 C 与 Go 的差异
虽说 Gopher 学习 C 语言有“先天优势”但是不经过脚踏实地的学习与实践就想掌握和精通C语言也是不可能的。而且C 和 Go 还是有很大差异的Gopher 们只有清楚这些差异,做到“知己知彼”,才能在学习过程中分清轻重,有的放矢。俗话说,“磨刀不误砍柴功”,下面我们就一起看看 C 与 Go 有哪些不同。
### 设计哲学
在人类自然语言学界,有一个很著名的假说——“[萨丕尔-沃夫假说](https://en.wikipedia.org/wiki/Linguistic_relativity)”。这个假说的内容是这样的:**语言影响或决定人类的思维方式**。对我来说,**编程语言也不仅仅是一门工具,它还影响着程序员的思维方式**。每次开始学习一门新的编程语言时,我都会先了解这门编程语言的设计哲学。
每种编程语言都有自己的设计哲学,即便这门语言的设计者没有将其显式地总结出来,它也真真切切地存在,并影响着这门语言的后续演进,以及这门语言程序员的思维方式。我在[《Tony Bai · Go 语言第一课》](http://gk.link/a/10AVZ)专栏里将Go语言的设计哲学总结成了5点分别是**简单、显式、组合、并发和面向工程**。
那么C语言的设计哲学又是什么呢从表面上看简单紧凑、性能至上、极致资源、全面移植这些都可以作为C的设计哲学但我倾向于一种更有人文气息的说法**满足和相信程序员**。
在这样的设计哲学下一方面C语言提供了几乎所有可以帮助程序员表达自己意图的语法手段比如宏、指针与指针运算、位操作、pragma指示符、goto语句以及跳转能力更为强大的longjmp等另一方面C语言对程序员的行为并没有做特别严格的限定与约束C程序员可以利用语言提供的这些语法手段进行天马行空的发挥访问硬件、利用指针访问内存中的任一字节、操控任意字节中的每个位bit等。总之C语言假定程序员知道他们在做什么并选择相信程序员。
C语言给了程序员足够的自由可以说在C语言世界你几乎可以“为所欲为”。但这种哲学也是有代价的那就是你可能会犯一些莫名其妙的错误比如悬挂指针而这些错误很少或不可能在其他语言中出现。
这里再用一个比喻来更为形象地表达下从Go世界到C世界就好比在动物园中饲养已久的动物被放归到野生自然保护区有了更多自由但周围也暗藏着很多未曾遇到过的危险。因此学习C语言的Gopher们要有足够的心理准备。
### 内存管理
接下来我们来看 C 与 Go 在内存管理方面的不同。我把这一点放在第二位,是因为这两种语言在内存管理上有很大的差异,而且这一差异会给程序员的日常编码带来巨大影响。
我们知道Go是带有垃圾回收机制俗称GC的静态编程语言。使用Go编程时内存申请与释放在栈上还是在堆上分配以及新内存块的清零等等这一切都是自动的且对程序员透明。
但在C语言中上面说的这些都是程序员的责任。手工内存管理在带来灵活性的同时也带来了极大的风险其中最常见的就是内存泄露memory leak与悬挂指针dangling pointer问题。
内存泄露主要指的是**程序员手工在堆上分配的内存在使用后没有被释放free进而导致的堆内存持续增加**。而悬挂指针的意思是**指针指向了非法的内存地址**未初始化的指针、指针所指对象已经被释放等都是导致悬挂指针的主要原因。针对悬挂指针进行解引用dereference操作将会导致运行时错误从而导致程序异常退出的严重后果。
Go语言带有GC而C语言不带GC这都是由各自语言设计哲学所决定的。GC是不符合C语言的设计哲学的因为一旦有了GC程序员就远离了机器程序员直面机器的需求就无法得到满足了。并且一旦有了GC无论是在性能上还是在资源占用上都不可能做到极致了。
在C中手工管理内存到底是一种什么感觉呢作为一名有着十多年C开发经验的资深C程序员我只能告诉你**与内存斗,其乐无穷**这是在带GC的编程语言中无法体会到的。
### 语法形式
虽然C语言是Go的先祖并且Go也继承了很多C语言的语法元素但在变量/函数声明、行尾分号、代码块是否用括号括起、标识符作用域,以及控制语句语义等方面,二者仍有较大差异。因此,对 Go 已经很熟悉的程序员在初学 C 时受之前编码习惯的影响往往会踩一些“坑”。基于此我总结了Gopher学习C语言时需要特别注意的几点接下来我们具体看看。
**第一,注意声明变量时类型与变量名的顺序。**
前面说过Go与C都是静态编译型语言这就要求我们在使用任何变量之前需要先声明这个变量。但Go采用的变量声明语法颇似Pascal语言即**变量名在前,变量类型在后**这与C语言恰好相反如下所示
```plain
Go:
var a, b int
var p, q *int
vs.
C
int a, b;
int *p, *q;
```
此外Go支持短变量声明并且由于短变量声明更短小无需显式提供变量类型Go编译器会根据赋值操作符后面的初始化表达式的结果自动为变量赋予适当类型。因此它成为了Gopher们喜爱和重度使用的语法。但短声明在C中却不是合法的语法元素
```plain
int main() {
a := 5; // error: expected expression
printf("a = %d\n", a);
}
```
不过和上面的变量类型与变量名声明的顺序问题一样C编译器会发现并告知我们这个问题并不会给程序带来实质性的伤害。
**第二,注意函数声明无需关键字前缀。**
无论是C语言还是Go语言函数都是基本功能逻辑单元我们也可以说**C程序就是一组函数的集合。**实际上我们日常的C代码编写大多集中在实现某个函数上。
和变量一样函数在两种语言中都需要先声明才能使用。Go语言使用func关键字作为**函数声明的前缀**并且函数返回值列表放在函数声明的最后。但在C语言中函数声明无需任何关键字作为前缀函数只支持单一返回值并且返回值类型放在函数名的前面如下所示
```plain
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中条件表达式外围的小括号
```plain
// 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语言编写代码时务必要想着补上这些括号
```plain
// 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代码就会存在潜在的“风险”。我们来看一个例子
```plain
// 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编译运行后得到的结果如下
```plain
a = 1
a = 2
a = 3
a = ?
```
这显然不符合我们输出“a = 1”的预期。对于初学C的Gopher而言这个问题影响还是蛮大的因为这样编写的代码在C编译器眼中是完全合法的但所代表的语义却完全不是开发人员想要的。这样的程序一旦流入到生产环境其缺陷可能会引发生产故障。
一些 C lint 工具可以检测出这样的问题因此对于写C代码的Gopher我建议在提交代码前使用lint工具对代码做一下检查。
### 构建机制
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编译器。这些编译器原生不支持增量构建为了提升工程级构建的效率避免每次都进行全量构建我们通常会使用第三方的构建管理工具比如makeMakefile或CMake。考虑移植性时我们还会使用到configure文件用于在目标机器上收集和设置编译器所需的环境信息。
### 依赖管理
我在前面提过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的动态共享库在安装的目录下文件集合通常是这样
```plain
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中的版本号符合[语义版本规范](https://semver.org/)即major.minor.patch。当两个版本的major号一致说明是向后兼容的两个版本
* sonameshared 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应用的可重现构建还是比较难。在实践中我们通常会使用专门的构建主机项目组将该主机上的依赖管理起来进而保证每次构建所使用的依赖版本是可控的。同时应用部署的目标主机上的依赖版本也应该得到管理避免运行时出现动态共享库版本不匹配的问题。
### 代码风格
Go语言是历史上首次实现了代码风格全社区统一的编程语言。它基本上消除了开发人员在代码风格上的无休止的、始终无法达成一致的争论以及不同代码风格带来的阅读、维护他人代码时的低效。gofmt工具格式化出来的代码风格已经成为Go开发者的一种共识融入到Go语言的开发文化当中了。所以如果你让某个Go开发者说说gofmt后的代码风格是什么样的多数Go开发者可能说不出因为代码会被gofmt自动变成那种风格大家已经不再关心风格了。
而在C语言的世界代码风格仍存争议。但经过多年的演进以及像Go这样新兴语言的不断“教育”C社区也在尝试进行这方面的改进涌现出了像[clang-format](https://clang.llvm.org/docs/ClangFormat.html)这样的工具。目前,虽然还没有在全社区达成一致的代码风格(由于历史原因,这很难做到),但已经可以减少很多不必要的争论。
对于正在学习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 语言第一课》](https://time.geekbang.org/column/intro/100093501?tab=catalog)。在这门课里,我会用我十年 Gopher 的经验,带给你一条系统、完整的 Go 语言入门路径。
感谢你看到这里,如果今天的内容让你有所收获,欢迎把它分享给你的朋友。