数据结构论坛

首页 » 分类 » 定义 » 深思考丨解读Go语言的变革前夜
TUhjnbcbe - 2021/4/11 17:29:00

转自InfoQ

作者

郝林策划

张晓楠

本文是InfoQ“解读”年终技术盘点系列文章之一。

在作者去年年底撰写《解读Go语言的》的时候,绝没有想到年将会如此的不平凡。全球范围内的疫情在大大地限制了人们和企业的对外活动之余,还带来了一个副作用,即:线下活动向线上的迅速迁移。

实际上,对于这种迁移,我们国内的民营企业和事业单位早就在做了,只不过在年之前还没有这么急迫。不知道你发现了没有,在这一年,那些已经存在的远程办公、视频会议、在线医疗、在线教育等方面的基础设施和应用程序给予了我们莫大的支撑。即便说它们辅助保障了社会的正常运转,也不为过。

目前来看,全球的疫情还会存在一段时间。虽然这个事件本身绝对不值得高兴,但是反过来想,这会倒逼国内数字经济的大踏步前进,甚至飞跃。

从基础层面讲,数字经济的发展必须要有半导体等高精尖领域的强力支持。而从应用层面说,数字经济将会依托于云计算、大数据和人工智能。更具体地说,云计算是高级的基础设施,大数据和人工智能是建立在云计算之上的高级应用。Go语言,早已霸占了云计算的大半个江山,今后它也将在大数据和人工智能方面发挥重要作用。

趋势纵览

下面,我们依旧先从整体趋势上看看Go语言在今年的发展。

在全球范围内,从年的集体追新,到之后几年内的理性对待,再到年、年的“第二春”,直至年的升降大反差和年的新反弹。Go语言可谓是经历了诸多风风雨雨,持续地在各种好评和诟病之间砥砺前行,既得意过也失意过。

下图展现了TIOBEIndex(著名编程语言排行榜)对Go语言使用情况的最新统计。

图1-TIOBEIndex之Go语言(年12月)

图2-TIOBEIndex(年12月)

我们从上面这两幅图中可以看出,Go语言在今年的排名又有了大幅的提升。作者个人认为,这与gomod工具的转正和推广,以及“泛型”实现的排期确定是分不开的。

同时,据StackOverflow(全球最大的编程社区和问答网站)在前不久发布的一份开发者生存报告显示,Go语言在年是继Python、Java、C++和C之后、排名第五的通用型、全平台编程语言。如果把脚本语言和标记语言都算在内的话,它的总排名是第12名。

图3-StackOverflowServey-TheMostPopularLanguages

不但如此,Go语言在“最喜爱”和“最需要”的编程语言排行中也名列前茅。

图4-StackOverflowServey-TheMostLovedLanguages

图5-StackOverflowServey-TheMostWantedLanguages

我们可以看到,Go语言不但是开发者们非常喜爱的编程语言之一(“最喜爱”排行榜第五名),而且从实际应用的角度看,大家也是非常需要它的(“最需要”排行榜第三名)。作者认为,正因为Go语言有着崇尚简约和实用主义的编程哲学,广大软件工程师才会如此地爱用它。

更重要的是,Go软件工程师的薪资待遇也是相当不错的。

图6-StackOverflowServey-TheHighestSalaries

你可能会奇怪,为什么Perl程序员的薪资排在了第一位?这可能是因为物以稀为贵,Perl程序员在当代已经非常少见了。而在当今很热门的通用型编程语言中,从薪资角度来看,Scala语言、Go语言和Rust语言都有着相当大的优势。

当然了,这是在全球范围内的情况,并且参与这份调查的中国开发者并不多。很可惜,作者没能找出一份公认且权威的国内开发者调查报告。

不过,从作者的亲身经历来看,Go语言在国内恐怕并不亚于国际上的热度,甚至还要更火热一些。

作者这两年一直在断断续续地帮助一些互联网企业招聘Go软件工程师。除了作为老一代霸主的BAT(百度、阿里巴巴、腾讯)以及作为新一代翘楚的TMD(今日头条、美团、滴滴)之外,还有很多知名的互联网公司都在招聘掌握Go语言的开发工程师和系统运维人员。像PingCAP、七牛、哔哩哔哩、探探、Grab这些公司,在很早以前就混迹于Go语言圈子了。而在最近几年才进入Go语言圈子的知名公司还有华为、小米、映客、云智联、轻松筹、贝壳网、美菜网、游族网络等等。就连刚开始大红大紫的工业互联网领域,也有不少公司选择Go语言作为其主力开发语言之一。比如,积梦智能、必可测等。

