核心库

log/slog

原生支持的结构化日志终于在go 1.21版本落地了,其路径为log/slog。
Go标准库还增加了testing/slogtest包,来帮助大家验证slog.Handler的实现,这个是以前没有的。
slog是一个高质量、高性能的结构化日志实现,这里建议大家在启动新Go项目时,尽量采用log/slog作为日志输出的方案。

slices、maps和cmp

在Go实验库“孵化”了一年多的几个泛型包slices、maps和cmp终于在Go 1.21版本中正式加入到标准库中了。
slices切片包提供了针对切片的常用操作,slices包使用了泛型函数,可处理任何元素类型的切片。同理,maps包与slices包地位相似,只不过操作对象换成了map类型变量,它可以处理任意类型键和元素类型的map。
cmp包是slices包依赖的包,这个包非常简单且内聚,它仅提供了与compare和ordered相关的约束类型定义与简单泛型函数:
以上三个包没有太多可说的,都是一些utils类的函数,大家在日常开发中记得用就ok了,基于泛型的实现以及unified中间代码的优化,这些函数的性能相对于基于interface实现的通用工具函数要高出一些。
注:在Go 1.21正式版发布之前,Go team删除了maps包中原有的Keys和Values函数,其原因是要在后续版本中提供iter包。

其他变化

  • 增加errors.ErrUnsupported
标准库各个包都有类似unsupported的Error类型的定义,第三方包更是多如牛毛。Go 1.21在errors包中增加了ErrUnsupported,旨在统一后续对unsupported的错误判定。不过在你的函数或方法中不要直接返回errors.ErrUnsupported,要么用自定义error包装(wrap) errors.ErrUnsupported,要实现Is方法。目的是使得你自己的Error类型提供的unsupported error满足:errors.Is(err, errors.ErrUnsupported) == true。
http包的ErrNotSupported采用的就是实现Is方法的方式支持errors.ErrNotSupported的:
注:errors.ErrUnsupported这种统一对unsupported类错误处理的设计方式直接借鉴。
  • flag:增加BoolFunc函数
多路径TCP协议可以让一个TCP连接在多个网络路径之间进行数据传输,从而提高传输速度和可靠性的技术。Go 1.21在linux平台上支持net包使用多路径TCP协议(如果linux kernel支持的话)。不过目前这不是默认开启的,可以通过Dialer的下面方法来显式设置:
在将来的版本中,该机制很大可能会变为默认开启的。
  • reflect:ValueOf允许在栈上分配Value的内容
在Go 1.21中,ValueOf不再强制Value内容在堆上分配,而是允许在栈上分配Value的内容。对Value的大多数操作也允许在栈中分配底层值。通过其代码实现可以更好地理解这点变化:
  • sync: 增加OnceFunc, OnceValue和OnceValues等语法糖函数
  • testing: 新增Testing函数
Go 1.21为testing包增加了func Testing() bool函数,该函数可以用来报告当前程序是否是go test创建的测试程序。使用Testing函数,我们可以确保一些无需在单测阶段执行的函数不被执行。比如下面这个例子:
  • crypto/tls:增加QUICConn以支持后续的QUIC实现
  • context包:新增WithoutCancel、WithDeadlineCause、WithTimeoutCause和AfterFunc
新增的WithoutCancel、WithDeadlineCause、WithTimeoutCause函数可以让你通过Cause函数获得导致cancel/timeout的真因:
AfterFunc函数是一个高级函数,与time.AfterFunc的机制和用法都类似,官方文档中有三个使用AfterFunc的例子,大家可以移步过去看看,这里就不赘述了。
  • runtime/trace:收集跟踪信息成本大幅降低(利好)
现在,trace在amd64和arm64上收集跟踪信息所需的CPU成本大幅降低:与上一版本相比,最多可提高10倍。
  • unicode: 升级到Unicode 15.0.0版本

语言

min、max和clear

min和max函数分别返回参数列表中的最小值和最大值,它们都是泛型函数
clear的操作对象是切片和map类型
  • 针对slice,clear保持slice的长度和容量,但将所有slice内已存在的元素(len个)都置为元素类型的零值;
  • 针对map,clear则是清空所有map的键值对,clear后,我们将得到一个empty map。

明确了包初始化顺序算法

在Go中,包既是功能单元,也是构建单元,Go代码通过导入其他包来复用导入包的导出功能(包括导出的变量、常量、函数、类型以及方法等)。Go程序启动时,程序会首先将依赖的包按一定顺序进行初始化,但长久以来,Go语言规范并没有明确依赖包初始化的顺序,这可能会导致一些对包初始化顺序有依赖的Go程序在不同Go版本下出现行为的差异。
为了消除这些可能存在的问题,Go核心团队在Go 1.21中明确了包初始化顺序的算法。
这个算法比较简单,其步骤如下:
  • 将所有依赖包按照导入路径排序,放入一个list;
  • 从list中按顺序找出第一个自身尚未初始化,但其依赖包已经全部初始化了的包,然后初始化该包,并将该包从list中删除;
  • 重新执行上面步骤,直到list为空。

type inference的增强

Go 1.21版本对泛型的类型推断能力做了增强
  1. 部分实例化的泛型函数:参数函数支持泛型
  1. 接口赋值推断:接口和方法支持泛型
  1. 对无类型常量的类型推断::如果多个不同类型的无类型常量参数(如一个无类型的 int 和一个无类型的浮点常量)被传递给具有相同类型参数类型的参数,现在类型推断将使用与具有无类型常量操作数的运算符相同的方法来确定类型

panic(nil)语义

Go编译器会将panic(nil)替换为panic(new(runtime.PanicNilError))

loop var per-loop -> loop var per-iteration

Go团队决定在Go 1.22版本移除这个“坑”,并在Go 1.21版本中以实验语义(GOEXPERIMENT=loopvar)提供了默认采用loop var per-iteration语义的for循环(包括for range)。新语义仅在GOEXPERIMENT=loopvar且在for语句(包括for range)的前置条件表达式中使用短变量声明循环变量时才生效。

编译器

PGO默认开启

Go 1.20版本引入了PGO(profile-guided optimization)优化技术预览版,Go 1.21版本中,PGO正式GA。如果main包目录下包含default.pgo文件,Go 1.21编译器在编译二进制文件时就会默认开启基于default.pgo中数据的pgo优化。优化带来的性能提升因程序而异,一般是2%~7%。
Go 1.21编译器自身就是基于PGO优化过的,编译速度提升约6%。

支持WASI(WebAssembly System Interface)

Go 1.21开始支持将Go编译为支持WASI规范的wasm程序

运行时

大幅降低GC尾部延迟

Go 1.21通过对运行时内部的GC的优化,应用程序的尾部延迟最多可减少40%,内存使用量也会略有减少。不过有些应用可能会观察到吞吐量的少量损失。内存使用量的减少与吞吐量的损失大约成正比。

Go工具链

Go 1.21在工具链方面最值得关注的就是Go团队对向后兼容(backwards compatibility)和向前兼容(forwards compatibility)的重新思考和新措施。
所谓向后兼容就是用新版Go编译器可以编译遗留的历史Go代码,并可以正常运行。比如用Go 1.21版本编译器编译基于Go 1.5版本编写的Go代码。Go在这方面做的一直很好,并提出了Go1兼容性承诺
而向前兼容指的是用旧版编译器编译新版本Go的代码,比如用Go 1.19版本编译器编译基于Go 1.21版本编写的Go代码。显而易见,如果Go代码中使用了Go 1.21引入的新语法特性,比如clear,那么Go 1.19编译Go代码时会失败。