标签 runtime 下的文章

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

本文永久链接 – https://tonybai.com/2023/02/08/some-changes-in-go-1-20

美国时间2023年2月1日,唯一尚未退休的Go语言之父Robert Griesemer代表Go核心开发团队在Go官博撰文正式发布了Go 1.20版本。就像Russ Cox在2022 GopherCon大会所说的那样:Go2永不会到来,Go 1.x.y将无限延续

注:似乎新兴编程语言都喜欢停留在1.x.y上无限延续,譬如已经演化到1.67版本的Rust^_^。

《Go,13周年》之后,Go 1.20新特性在开发主干冻结(2022.11)之前,我曾写过一篇《Go 1.20新特性前瞻》,对照着Go 1.20 milestone中内容,把我认为的主要特性和大家简单过了一遍,不过那时Go 1.20毕竟没有正式发布,前瞻肯定不够全面,某些具体的点与正式版本可能也有差异!现在Go 1.20版本正式发布了,其Release Notes也补充完整了,在这一篇中,我再来系统说说Go 1.20版本中值得关注的那些变化。对于在前瞻一文中详细介绍过的特性,这里不会再重复讲解了,大家参考前瞻一文中的内容即可。而对于其他一些特性,或是前瞻一文中着墨不多的特性,这里会挑重点展开说说。

按照惯例,我们依旧首先来看看Go语法层面都有哪些变化,这可能也是多数Gopher们最为关注的变化点。

一. 语法变化

Go秉持“大道至简”的理念,对Go语法特性向来是“不与时俱进”的。自从Go 1.18大刀阔斧的加入了泛型特性后,Go语法特性就又恢复到了之前的“新三年旧三年,缝缝补补又三年”的节奏。Go 1.20亦是如此啊!Release Notes说Go 1.20版本在语言方面包含了四点变化,但看了变化的内容后,我觉得真正的变化只有一个,其他的都是修修补补。

1. 切片到数组的转换

唯一算是真语法变化的特性是支持切片类型到数组类型(或数组类型的指针)的类型转换,这个特性在前瞻一文中系统讲过,这里就不赘述了,放个例子大家直观认知一下就可以了:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/slice2arr.go

func slice2arrOK() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var arr = [7]int(sl)
    var parr = (*[7]int)(sl)
    fmt.Println(sl)  // [1 2 3 4 5 6 7]
    fmt.Println(arr) // [1 2 3 4 5 6 7]
    sl[0] = 11
    fmt.Println(arr)  // [1 2 3 4 5 6 7]
    fmt.Println(parr) // &[11 2 3 4 5 6 7]
}

func slice2arrPanic() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    fmt.Println(sl)
    var arr = [8]int(sl) // panic: runtime error: cannot convert slice with length 7 to array or pointer to array with leng  th 8
    fmt.Println(arr)     // &[11 2 3 4 5 6 7]

}

func main() {
    slice2arrOK()
    slice2arrPanic()
}

有两点注意一下就好:

  • 切片转换为数组类型的指针,那么该指针将指向切片的底层数组,就如同上面例子中slice2arrOK的parr变量那样;
  • 转换的数组类型的长度不能大于原切片的长度(注意是长度而不是切片的容量哦),否则在运行时会抛出panic。

2. 其他的修修补补

  • comparable“放宽”了对泛型实参的限制

下面代码在Go 1.20版本之前,比如Go 1.19版本中会无法通过编译:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable.go

func doSth[T comparable](t T) {
}

func main() {
    n := 2
    var i interface{} = n // 编译错误:interface{} does not implement comparable
    doSth(i)
}

之前,comparable约束下的泛型形参需要支持严格可比较(strictly comparable)的类型作为泛型实参,哪些是严格可比较的类型呢?Go 1.20的语法规范做出了进一步澄清:如果一个类型是可比较的,且不是接口类型或由接口类型组成的类型,那么这个类型就是严格可比较的类型,包括:

- 布尔型、数值类型、字符串类型、指针类型和channel是严格可比较的。
- 如果结构体类型的所有字段的类型都是严格可比较的,那么该结构体类型就是严格可比较的。
- 如果数组元素的类型是严格可比较的,那么该数组类型就是严格可比较的。
- 如果类型形参的类型集合中的所有类型都是严格可比较的,那么该类型形参就是严格可比较的。

我们看到:例外的就是接口类型了。接口类型不是“严格可比较的(strictly comparable)”,但未作为类型形参的接口类型是可比较的(comparable),如果两个接口类型的动态类型相同且值相等,那么这两个接口类型就相等,或两个接口类型的值均为nil,它们也相等,否则不等。

