Go 1.14 正式发布

4,544 阅读7分钟

Go 在 2019 年发布了Go 1.12Go 1.13。Go 1.13 的大部分变化在于工具链、运行时和库的实现。时隔半年,Go 1.14 正式发布。

和之前的版本一样,该版本保留了 Go 1 兼容性的承若,这个版本的大部分更新在工具链 、运行时库的性能提升方面。总的来说,还是在已有的基础上不断优化提成,大家期待的泛型还没有到来,下面一块看看新的变化吧。重大的更新如下:

  1. Go 命令中的 Module 支持现在可以投入生产
  2. 嵌入具有重叠方法集的接口
  3. defer 性能改进
  4. goroutine 支持异步抢占
  5. 工具的变化
  6. time.Timer 定时器性能大幅提升

Go 命令中的 Module 支持现在可以投入生产

现在,可以在 Go 命令中使用 Module 支持,以供生产使用,并且鼓励所有用户迁移到 Go Module 以进行依赖项管理。

嵌入具有重叠方法集的接口

Go 1.14 现在允许嵌入具有重叠方法集的接口:来自嵌入式接口的方法允许与 (嵌入) 接口中已存在的方法拥有相同的名称和签名。

在 Go 1.14 之前,如下的定义会编译报错。

type ReadWriteCloser interface {
	io.ReadCloser
	io.WriteCloser
}

因为 io.ReadCloser 和 io.WriteCloser 中 Close 方法重复了。Go 1.14开始允许相同签名的方法可以内嵌入一个接口中。与以前一样,接口中显式声明的方法必须保持唯一性。

defer 性能改进

Go1.14 提高了 defer 的大多数用法的性能,几乎 0 开销!defer 已经可以用于对性能要求很高的场景了。

关于 defer,在Go 1.13 版本已经做了一些的优化,相较于 Go 1.12,defer 大多数用法性能提升了 30%。而 Go 1.14 的此次改进之后更加高效!

goroutine 支持异步抢占

调度器使用的 G-M-P 模型。下面是相关的概念:

  • G(Goroutine):goroutine,由关键字 go 创建
  • M(Machine):在 Go 中称为 Machine,可以理解为工作线程
  • P(Processor): 处理器 P 是线程 M 和 Goroutine 之间的中间层(并不是CPU)

M 必须持有 P 才能执行 G 中的代码,P有自己本地的一个运行队列,由可运行的 G 组成,Go 语言调度器的工作原理就是处理器P的队列中选择队列头的 goroutine 放到线程 M 上执行,上图展示了 线程 M、处理器 P 和 goroutine 的关系。

每个P维护的G可能是不均衡的,调度器还维护了一个全局G队列,当P执行完本地的G任务后,会尝试从全局队列中获取G任务运行(需要加锁),当P本地队列和全局队列都没有可运行的任务时,会尝试偷取其他P中的G到本地队列运行(任务窃取)。

在 Go 1.1 版本中,调度器还不支持抢占式调度,只能依靠 goroutine 主动让出 CPU 资源,存在非常严重的调度问题:

  • 单独的 goroutine 可以一直占用线程运行,不会切换到其他的 goroutine,造成饥饿问题
  • 垃圾回收需要暂停整个程序(Stop-the-world,STW),如果没有抢占可能需要等待几分钟的时间,导致整个程序无法工作

Go 1.12 中编译器在特定时机插入函数,通过函数调用作为入口触发抢占,实现了协作式的抢占式调度。但是这种需要函数调用主动配合的调度方式存在一些边缘情况,就比如说下面的例子:

import (
	"runtime"
	"time"
)

func main() {
	runtime.GOMAXPROCS(1)
	go func() { //创建一个goroutine并挂起
		for {
		}
	}()
	time.Sleep(time.Millisecond) //main goroutine 优先调用了 休眠
	println("OK")
}