这么多的优秀企业,以及活跃在技术社区中的大佬和新秀共同营造出了Go语言工程师的供需网络。作者认为,在国内的服务端编程市场,除了Java和PHP,就当属Go语言了。

年回顾

在了解了Go语言的发展趋势之后,我们再一起来看看它在年都有哪些重要的更新。

模块:终于稳定

自年2月份发布的1.14版本起,Go语言官方就开始正式地推广gomodules了。这说明它已经完全可以在生成环境中使用了。

如果你是老牌的Go工程师的话,那么肯定使用过像glide、dep、govendor这类第三方的依赖管理工具。这些工具都非常的优秀,并且在很大程度上解决了我们在项目开发过程中遇到的痛点。

不过,现在是时候迁移到gomodules了。Gomodules综合了这些第三方工具的优点,并经历了数年的设计和磨合,最终成为了Go程序依赖管理的官方工具。

即使现存的项目已经使用了前面提及的某一个依赖管理工具,那么也无需担心。我们只需要在Go项目的根目录中运行命令“gomodinit项目主模块的导入路径”就可以实现自动地迁移。gomod命令会读取那些已经存在的依赖配置文件,然后在其创建的go.mod文件中添加相应的内容。不过在这之后,我们最好再次使用gobuild命令构建一下项目并运行相应的单元测试,以确保一切正常。

还记得系统环境变量GOPATH吗?现在的go命令会自动地把项目所需的依赖包下载到它指向的第一个工作区目录中的pkg/mod子目录里。这里有一点需要注意,如果我们的项目中存在处于顶层的vendor目录,那么go命令将会优先在该目录中查找对应的依赖包。

如果我们使用的是Go语言的1.15版本,那么也可以通过设置系统环境变量GOMODCACHE来自定义上述存储依赖包的目录。这实际上是为以后彻底废弃GOPATH埋下的一个伏笔。

另外,执行一下gomodtidy命令也是一个很好的主意。这个命令会对gomodules的依赖配置文件进行整理,添加那些实际在用的依赖项,并去除那些未用的依赖项。换句话说,它会确保项目的依赖配置文件与项目源码的实际依赖相对应。

Go语言的大多数标准命令都得到了不同程度的改进以更加适配gomodules,包括一些标记(flag)的调整和一些行为上的优化。比如,goget命令在默认情况下不再会去更新非兼容版本的依赖库。不兼容的依赖库更新常常会让我们很恼火,但现在不会再出现这种情况了。

环境变量:跟进的调整

Go语言可识别的系统环境变量GOMODULE在1.14和1.15版本中的默认值都是auto。这意味着,go命令仅在当前目录或上层目录中存在go.mod文件的情况下才会以gomodules的方式运行,否则它就会退回到之前以GOPATH为中心的运行方式。不过,预计在明年发布的1.16版本中,Go语言将会把这个环境变量的默认值设置为on。也就是说,到了那时,GOPATH这一古老但能勾起我们满满回忆的东西终于要默默地退出了。

另外,我们现在可以在系统环境变量GOPROXY的值中使用管道符“|”了。在这之前,GOPROXY的值中只能出现分隔符“,”。如果一个代理URL跟在了分隔符后面,那么只有在前一个代理URL指向的服务器返回或错误时,go命令才会尝试使用当前的代理URL。现在,如果一个代理URL跟在了管道符后面,那么只要在访问前一个服务器时发生了(任何的)错误,go命令就会马上使用当前的代理URL。换句话说,新的管道符让我们多了一种容错的选择,即范围更广的容错。合理使用它,可以让我们更快地从可用的代理那里下载到所需的代码包。

顺便说一下,我们现在有了一个新的系统环境变量GOINSECURE。这个环境变量的存在单纯是为了让我们能够从非HTTPS的代码包服务器上下载依赖包(或者模块)。

有关环境变量的更多细节,我就不在这里说了。大家如果想了解的话,可以去参看Go语言的相关文档。

语言语法:可重叠的接口方法