Go 1.19版本及之前,作为非严格比较类型的接口类型是不能作为comparable约束的类型形参的类型实参的,就像上面comparable.go中示例代码那样,但Go 1.20版本开始,这一要求被防控,接口类型被允许作为类型实参赋值给comparable约束的类型形参了!不过这么做之前,你也要明确一点,如果像下面这样两个接口类型底层类型相同且是不可比较的类型(比如切片),那么代码将在运行时抛panic:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable1.go

func doSth[T comparable](t1, t2 T) {
    if t1 != t2 {
        println("unequal")
        return
    }
    println("equal")
}

func main() {
    n1 := []byte{2}
    n2 := []byte{3}
    var i interface{} = n1
    var j interface{} = n2
    doSth(i, j) // panic: runtime error: comparing uncomparable type []uint8
}

Go 1.20语言规范借此机会还进一步澄清了结构体和数组两种类型比较实现的规范:对于结构体类型,Go会按照结构体字段的声明顺序,逐一字段进行比较,直到遇到第一个不相等的字段为止。如果没有不相等字段,则两个结构体字段相等;对于数组类型,Go会按数组元素的顺序,逐一元素进行比较,直到遇到第一个不相等的元素为止。如果没有不相等的元素,则两个数组相等。

  • unsafe包继续添加“语法糖”

Go 1.17版本在unsafe包增加Slice函数后,Go 1.20版本又增加三个语法糖函数:SliceData、String和StringData:

// $GOROOT/src/unsafe/unsafe.go
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte

值得注意的是由于string的不可更改性,String函数的参数ptr指向的内容以及StringData返回的指针指向的内容在String调用和StringData调用后不允许修改,但实际情况是怎么样的呢?

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/unsafe.go

func main() {
    var arr = [6]byte{'h', 'e', 'l', 'l', 'o', '!'}
    s := unsafe.String(&arr[0], 6)
    fmt.Println(s) // hello!
    arr[0] = 'j'
    fmt.Println(s) // jello!

    b := unsafe.StringData(s)
    *b = 'k'
    fmt.Println(s) // kello!

    s1 := "golang"
    fmt.Println(s1) // golang
    b = unsafe.StringData(s1)
    *b = 'h' // fatal error: fault, unexpected fault address 0x10a67e5
    fmt.Println(s1)
}

我们看到:unsafe.String函数调用后,如果我们修改了传入的指针指向的内容,那么该改动会影响到后续返回的string内容!而StringData返回
的指针所指向的内容一旦被修改,其结果要根据字符串的来源而定了。对于由可修改的底层数组“创建”的字符串(如s),通过StringData返回的指
针可以“修改”字符串的内容;而对于由字符串字面值初始化的字符串变量(如s1),其内容是不可修改的(编译器将字符串底层存储分配在了只读数据区),尝试通过指针修改指向内容,会导致运行时的段错误。

二. 工具链

1. Go安装包“瘦身”

这些年,Go发布版的安装包“体格”是越来越壮了,动辄100多MB的压缩包,以go.dev/dl页面上的go1.xy.linux-amd64.tar.gz为例,我们看看从Go 1.15版本到Go 1.19版本的“体格”变化趋势:

Go 1.15 - 116MB
Go 1.16 - 123MB
Go 1.17 - 129MB
Go 1.18 - 135MB
Go 1.19 - 142MB

如果按此趋势,Go 1.20势必要上到150MB以上。但Go团队找到了“瘦身”方法,那就是:从Go 1.20开始发行版的安装包不再为GOROOT中的软件包提供预编译的.a文件了,这样我们得到的瘦身后的Go 1.20版本的size为95MB!相较于Go 1.19,Go 1.20的安装包“瘦”了三分之一。安装包解压后这种体现更为明显:

➜  /Users/tonybai/.bin/go1.19 git:(master) ✗ $du -sh
495M    .
➜  /Users/tonybai/.bin/go1.20 git:(master) ✗ $du -sh
265M    .

我们看到:Go 1.20占用的磁盘空间仅为Go 1.19版本的一半多一点而已。 并且,Go 1.20版本中,GOROOT下的源码将像其他用户包那样在构建后被缓存到本机cache中。此外,go install也不会为GOROOT下的软件包安装.a文件。

2. 编译器

1) PGO(profile-guided optimization)

