Go2 Transition
Go2的设计草案在Go 2 Draft Designs或者这里,而Go1如何迁移到Go2也是我个人特别关心的问题,Python2和Python3的那种不兼容的迁移方式简直就是噩梦一样的记忆。Go的提案中,有一个专门说了迁移的问题,参考Go2 Transition。
Go2 Transition还不是最终方案,不过它也对比了各种语言的迁移,还是很有意思的一个总结。这个提案描述了在非兼容性变更时,如何给开发者挖的坑最小。
目前Go1的标准库是遵守兼容性原则的,参考Go 1 compatibility guarantee,这个规范保证了Go1没有兼容性问题,几乎可以没有影响的升级比如从Go1.2升级到Go1.11。几乎
的意思,是很大概率是没有问题,当然如果用了一些非常冷门的特性,可能会有坑,我们遇到过json解析时,内嵌结构体的数据成员也得是exposed的才行,而这个在老版本中是可以非exposed;还遇到过cgo对于链接参数的变更导致编译失败,这些问题几乎很难遇到,都可以算是兼容的吧,有时候只是把模糊不清的定义清楚了而已。
Go2在语言和标准库上,会打破Go1的兼容性规范,也就是和Go1不再兼容。不过Go是分布式开源社区在维护,不能依赖于flag day,还是要容许不同Go版本写的package的互操作性。先了解下各个语言如何考虑兼容性:
-
C是严格向后兼容的,很早写的程序总是能在新的编译器中编译。另外新的编译器也支持指定之前的标准,比如
-std=c90
使用ISO C90
标准编译程序。关键的特性是编译成目标文件后,不同版本的C的目标文件,能完美的链接成执行程序。C90实际上是对之前K&R C
版本不兼容的,主要引入了volatile
关键字,还有整数精度问题,还引入了trigraphs,最糟糕的是引入了undefined行为比如数组越界和整数溢出的行为未定义。从C上可以学到的是:后向兼容非常重要;非常小的打破兼容性也问题不大特别是可以通过编译器选项来处理;能将不同版本的目标文件链接到一起是非常关键的;undefined行为严重困扰开发者容易造成问题。 - C++也是ISO组织驱动的语言,和C一样也是向后兼容的。C++和C一样坑爹的地方坑到吐血,比如undefined行为等等。尽管一直保持向后兼容,但是新的C++代码比如C++11看起来完全不同,这是因为有新的改变的特性,比如很少会用裸指针,比如range代替了传统的for循环,这导致熟悉老C++语法的程序员看新的代码非常难受甚至看不懂。C++毋庸置疑是非常流行的,但是新的语言标准在这方面没有贡献。从C++上可以学到的新东西是:尽管保持向后兼容,语言的新版本可能也会带来巨大的不同的感受(保持向后兼容并不能保证能持续看懂)。
- Java也是向后兼容的,是在字节码层面和语言层面都向后兼容,尽管语言上不断的新增了关键字。Java的标准库非常庞大,也不断的在更新,过时的特性会被标记为deprecated并且编译时会有警告,理论上一定版本后deprecated的特性会不可用。Java的兼容性问题主要在JVM解决,如果用新的版本编译的字节码,得用新的JVM才能执行。Java还做了一些前向兼容,这个影响了字节码啥的(我本身不懂Java,作者也不说自己不是专家,我就没仔细看了)。Java上可以学到的新东西是:要警惕因为保持兼容性而限制语言未来的改变。
-
Python2.7是2010年发布的,目前主要是用这个版本。Python3是2006年开始开发,2008年发布,十年后的今天还没有迁移完成,甚至主要是用的Python2而不是Python3,这当然不是Go2要走的路。看起来是因为缺乏向后兼容导致的问题,Python3刻意的和之前版本不兼容,比如print从语句变成了一个函数,string也变成了Unicode(这导致和C调用时会有很多问题)。没有向后兼容,同时还是解释型语言,这导致Python2和3的代码混着用是不可能的,这以为着程序依赖的所有库必须支持两个版本。Python支持
from __future__ import FEATURE
,这样可以在Python2中用Python3的特性。Python上可以学到的东西是:向后兼容是生死攸关的;和其他语言互操作的接口兼容是非常重要的;能否升级到新的语言是由调用的库支持的。 - Perl6是2000年开始开发的,15年后才正式发布,这也不是Go2应该走的路。这么漫长的主要原因包括,刻意没有向后兼容,只有语言的规范没有实现而这些规范不断的修改。Perl上可以学到的东西是:不要学Perl;设置期限按期交付;别一下子全部改了。
特别说明的是,非常高兴的是Go2不会重新走Python3的老路子,当初被Python的版本兼容问题坑得不要不要的。
虽然上面只是列举了各种语言的演进,确实也了解得更多了,有时候描述问题本身,反而更能明白解决方案。C和C++的向后兼容确实非常关键,但也不是他们能有今天地位的原因,C++11的新特性到底增加了多少DAU呢,确实是值得思考的。另外C++11加了那么多新的语言特性,比如WebRTC代码就是这样,很多老C++程序员看到后一脸懵逼,和一门新的语言一样了,是否保持完全的兼容不能做一点点变更,其实也不是的。
应该将Go的语言版本和标准库的版本分开考虑,这两个也是分别演进的,例如alias是1.9引入的向后兼容的特性,1.9之前的版本不支持,1.9之后的都支持。语言方面包括:
- Language additions新增的特性,比如1.9新增的type alias。这些向后兼容的新特性,并不要求代码中指定特殊的版本号,比如用了alias的代码不用指定要1.9才能编译,用之前的版本会报错。向后兼容的语言新增的特性,是依靠程序员而不是工具链来维护的,要用这个特性或库升级到要求的版本就可以。
-
Language removals删除的特性。比如有个提案#3939去掉
string(int)
,字符串构造函数不支持整数,假设这个在Go1.20版本去掉,那么Go1.20之后这种string(1000)
代码就要编译失败了。这种情况没有特别好的办法能解决,我们可以提供工具,将代码自动替换成新的方式,这样就算库维护者不更新,使用者自己也能更新。这种场景引出了指定最大版本,类似C的-std=C90
,可以指定最大编译的版本比如-lang=go1.19
,当然必须能和Go1.20的代码链接。指定最大版本可以在go.mod中指定,这需要工具链兼容历史的版本,由于这种特性的删除不会很频繁,维护负担还是可以接受的。 -
Minimum language version最小要求版本。为了可以更明确的错误信息,可以允许模块在
go.mod
中指定最小要求的版本,这不是强制性的,只是说明了这个信息后编译工具能明确的给出错误,比如给出应该用具体哪个版本。 - Language redefinitions语言重定义。比如Go1.1时,int在64位系统中长度从4字节变成了8字节,这会导致很多潜在的问题。比如#20733修改了变量在for中的作用域,看起来是解决潜在的问题,但也可能会引入问题。引入关键字一般不会有问题,不过如果和函数冲突就会有问题,比如error: check。为了让Go的生态能迁移到Go2,语言重定义的事情应该尽量少做,因为我们不再能依赖编译器检查错误。虽然指定版本能解决这种问题,但是这始终会导致未知的结果,很有可能一升级Go版本就挂了。我觉得对于语言重定义,应该完全禁止。比如#20733可以改成禁止这种做法,这样就会变成编译错误,可能会帮助找到代码中潜在的BUG。
- Build tags编译tags。在指定文件中指定编译选项,是现有的机制,不过是指定的release版本号,它更多是指定了最小要求的版本,而没有解决最大依赖版本问题。
-
Import go2导入新特性。和Python的特性一样,可以在Go1中导入Go2的新特性,比如可以显示的导入
import "go2/type-aliases"
,而不是在go.mod中隐式的指定。这会导致语言比较复杂,将语言打乱成了各种特性的组合。而且这种方式一旦使用,将无法去掉。这种方式看起来不太适合Go。
如果有更多的资源来维护和测试,标准库后续会更快发布,虽然还是6个月的周期。标准库方面的变更包括:
-
Core standard library核心标准库。有些和编译工具链相关的库,还有其他的一些关键的库,应该遵守6个月的发布周期,而且这些核心标准库应该保持Go1的兼容性,比如
os/signal
、reflect
、runtime
、sync
、testing
、time
、unsafe
等等。我可能乐观的估计net
,os
, 和syscall
不在这个范畴。 -
Penumbra standard library边缘标准库。它们被独立维护,但是在一个release中一起发布,当前核心库大部分都属于这种。这使得可以用
go get
等工具来更新这些库,比6个月的周期会更快。标准库会保持和前面版本的编译兼容,至少和前面一个版本兼容。 -
Removing packages from the standard library去掉一些不太常用的标准库,比如
net/http/cgi
等。
如果上述的工作做得很好的话,开发者会感觉不到有个大版本叫做Go2,或者这种缓慢而自然的变化逐渐全部更新成了Go2。甚至我们都不用宣传有个Go2,既然没有C2.0为何要Go2.0呢?主流的语言比如C、C++和Java从来没有2.0,一直都是1.N的版本,我们也可以模仿他们。事实上,一般所认为的全新的2.0版本,若出现不兼容性的语言和标准库,对用户也不是个好结果,甚至还是有害的。
Links
由于简书限制了文章字数,只好分成不同章节:
- Overview 为何Go有时候也叫Golang?为何要选择Go作为服务器开发的语言?是冲动?还是骚动?Go的重要里程碑和事件,当年吹的那些牛逼,都实现了哪些?
- Could Not Recover 君可知,有什么panic是无法recover的?包括超过系统线程限制,以及map的竞争写。当然一般都能recover,比如Slice越界、nil指针、除零、写关闭的chan等。
- Errors 为什么Go2的草稿3个有2个是关于错误处理的?好的错误处理应该怎么做?错误和异常机制的差别是什么?错误处理和日志如何配合?
- Logger 为什么标准库的Logger是完全不够用的?怎么做日志切割和轮转?怎么在混成一坨的服务器日志中找到某个连接的日志?甚至连接中的流的日志?怎么做到简洁又够用?
- Interfaces 什么是面向对象的SOLID原则?为何Go更符合SOLID?为何接口组合比继承多态更具有正交性?Go类型系统如何做到looser, organic, decoupled, independent, and therefore scalable?一般软件中如果出现数学,要么真的牛逼要么装逼。正交性这个数学概念在Go中频繁出现,是神仙还是妖怪?为何接口设计要考虑正交性?
- Modules 如何避免依赖地狱(Dependency Hell)?小小的版本号为何会带来大灾难?Go为什么推出了GOPATH、Vendor还要搞module和vgo?新建了16个仓库做测试,碰到了9个坑,搞清楚了gopath和vendor如何迁移,以及vgo with vendor如何使用(毕竟生产环境不能每次都去外网下载)。
- Concurrency & Control 服务器中的并发处理难在哪里?为什么说Go并发处理优势占领了云计算开发语言市场?什么是C10K、C10M问题?如何管理goroutine的取消、超时和关联取消?为何Go1.7专门将context放到了标准库?context如何使用,以及问题在哪里?
- Engineering Go在工程化上的优势是什么?为什么说Go是一门面向工程的语言?覆盖率要到多少比较合适?什么叫代码可测性?为什么良好的库必须先写Example?
- Go2 Transition Go2会像Python3不兼容Python2那样作吗?C和C++的语言演进可以有什么不同的收获?Go2怎么思考语言升级的问题?
- SRS & Others Go在流媒体服务器中的使用。Go的GC靠谱吗?Twitter说相当的靠谱,有图有真相。为何Go的声明语法是那样?C的又是怎样?是拍的大腿,还是拍的脑袋?