从去年接触Go语言(以后简称”Golang”)到现在,已经有1年的时间了。感觉随着使用经验的积累,越发地喜爱这么编程语言。作为一个以C, C#语言出道,并自学了Java, Python, Golang的无证程序员,使用 了一段时间Go语言后,最明显的感觉就是:“这正式我所期盼的语言”。特别是写多了C语言代码,虽然感叹于C语言在语言上的简洁与性能上的高效,但是对于现实中C语言在开发方面的低效也总是吐槽不断。遭遇了Go语言之后,认为这门语言是C语言的最好传人(不要提C++)。
以下分享一些我认为Golang做的比较值得推崇的地方:
- Golang是直接编译成本地机器码,这就决定了其二进制是无法跨平台的。但是得益于Golang社区在各个平台上开发的编译器都很给力,而且提供了共通且强大的标准库,使得对于Go语言的开发者而言事实上不需要去太关注跨平台的问题(当然,可能会需要在特定平台上额外加一些分支代码以做优化或处理平台本身的差异)也能保证Golang程序拥有不弱于C的运行性能。这一点相比较C语言开发而言,实在时友善了太多:
- C语言为了兼容不通平台,往往会用到条件编译,如果没有实现好好设计跨平台的问题,代码中就会充斥着一大堆#ifdef ,导致可读性极差;但如果每个C语言项目都要去专门为跨平台好好设计一番,却又会导致项目的额外投入以及关注点扩散。总之实际开发中的效率不会高
- C语言标准库功能太薄弱,所以各个平台往往为了相似的基础功能折腾出一堆不通的基础库。尽管后来有了Posix规范,但毕竟这只是个弱约束。比如说巨硬的WinAPI,完全看不到一点Posix的影子,你也不能拿它怎么样。所以还是导致C语言项目会专门为了跨平台投入过多的人力和成本。
- Golang提供了一个内存管理机制(含GC)。这相对于原先的C语言开发而言,可算是大大地解放生产力了:
- C语言开发需要自行管理内存的分配与释放(其实说法不确切,标准库的malloc函数群其实是实现了一个内存池以避免触发过多系统调用),每一个C语言工程师肯定都与诸如内存泄露,重复释放,野指针误操作等问题搏斗过,费时费力。
- 不少C语言开发项目的工作量都耗费在了为本项目进行的内存管理的设计与开发上了。
得益于Golang自带内存管理功能,因此Golang提供了源生的string类型,而且颇为独特地将内置字符串的文字编码定义为UTF-8(Java,C#,Python3的内置字符串类型的文字编码都是UTF-16)。这样的设计带来了一些显而易见的好处:
相较于C语言开发,用Golang开发时终于不用自己去处理
char *
指针了。我们学C语言,道行尚浅往往会有个错觉,以为字符串一定会以’\0’结尾。这个理解得不深刻,往往会给我们带来一些麻烦。之后被坑得多了,才终于反应过来——NM就是一个字节数组。这个积累过程往往需要一些时间,归根结底还是没有一个专门的字符串类型(但事实上,通常情况下,我们要处理的数据有大多数都是字符串)。所以Golang提供了一个string类型也是理所当然。Golang对于String的实现也很符合实际,就是一个形如下方的结构体。这也导致了为什么在Golang中对string进行遍历时是基于字节而非字符来遍历的:struct String { byte* str; intgo len; };
与Java,C#,Python不同的是,Golang的字符串类型的文字编码是UTF8。我个人认为这是考虑到了互联网的需求。毕竟UTF16的每一个字符要占用两个字节。而考虑实际的情况,不管你是传一个html,还是传一个json报文,终究大部分字符还是那些在标准ASCII码范围内的字符。而且,假设数据真的按UTF16来传,还会牵扯到Big-endian和Little-endian的问题。而按UTF8编码传递则没有此问题。可以说,选择UTF-16的话,只对于语言本身的实现时可以减轻不少负担。所幸,Golang的设计者们也认识到了这一点。
Golang中对于函数库的构建,都是统一编译为静态库。当调用者要使用时是要在编译阶段就将函数库静态链接进来的。这个特性有时不被开发者理解,甚至有人认为是一种倒退。但如果我们仔细梳理一下就会发现用静态库时经过深思熟虑的选择:
- 部署方便,不会再存在Dependency Hell的问题(代价是程序加载时会多吃点内存)。Linux 系的程序经常陷入Dependency Hell的困扰;而Windows程序虽然稍微好一点,但它是因为在Windows系程序的最佳实践,通常时把所依赖的动态库一并发布。保证程序和程序之间井水不犯河水,以希图消除Dependency Hell的问题,但结果就是,把动态库(又称“共享库”)技术中的“共享”给丢了。
- 另一方面,从程序开发技术的历史来看,静态链接技术的诞生是早于动态链接技术的。甚至应该说,动态链接技术更像是静态链接当年面对贫瘠的内存与磁盘资源所做出的一个妥协性质的进化。既然在21世纪的今天,连PC机的内存与磁盘都已经不再是问题的情况下,作为一门新语言,选择在技术上返朴归真也是情有可原。更何况,如果从纯性能角度考量,静态库是要优于动态库的。
Golang还是一门没有实现“类”这个概念但却仍旧可以让你进行面向对象编程的语言。这样一来,概念就非常清晰了,和当年C语言的“简约”的思想一脉相承:
- 从80时代开始,“面向对象”思想就开始广为流传。它作为程序设计的一种方法论本身没有错,但问题是如果一个语言非要以支持不支持类这个概念来一刀切地区分自己是不是一个支持面向对象的语言。这就有点走火入魔了。在这方面,Java和C#是这类语言的极端代表,于是写一个入口函数main函数,都得先定义一个类,这实在是让人哭笑不得。更不用说Java和C#在后面的发展中被自己的“必须得有类”的这个概念所坑,先后都引入了一个称之为“静态类”的不伦不类的东西。万幸,Golang吸取了这个教训,没有搞出“类”这个东西。
- 面向对象的思想其实是与语言无关,事实上,就算是用C语言也仍然可以在“面向对象”思想的主导下实施开发,从一些优秀开源代码(如PostgreSQL)来看,用C语言也照样可以写出面向对象的风格。与C语言类似,Golang对于“面向对象”思想的三个基本特征也是予以了一定的支持,实现了对“面向对象”技术的“取取其精华去其糟粕”:
- 封装 —— 通过golang的package机制的数据/函数公开规则予以实现
- 继承 —— 通过Interface机制在思想上进行了实现(尽管Golang的接口时通过“组合”(Composite)来实现的),但是在语言特性层面却没有专门的“继承”功能
- 多态 —— 语言特性上没有专门的“多态”,甚至连函数的参数化多态(Overloading)也不支持
Golang保留了指针,有助于帮助开发者厘清数据之间关系的真实情况:
- 在函数参数方面,Java和C#算是捣糨糊的高手。Java为了所谓的“值传递”与“引用传递”,凭空捣鼓出了“原始类型”和“引用类型”这两个概念;C#则更进一步,在彻底地把所有类型都变成“类”了之后,捣鼓出了“值类型”和“引用类型”这两个概念。当然,正式因为这两个高级语言对开发者屏蔽了“地址”(也就是“指针”)这个概念后不得不生造出的概念。其实事情本可以很简单——只要一门语言中的函数调用的参数传递还是基于栈模型(有没有其他模型我不知道…),那么参数传递一定是把你希望传递的数据给拷贝走一份后再进行操作。因此,如果你想在一个函数中改变一个已有的数据结构实例中的属性(成员变量),那么你就老老实实地把这个实例的地址(指针)给传过去。
Golang通过”接受者”(receiver)这个概念,可以把一个函数变成某个数据类型的“方法”,从而相较于C语言的代码更易理解,易维护:
- 正如“类”不是必须的,“方法”其实也不是必须的。receiver这个特性更像是一个语法糖。只是,有了方法之后从代码层面就可以更明显地看出一个操作和一个数据结构之间时强联系还是弱联系。也正因为有了这个特性,所以Golang的“面向对象”的特征就“更加明显”了。
- 写C语言代码时,通常为了在代码中体现一个函数与一个数据结构是强联系,函数是数据结构的“方法”,所以通常会选择把这个数据结构的参数放在函数参数列表的第一个。也许Golang设计团队对于这个最佳实践也是深有感触之后才下决心做了“receiver”这个特性。
Golang中的
defer
机制可以有效增强代码的健壮性。用其他语言写代码,一旦涉及到数据库连接,文件句柄等资源操作时要格外小心,生怕有什么分支忘记关闭这些资源。有了defer机制,打可以在打开资源语句的下一条语句就用defer把关闭资源的操作塞入栈中,确保关闭操作一定被执行。并且有效地减少了函数中各种退出分支中冗长的关闭资源代码。至于Golang语言中以go关键字和chan关键字体现出的 “协程”和 “信道”这两个并发程序开发中的利器,已经有太多人盛赞过了,这里就不在赘述。
从以上可以看出,其实Golang语言的一些特性是源于C语言,但又更好地解决了C语言开发中的一些通点,使得在不太牺牲性能的前提下提升开发效率。Golang团队不愧是有C语言的发明者之一的Ken Tompson参与,所以能够有的放矢地去改善C语言在新时代形势下的一些课题。
然而,Golang中终究还是有一些我个人觉得比较遗憾的地方,也记录在此吧:
- 以
${变量名} 类型
这样类型在后的声明方式终究还是太小众了,特别是有时候定义函数时,写着写着就把参数列表写成了 类型 ${参数名} 这样的传统方式了。然后非要等编译出错才能反应过来(有时候还未必能反应过来)。 类型在后的声明方式,想了半天似乎也就只有SQL是这样的。 - 与C语言一样,Golang中无法定义参数类型不同,函数名与返回值类型相同的 所谓Function Overloading。因此一个功能相同仅仅是为了处理不同参数类型的一组函数,非得费破脑细胞取给各个函数想名字,这也是很无奈的。
- Golang因为是 强类型 & 静态类型 的关系(C语言是 弱类型 & 静态类型),很多在C语言,Java, C#中都可以隐式类型转换的写法在C语言中必须得做显式类型转换,代码就难免会多起来。感觉语言设计者想通过这种方式来甩锅啊……
不过,瑕不掩瑜。Golang还是一门非常值得学习的语言。2015年末我自学了Python,本来想好好研究一下Python的。结果一接触Golang之后,就立刻爱不释手,我想大概还是因为我是从C语言这一路学习过来的吧。所以我建议所有有C语言开发经验的人都取接触一下Golang,也许就会和我一样有“相见恨晚”的感觉。