Go 1.20编译器的一个最大的变更点是引入了PGO优化技术预览版,这个在前瞻一文中也有对PGO技术的简单介绍。说白了点,PGO技术就是在原有compiler优化技术的基础上,针对程序在生产环境运行中的热点关键路径再进行一轮优化,并且针对热点代码执行路径,编译器会放开一些限制,比如Go决定是否对函数进行内联优化的复杂度上限默认值是80,但对于PGO指示的关键热点路径,即便函数复杂性超过80很多,也可能会被inline优化掉。

之前持续性能剖析工具开发商Polar Signals曾发布一篇文章《Exploring Go’s Profile-Guided Optimizations》,专门探讨了PGO技术可能带来的优化效果,文章中借助了Go项目中自带的测试示例,这里也基于这个示例带大家重现一下。

我们使用的例子在Go 1.20源码/安装包的\$GOROOT/src/cmd/compile/internal/test/testdata/pgo/inline路径下:

$ls -l
total 3156
-rw-r--r-- 1 tonybai tonybai    1698 Jan 31 05:46 inline_hot.go
-rw-r--r-- 1 tonybai tonybai     843 Jan 31 05:46 inline_hot_test.go

我们首先执行一下inline目录下的测试,并生成用于测试的可执行文件以及对应的cpu profile文件供后续PGO优化使用:

$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof
goos: linux
goarch: amd64
pkg: cmd/compile/internal/test/testdata/pgo/inline
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkA-8        1348        870005 ns/op
PASS
ok      cmd/compile/internal/test/testdata/pgo/inline   1.413s

接下来,我们对比一下不使用PGO和使用PGO优化,Go编译器在内联优化上的区别:

$diff <(go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m" 2>&1 | grep "can inline") <(go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m -pgoprofile inline_hot.pprof" 2>&1 | grep "can inline")
4a5,6
> ./inline_hot.go:53:6: can inline (*BS).NS with cost 106 as: method(*BS) func(uint) (uint, bool) { x := int(i >> lWSize); if x >= len(b.s) { return 0, false }; w := b.s[x]; w = w >> (i & (wSize - 1)); if w != 0 { return i + T(w), true }; x = x + 1; for loop; return 0, false }
> ./inline_hot.go:74:6: can inline A with cost 312 as: func() { s := N(100000); for loop; for loop }

上面diff命令中为Go test命令传入-run=none -tags=”" -gcflags=”-m -m”是为了仅编译源文件,而不执行任何测试。

我们看到,相较于未使用PGO优化的结果,PGO优化后的结果多了两个inline函数,这两个可以被inline的函数,一个的复杂度开销为106,一个是312,都超出了默认的80,但仍然可以被inline。

我们来看看PGO的实际优化效果,我们分为在无PGO优化与有PGO优化下执行100次benchmark,再用benchstat工具对比两次的结果:

$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof -count=100 > without_pgo.txt
$go test -o inline_hot.test -bench=. -gcflags="-pgoprofile inline_hot.pprof" -count=100 > with_pgo.txt

$benchstat without_pgo.txt with_pgo.txt
goos: linux
goarch: amd64
pkg: cmd/compile/internal/test/testdata/pgo/inline
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
    │ without_pgo.txt │            with_pgo.txt             │
    │     sec/op      │   sec/op     vs base                │
A-8       874.7µ ± 0%   872.6µ ± 0%  -0.24% (p=0.024 n=100)

注:benchstat的安装方法:\$go install golang.org/x/perf/cmd/benchstat@latest

我们看到,在我的机器上(ubuntu 20.04 linux kerenel 5.4.0-132),PGO针对这个测试的优化效果并不明显(仅仅有0.24%的提升),Polar Signals原文中的提升幅度也不大,仅为1.05%。

Go官方Release Notes中提到benchmark提升效果为3%~4%,同时官方也提到了,这个仅仅是PGO初始技术预览版,后续会加强对PGO优化的投入,直至对多数程序产生较为明显的优化效果。个人觉得目前PGO尚处于早期,不建议在生产中使用。

Go官方也增加针对PGO的ref页面,大家重点看看其中的FAQ,你会有更多收获!

2) 构建速度

Go 1.18泛型落地后,Go编译器的编译速度出现了回退(幅度15%),Go 1.19编译速度也没有提升。虽然编译速度回退后依然可以“秒杀”竞争对手,但对于以编译速度快著称的Go来说,这个问题必须修复。Go 1.20做到了这一点,让Go编译器的编译速度重新回归到了Go 1.17的水准!相对Go 1.19提升10%左右。