此时唯一的 P 会转去执行 for 循环所创建的 goroutine,进而 main goroutine 永远不会再被调度。换一句话说在Go1.14之前,上边的代码永远不会输出 OK。这是因为 Go 1.12 实现的协作式的抢占式调度是不会使一个没有主动放弃执行权、且不参与任何函数调用的 goroutine 被抢占。

Go1.14 通过实现了基于信号的真抢占式调度解决了上述问题,这是一个非常大的改动,Go 团队对已有的逻辑进行重构并为 goroutine 增加新的状态和字段来支持抢占。没有函数调用的循环不再能致使调度程序死锁或影响 GC。 除了 Windows/arm,darwin/arm,js/wasm 和 plan9/* 之外的所有平台均支持此功能。

实施抢占的结果是,在包括 Linux 和 macOS 系统在内的 Unix 系统上,使用 Go 1.14 构建的程序将比使用早期版本构建的程序接收更多的信号。这意味着使用诸如 syscall 或 golang.org/x/sys/unix 之类的软件包的程序将看到更多较慢的系统调用,并出现 EINTR 错误。这些程序将必须以某种方式处理那些错误,最有可能的循环是再次尝试系统调用。有关此内容的更多信息,请参见用于 Linux 系统的 man 7 signal 或用于其他系统的类似文档。

工具的变化

关于Go1.14中对工具的完善,主要说一下 go mod 和 go test,Go官方肯定希望开发者使用官方的包管理工具,Go1.14 完善了很多功能。 go mod 主要做了以下改进:

  • incompatiable versions:如果模块的最新版本包含go.mod文件,则除非明确要求或已经要求该版本,否则go get将不再升级到该模块的不兼容主要版本。直接从版本控制中获取时,go list还会忽略此模块的不兼容版本,但如果由代理报告,则可能包括这些版本。
  • go.mod文件维护:除了 go mod tidy 之外的 go 命令不再删除 require指令,该指令指定了间接依赖版本,该版本已由主模块的其他依赖项隐含。除了 go mod tidy 之外的 go 命令不再编辑 go.mod 文件,如果更改只是修饰性的。
  • Module下载:在module模式下,go命令支持 SVN 仓库,go 命令现在包括来自模块代理和其他HTTP服务器的纯文本错误消息的摘要。如果错误消息是有效的UTF-8,且包含图形字符和空格,只会显示错误消息。

go test -v 现在将 t.Log 输出流式传输,而不是在所有测试数据结束时输出。

time.Timer 定时器性能大幅提升

在 Go 1.10 之前的版本中,Go语言使用1个全局的四叉小顶堆维护所有的timer。由time.after,time.Tick,net.Conn.SetDeadline和friends所使用的内部计时器效率更高,锁争用更少,上下文切换更少。这是一项性能改进,不会引起任何用户可见的更改。

这边具体的改进,大家可以自行了解下,相对比较复杂,笔者正在学习最新的实现,后续专门讲这部分内容。

小结

Go 1.14 还有很多其他变更:

  • WebAssembly的变化
  • reflect包的变化
  • 很多其他重要的包(math,http等)的改变

Go语言的错误处理提案获得了社区很多人的支持,但是也有很多人反对,结论是:Go已经放弃了这一提案!这些思想还没有得到充分的发展,尤其考虑到更改语言的实现成本时,所以有关枚举和不可变类型,Go语言团队最近也是不给予考虑实现的。

Go1.14 也有一些计划中但是未完成的工作,Go1.14 尝试优化页分配器(page allocator),能够实现在 GOMAXPROCS 值比较大时,显著减少锁竞争。这一改动影响很大,能显著的提高 Go 并行能力,也会进一步提升 timer 的性能。但是由于实现起来比较复杂,有一些来不及解决的问题,要 delay 到 Go1.15 完成了。

关于 Go 1.14 的详细发布日志,可参见 golang.org/doc/go1.14。

参考

  1. go 1.14 golang.org/doc/go1.14
  2. 关于Go1.14,你一定想知道的性能提升与新特性

订阅最新文章,欢迎关注我的公众号

微信公众号