我们都知道,Go语言这些年在语法方面一向很稳定,少有改动,更没有不兼容的变化出现。在年,Go语言只做了一项语法改进。这是关于接口声明的,并且完全保证了向后兼容性。我们下面来看一组代码示例。假设,我们有如下两个接口声明:

//MyReader代表可读的自定义接口。typeMyReaderinterface{io.ReadCloser}//MyWriter代表可写的自定义接口。typeMyWriterinterface{io.WriteCloser}

在Go1.14之前,这两个接口是无法内嵌到同一个接口声明中去的。就像这样:

//MyIO代表可输入输出的自定义接口。typeMyIOinterface{MyReaderMyWriterio.Closer}

这会让Go语言的编译器报错。报错的原因是:在同一个接口声明中发现了重复的方法声明。更具体地说,Go语言标准库中的io.ReadCloser接口和io.WriteCloser接口都包含了一个名为Close的方法,而分别内嵌了这两个接口的MyReader和MyWriter又已经嵌入到了接口MyIO之中。这导致MyIO接口里现在存在两个Close方法的声明。所以,MyIO的声明是无效的,并且无法通过编译。

这看上去是合规的,但却不一定合理。因为在很多情况下,我们想做的只是把多个接口合并在一起,而不在乎方法声明是否有重叠。我们一般认为,如果有重叠的方法,那么就当作一个就好了。很可惜,之前的Go语言并不这么认为。更重要的是,对于像上面那样深层次的接口内嵌问题,我们排查和解决起来都会很麻烦。有时候,这还会涉及到第三方库。

值得庆幸的是,自Go1.14开始,我们的这个合理诉求终于得到了满足。Go语言的语法已经认可了上述情况。这将给我们的接口整合工作带来极大的便利。

不过请注意,Go语言只接受在同一个接口声明中完全重叠的多个方法声明。换句话说,只有这些方法声明在名称和签名上完全一致,它们才能够合而为一。别忘了,接口中方法的签名包括了参数列表和结果列表。如果仅名称相同但签名不同,那么Go语言编译器照样会报错。例如:

//MyIO代表可输入输出的自定义接口。typeMyIOinterface{MyReaderMyWriterClose()}

由于这里最后面的方法声明Close()与接口io.ReadCloser和io.WriteCloser中的方法声明Close()error不完全一致(请注意结果声明上的差异),所以Go语言仍然会报出错误“duplicatemethodClose”,并使得程序编译不通过。

运行时内部:性能提升

Go语言每次的版本更新都会包含针对其运行时系统的性能提升。在年的优化中,有几点值得我们注意:

goroutine真正实现了异步的抢占。也就是说,现在即使是不包含任何函数调用的for循环也绝不会引起程序的死锁和垃圾回收的延误了。

defer语句的执行效率又得到了进一步的提升,额外的开销已几乎为零。所以,我们已经完全可以将defer语句用在对性能有严苛要求的应用场景中了。

运行时系统内部的内存分配器获得了改进。这使得在系统环境变量GOMAXPROCS有较高数值的情况下,内存的申请效率会得到大幅的提升。这间接地让运行时系统的整体性能明显提高。

除了以上这些,Go语言运行时系统还有一些比较小的改进,比如:panic函数已经可以正确地打印出我们传入的参数值当中的各种数值了、在将较小的整数值转换为接口值的过程中不再会引起内存分配、针对通道的非阻塞接收操作得到了进一步的优化,等等。

并发编程:一些微调

我们都知道,runtime.Goexit函数在被调用之后会中止当前的goroutine的运行。然而,在嵌套的panic/recover处理流程中,程序对它的调用会被忽略掉。虽然这种应用场景非常少见,但终归是一个问题。幸好自Go1.14开始,这个问题被彻底地解决了。

还要注意,如果runtime.Goexit函数被主goroutine中的代码调用了,那么它并不会终止整个Go程序的运行,而只会中止主goroutine而已。在这种情况下,其他的goroutine会继续运行。如果,在这之后,这些其他的goroutine都运行完毕了,那么程序就会崩溃。所以说,我们千万不要在主goroutine中调用runtime.Goexit函数。

在同步工具方面,现在的Go运行时系统会更加

1
查看完整版本: 深思考丨解读Go语言的变革前夜