我使用github.com/reviewdog/reviewdog这个库实测了一下,分别使用go 1.17.1、go 1.18.6、go 1.19.1和Go 1.20对这个module进行go build -a构建(之前将依赖包都下载本地,排除掉go get环节的影响),结果如下:

go 1.20:
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  48.01s user 7.96s system 536% cpu 10.433 total

go 1.19.1:
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  54.40s user 10.20s system 506% cpu 12.757 total

go 1.18.6:
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  53.78s user 9.85s system 545% cpu 11.654 total

go 1.17.1:
$time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -a github.com/reviewdog/reviewdog/cmd/reviewdog  50.30s user 9.76s system 580% cpu 10.338 total

虽然不能十分精确,但总体上反映出各个版本的编译速度水准以及Go 1.20相对于Go 1.18和Go 1.19版本的提升。我们看到Go 1.20与Go 1.17版本在一个水平线上,甚至要超过Go 1.17(但可能仅限于我这个个例)。

3) 允许在泛型函数/方法中进行类型声明

Go 1.20版本之前下面代码是无法通过Go编译器的编译的:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/tools/compiler/local_type_decl.go
package main

func F[T1 any]() {
    type x struct{} // 编译错误:type declarations inside generic functions are not currently supported
    type y = x      // 编译错误:type declarations inside generic functions are not currently supported
}

func main() {
    F[int]()
}

Go 1.20改进了语言前端的实现,通过unified IR实现了对在泛型函数/方法中进行类型声明(包括定义type alias)的支持。

同时,Go 1.20在spec中还明确了哪些使用了递归方式声明的类型形参列表是不合法的

type T1[P T1[P]] …                    // 不合法: 形参列表中作为约束的T1引用了自己
type T2[P interface{ T2[int] }] …     // 不合法: 形参列表中作为约束的T2引用了自己
type T3[P interface{ m(T3[int])}] …   // 不合法: 形参列表中作为约束的T3引用了自己

type T4[P T5[P]] …                    // 不合法: 形参列表中,T4引用了T5 并且
type T5[P T4[P]] …                    //          T5引用了T4

type T6[P int] struct{ f *T6[P] }     // 正确: 虽然引用了T6,但这个引用发生在结构体定义中而不是形参列表中

4) 构建自举源码的Go编译器的版本选择

Go从Go 1.5版本开始实现自举,即使用Go实现Go,那么自举后的Go项目是谁来编译的呢?最初对应编译Go 1.5版本的Go编译器版本为Go 1.4。

以前从源码构建Go发行版,当未设置GOROOT_BOOTSTRAP时,编译脚本会默认使用Go 1.4,但如果有更高版本的Go编译器存在,会使用更高版本的编译器。

Go 1.18和Go 1.19会首先寻找是否有go 1.17版本,如果没有再使用go 1.4。

Go 1.20会寻找当前Go 1.17的最后一个版本Go 1.17.13,如果没有,则使用Go 1.4。

将来,Go核心团队计划一年升级一次构建自举源码的Go编译器的版本,例如:Go 1.22版本将使用Go 1.20版本的编译器。

5) cgo

Go命令现在在没有C工具链的系统上会默认禁用了cgo。更具体来说,当CGO_ENABLED环境变量未设置,CC环境变量未设置以及PATH环境变量中没有找到默认的C编译器(通常是clang或gcc)时,CGO_ENABLED会被默认设置为0。

3. 其他工具

1) 支持采集应用执行的代码盖率

在前瞻一文中,我提到过Go 1.20将对代码覆盖率的支持扩展到了应用整体层面,而不再仅仅是unit test。这里使用一个例子来看一下,究竟如何采集应用代码的执行覆盖率。我们以gitlab.com/esr/loccount这个代码统计工具为例,先修改一下Makefile,在go build后面加上-cover选项,然后编译loccount,并对其自身进行代码统计:

// /home/tonybai/go/src/gitlab.com/loccount
$make
$mkdir mycovdata
$GOCOVERDIR=./mycovdata loccount .
all          SLOC=4279    (100.00%) LLOC=1213    in 110 files
Go           SLOC=1724    (40.29%)  LLOC=835     in 3 files
asciidoc     SLOC=752     (17.57%)  LLOC=0       in 5 files
C            SLOC=278     (6.50%)   LLOC=8       in 2 files
Python       SLOC=156     (3.65%)   LLOC=0       in 2 files
... ...

上面执行loccount之前,我们建立了一个mycovdata目录,并设置GOCOVERDIR的值为mycovdata目录的路径。在这样的上下文下,执行loccount后,mycovdata目录下会生成一些覆盖率统计数据文件:

