Go 在 2019 年发布了Go 1.12与Go 1.13。Go 1.13 的大部分变化在于工具链、运行时和库的实现。时隔半年,Go 1.14 正式发布。
和之前的版本一样,该版本保留了 Go 1 兼容性的承诺,这个版本的大部分更新在工具链 、运行时库的性能提升方面。总的来说,还是在已有的基础上不断优化提成,大家期待的泛型还没有到来,下面一块看看新的变化吧。重大的更新如下:
- Go 命令中的 Module 支持现在可以投入生产
- 嵌入具有重叠方法集的接口
- defer 性能改进
- goroutine 支持异步抢占
- 工具的变化
- 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 的详细发布日志,可参见 https://golang.org/doc/go1.14。
参考
- go 1.14 https://golang.org/doc/go1.14
- 关于Go1.14,你一定想知道的性能提升与新特性 https://studygolang.com/articles/26529?fr=sidebar