$ls mycovdata
covcounters.4ec45ce64f965e77563ecf011e110d4f.926594.1675678144659536943  covmeta.4ec45ce64f965e77563ecf011e110d4f

怎么查看loccount的执行覆盖率呢?我们使用go tool covdata来查看:

$go tool covdata percent -i=mycovdata
    loccount    coverage: 69.6% of statements

当然, covdata子命令还支持其他一些功能,大家可以自行查看manual挖掘。

2) vet

Go 1.20版本中,go工具链的vet子命令增加了两个十分实用的检测:

  • 对loopclosure这一检测策略进行了增强

具体可参见https://github.com/golang/tools/tree/master/go/analysis/passes/loopclosure代码

  • 增加对2006-02-01的时间格式的检查

注意我们使用time.Format或Parse时,最常使用的是2006-01-02这样的格式,即ISO 8601标准的时间格式,但一些代码中总是出现2006-02-01,十分容易导致错误。这个版本中,go vet将会对此种情况进行检查。

三. 运行时与标准库

1. 运行时(runtime)

Go 1.20运行时的调整并不大,仅对GC的内部数据结构进行了微调,这个调整可以获得最多2%的内存开销下降以及cpu性能提升。

2. 标准库

标准库肯定是变化最多的那部分。前瞻一文中对下面变化也做了详细介绍,这里不赘述了,大家可以翻看那篇文章细读:

  • 支持wrap multiple errors
  • time包新增DateTime、DateOnly和TimeOnly三个layout格式常量
  • 新增arena包
    … …

标准库变化很多,这里不能一一罗列,再补充一些我认为重要的,其他的变化大家可以到Go 1.20 Release Notes去看:

1) arena包

前瞻一文已经对arena包做了简要描述,对于arena包的使用以及最佳适用场合的探索还在进行中。著名持续性能剖析工具pyroscope的官方博客文章《Go 1.20 arenas实践:arena vs. 传统内存管理》对于arena实验特性的使用给出了几点好的建议,比如:

  • 只在关键的代码路径中使用arena,不要到处使用它们
  • 在使用arena之前和之后对你的代码进行profiling,以确保你在能提供最大好处的地方添加arena。
  • 密切关注arena上创建的对象的生命周期。确保你不会把它们泄露给你程序中的其他组件,因为那里的对象可能会超过arena的寿命。
  • 使用defer a.Free()来确保你不会忘记释放内存。
  • 如果你想在arena被释放后使用对象,使用arena.Clone()将其克隆回heap中。

pyroscope的开发人员认为arena是一个强大的工具,也支持标准库中保留arena这个特性,但也建议将arena和reflect、unsafe、cgo等一样纳入“不推荐”使用的包行列。这点我也是赞同的。我也在考虑如何基于arena改进我们产品的协议解析器的性能,有成果后,我也会将实践过程分享出来的。

2) 新增crypto/ecdh包

密码学包(crypto)的主要maintainer Filippo Valsorda从google离职后,成为了一名专职开源项目维护者。这似乎让其更有精力和动力对crypto包进行更好的规划、设计和实现了。crypto/ecdh包就是在他的提议下加入到Go标准库中的

相对于标准库之前存在的crypto/elliptic等包,crypto/ecdh包的API更为高级,Go官方推荐使用ecdh的高级API,这样大家以后可以不必再与低级的密码学函数斗争了。

3) HTTP ResponseController

以前HTTP handler的超时都是http服务器全局指定一个的:包括ReadTimeout和WriteTimeout。但有些时候,如果能在某个请求范围内支持这些超时(以及可能的其他选项)将非常有用。Damien Neil就创建了这个增加ResponseController的提案,下面是一个在HandlerFunc中使用ResponseController的例子:

http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
  ctl := http.NewResponseController(w, r)
  ctl.SetWriteDeadline(time.Now().Add(1 * time.Minute)) // 仅为这个请求设置deadline
  fmt.Fprintln(w, "Hello, world.") // 这个写入的timeout为1-minute
})

4) context包增加WithCancelCause函数

context包新增了一个WithCancelCause函数,与WithCancel不同,通过WithCancelCause返回的Context,我们可以得到cancel的原因,比如下面示例:

// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/library/context.go

func main() {
    myError := fmt.Errorf("%s", "myError")
    ctx, cancel := context.WithCancelCause(context.Background())
    cancel(myError)
    fmt.Println(ctx.Err())          // context.Canceled
    fmt.Println(context.Cause(ctx)) // myError
}

我们看到通过context.Cause可以得到Context在cancel时传入的错误原因。

四. 移植性

Go对新cpu体系结构和OS的支持向来是走在前面的。Go 1.20还新增了对freebsd在risc-v上的实验性支持,其环境变量为GOOS=freebsd, GOARCH=riscv64。但Go 1.20也将成为对下面平台提供支持的最后一个Go版本:

  • Windows 7, 8, Server 2008和Server 2012
  • MacOS 10.13 High Sierra和10.14 (我的安装了10.14的mac os又要在go 1.21不被支持了^_^)

近期Go团队又有了新提案:支持WASI(GOOS=wasi GOARCH=wasm),WASI是啥,它是WebAssembly一套与引擎无关(engine-indepent)的、面向非Web系统的WASM API标准,是WebAssembly脱离浏览器的必经之路!一旦生成满足WASI的WASM程序,该程序就可以在任何支持WASI或兼容的runtime上运行。不出意外,该提案将在Go 1.21或Go 1.22版本落地。

本文中的示例代码可以在这里下载。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商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
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

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

Go为什么能成功

本文永久链接 – https://tonybai.com/2022/12/07/why-go-succeed


大家在入门Go语言时,多埋头于Go语法,忙于练手或快速完成公司的项目,无暇思考。

但当大家到了要进阶,要冲刺高级阶段的时候,我建议你不能再稀里糊涂了。既然入了Go这个坑,在进入高级阶段前,我们最好在门口的“影壁墙”前驻留一下。

仔细思考一下我们投入这么多精力研究的Go为什么能成功,后续还能否持续成功下去。你要有自己的基本的判断,自我暗示也好,坚定信心也罢,我们要为继续攀登Go高峰进行蓄能

一. 头脑风暴一下Go成功的因素

相信无论针对哪个gopher群体做头脑风暴,让大家列举Go成功的因素,大家的主流答案也无外乎下图中这些:

图中的各个因素与Go的成功都不无干系,但是究竟哪个或哪几个是决定性的呢?

二. Go成功的根本因素

很显然,这个问题是没有标准答案,是见仁见智的。这里我列举一下我的观点,供大家参考。

直接上结论,我认为Go成功的根本因素就一个:Google

为什么这么说呢?下面我们展开来看(见下图)!

我将Go社区比做一支军队,而Go就是Go社区的武器,与其他编程语言搏杀,占地盘(fans)。下面我们就来解构一下这支军队的构成以及为什么这支军队目前有诸多成功案例!

1. Google为Go社区提供了统帅与武器

众所周知,2007年Google的三名员工Robert Griesemer、Rob Pike和Ken Thompson(retire很早,精神上领袖,给予Go名誉上的背书)一起发明了Go语言,2009年Go开源后,Go社区逐渐形成。统帅是一支军队的灵魂,他们做出了影响Go和Go社区的最初的也是最重要的决策和这背后的Go设计哲学!

a) 设计决策

在2022年,Go团队在美国计算机学会通讯(Communications of the ACM)期刊上发表paper:《Go编程语言与环境》,对当年做出的诸多决策做了细致说明,这里对其中两个最重要的决策做简单说明:

  • Go旨在成为一个编程环境

Go语言之父们认为语言特性仅是编程语言的一部分,而编程环境特性与语言特性同等重要,这些环境特性包括:库、工具、惯例和针对软件工程的整体做法,它们都对使用Go语言编程提供了支持,不可或缺。而这些环境特性恰恰是在传统的编程语言设计中并没有受到应有的重视的。

这样的决策让Go在开源之初就为开发者提供了使用Go进行编程所需的几乎一切:包括功能丰富、开箱即用的标准库以及全面的工具集,代码格式化、代码静态检查、依赖关系管理、构建(包括跨平台交叉编译)、测试、性能剖析、查看和生成文档等,并且这些工具集在今天都统一放在了go命令的下面。这个决策也帮助Go在开源后吸引了第一批Go社区用户。

  • Go的一致性的表现

Go的一个目标是让它在不同的实现、执行环境中,甚至在不同的时间内表现出相同的行为。所以,Go语言尽可能地规定了一致的结果。比如:Go程序生命周期内一致的性能(相对于使用JIT慢启动的语言)、一致的GC的开销等。甚至对于最常见的编程错误提供了明确定义的语义,这有助于可理解性和调试,而不是像C/C++中那样,充斥着各种未定义的行为。

而我认为最重要的一致性则是从2012年发布的Go 1.0开始,Go团队公开承诺只对语言和标准库进行向后兼容的修改,这样程序在编译到较新的Go版本时可以继续运行而不发生变化。这一承诺对业界产生了吸引力,它不仅鼓励了那些长声明周期的工程项目(比如Google内部的一些大型项目或者像Kubernetes这样的社区顶级项目),也鼓励了其他努力,如书籍、培训课程和第三方软件包的繁荣生态系统。这一一致性的决策也为Go招募了相当数量的拥趸。 Go1兼容性,同样可以避免社区分裂(像python2/python3那样),即便是10多年来变更最大的泛型语法落地,也没有违反Go1兼容性,这实属不易。

b) 设计哲学

上述的设计决策的背后蕴含着Go语言之父们的设计哲学。

  • 简单

Tony Hoare在1980年图灵奖演讲中说了这样的观点:“我的结论是,构建软件设计有两种方法:一种方法是让它变得如此简单,显然没有缺陷,另一种方法是让它变得如此复杂,以至于没有明显的缺陷。第一种方法要困难得多。它需要同样的技能,奉献,洞察力,甚至灵感,就像发现作为自然复杂现象基础的简单物理定律一样。它还要求愿意接受受物理,逻辑和技术限制的目标,并在无法实现冲突目标时接受妥协。”

Go选择的正是Tony Hoare演进中的第一种构建软件的设计方法。Rob Pike说过的一句Go流行谚语“less is exponentially more”与此异曲同工。Go的语法简单,API简单,这些为Gopher提供了极大便利,但这些简单的背后其实是Go团队长时间的复杂的思考与实现,努力将语法和API简化为最小、最有用、最接近本质的努力工作。

同时,简单意味着可读性、可维护性,意味着代码的清晰。另一句Go谚语“Clear is better than clever”告诫Gopher们编写平淡如水的Go代码才是“政治正确”的,不要炫技。

  • 并发

多核时代,Go将并发作为语言内置特性。Go内置并发原语,包括goroutine、channel、select等。

Go鼓励在较高级别使用并发性,特别是通过通信的方式。我们耳熟能详的一句Go谚语是“Don’t communicate by sharing memory. Share memory by communicating”就是并发哲学的外在体现。

  • 组合

Go拥有类型,类型可以有method,这似乎像是一种面向对象style的实现,但Go并没有OO语言那种类型层次体系(type hierarchy),在Go中,组合才是Go类型之间建立联系的最主要手段,而interface和类型嵌入恰是这种组合哲学的具体体现。

  • 面向工程

2012年, Go开源元年,Rob Pike就在SPLASH 2012大会上以“Google的Go:为软件工程服务的语言设计”为题,讲解了Go是如何围绕Google内部存在的软件工程问题进行有针对性的语言设计的。可以看出,Go从诞生伊始就将解决软件工程领域问题作为语言的目标。同时,我们看到面向工程这个哲学与上面的旨在成为一个编程环境的决策息息相关。

除了统帅之外,Go社区的治理架构也是以Google“将领”为核心的,我们继续来看。

2. Google出钱:以Google“将领”(googler and ex-googler)为核心的Go社区治理架构

Go开源10年了,Go社区形成了以Googler和ex-googler(前google员工)为核心的Go社区治理架构,这些人就是上图中的那些“将领”,他们是Go项目某个细分领域,比如:编译器、运行时goroutine调度、GC、内存管理、网络、安全等的领头人。根据Go项目一名产品经理的描述:2021年,Google Go项目的专职人员多达50多人,Google这个“亲爹”在金钱的投入上显然表现的十分大方,不得不承认:在编程语言领域里,有个有钱的“亲爹”就是好

这种以googler和Ex-googler为开源社区治理核心的架构决定了Go社区采用的是一种我称之为“民主集中制”的决策机制。在Go社区你不要幻想会有绝对的公平投票,Go项目决策向来是由少数Googler和ex-googler主导的。这样意味着很多情况下,核心治理团队的人提出的proposal以及Google内部gopher提出proposal很容易被accept,而来自外部社区的proposal要想被accept,可能难度就要大一些。怎么说呢?Google的方案不一定总是最好的,但我们也不能不承认多数情况下,Googler提的proposal还是更优的,并且通常这些proposal对应的实现都已经在google内部测试过了,甚至和Go决策组在公司内部“吹过风”,如果你是Go社区的决策人,你会怎么做呢?你是更相信Googler还是外部一个没有任何背景的gopher呢?

我觉得在Google依然引领IT前沿的今天以及未来若干年,这种机制可能还是有利于Go的蓬勃发展的。

3. Google为Go社区提供战场/试验场

就像上面所说的那样,Go是有着非常鲜明Google烙印的编程语言,除了Go语言之父都来自google,Go社区治理架构的核心都来自Google和前google员工外,Google内部为Go的设计提供了足够的一流的问题域,也为Go的真实应用提供了试验场和真实战场,即便Go至今没有成为Google内部的第一语言。面向Google的一手且一流问题域,让Go设计者和Go开发者能够获得一手的反馈,从而对Go做进一步的打磨。

举几个例子:

  • Google内部的单一代码仓库让Go最初设计了不带版本的go get(后在社区的强烈要求下引入了go module,go get才支持版本号);
  • googler反馈,google内部工具超好用,这一定程度让Go团队认识到向Gopher提供完善的go工具链的重要性;
  • Google内部的多核与网络服务让Go设计者决定内置原生goroutine以应对多核时代的应用开发;
  • Google内性能与开发效率并重让Go设计者决定设计一门带gc的静态编程语言,将内存管理、并发管理下沉到runtime,这与近两年出现的服务网格, dapr等概念的思路一致;

  • Google内部大规模人员协作让Go决定面向软件工程,不仅要设计好语言特性,还要提供体验良好的编程环境(工具链、标准库等);
  • Google超大规模的系统构建慢让Go决定提供快速的构建能力,为此对包格式与包依赖做了精心的设计;
  • Google内部长期维护的系统(生命周期长) 让Go团队决定支持Go1兼容性并提供支持重构的语法,比如type alias等;
  • Google认为安全十分重要,促使Go提供了go sumdb和对sbom的良好支持;

同时Google内部系统为了支持Go的内部试验也是不遗余力,比如:每当Go发布大版本的RC版本,甚至是Beta版本时,Google App Engine都会首当其冲的充当“小白鼠”,在生产环境支持尚未发布正式版的Go。

另外Google在业内的领先性也让“近水楼台”的Go受益,比如像容器调度编排这样的平台,Google十年前就有了(borg),后续Googler以另起开源项目的方式将其中经验外溢输出,让Kubernetes最终选择了Go作为开发语言,从而成为Go的最大的也是最典型的成功战例。

综上,我们看到Google对Go成功的决定性作用,这种作用可决不能被理解为简单的金钱上的支撑。

三. Go语言演进历史

进入Go高级阶段后,对Go语言的演化历史要知道,当然能做到如数家珍更佳,即便不能,也要能记住Go语言的主要演化历史:

  • 2007年9月,Go语言诞生;
  • 2009年11月,Go正式开源;
  • 2012年3月,Go 1.0发布,同时Go1兼容性承诺官宣;
  • 2014年12月,Go 1.4版本发布,这是最后一个编译器和runtime由C语言实现的版本;
  • 2015年8月,Go 1.5版本发布,这个版本Go实现了自举(用go编译go),同时编译器和runtime中的绝大部分c代码都换成了go,新版gc让延迟大幅降低;
  • 2018年8月,Go 1.11版本发布,go module被正式引入;
  • 2022年3月,Go 1.18版本发布,Go泛型语法正式落地。

四. 小结

C++之父说过:“世上只有两种编程语言:一种是总是被人抱怨的,一种是从来没人用的”。

Go属于前者。世界上没有完美的编程语言,Go经过十年的打磨已经有了长足的进步,并且取得了不错的战绩,尤其是在云基础设施和云原生因公领域,就连Rob Pike也承认Go确实已成为云基础架构的语言。而这个Go走向成功的过程中,Google起着根本性的作用。

不过中国古语有云:成也萧何,败也萧何!目前Google仍然引领IT技术前沿,这对Go的发展来说是一个利好,也会不断推动Go向着好的方向发展。

但我大胆预测一下:“成也Google,败也Google”,一旦Google开始走下坡路的那天,Go语言成功的根基就不在了,Go还能像今天这样顺风顺水么?如果Go社区治理结构不重构,很可能不会再有今天这样的良好状态。大家觉得呢?

五. 参考资料

-《Go编程语言与环境:万字长文复盘导致Go语言成功的那些设计决策
-《Go内存模型》- https://research.swtch.com/gomm
-《Go语言真正的问题》 – https://vanitynotes.com/posts/20221101-the-real-problem-with-go


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

img{512x368}
img{512x368}

img{512x368}
img{512x368}

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

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系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