Go 版本新特性
版本新特性
Go 1.16
embed
embed 是在 Go 1.16 中新加包。它通过 //go:embed
指令,可以在编译阶段将静态资源文件打包进编译好的程序中,并提供访问这些文件的能力。
为什么需要 embed 包
- 部署过程更简单。传统部署要么需要将静态资源与已编译程序打包在一起上传,或者使用 docker 和 dockerfile 自动化前者
- 确保程序的完整性。在运行过程中损坏或丢失静态资源通常会影响程序的正常运行。
- 您可以独立控制程序所需的静态资源。
embed 的基本语法
基本语法非常简单,首先导入 embed 包,然后使用指令 //go:embed
文件名 将对应的文件或目录结构导入到对应的变量上。 例如: 在当前目录下新建文件 version.txt,并输入内容 0.0.1
|
|
Go 1.18
any
any 作为一个新的关键字出现,any 有一个真身,本质就上是 interface{} 的别名:
|
|
使用例子:
|
|
泛型
详见 “Go 泛型”
扩容
Go1.18 之前切片的扩容是以容量 1024 为临界点:
- 当新 Slice 需要的容量大于原 Slice 容量的两倍,则直接按照新切片需要的容量扩容;
- 当原 slice 容量 < 1024 的时候,新 slice 容量变成原来的 2 倍;
- 当原 slice 容量 > 1024,进入一个循环,每次容量变成原来的 1.25 倍,直到大于期望容量。
然而这个扩容机制已经被 Go 1.18 弃用了,官方说新的扩容机制能更平滑地过渡。
Go1.18 不再以 1024 为临界点,而是设定了一个值为 256 的 threshold
,以 256 为临界点;超过 256,不再是每次扩容 1/4,而是每次增加(旧容量 + 3*256)/4;
- 当新 Slice 需要的容量大于原 Slice 容量的两倍,则直接按照新切片需要的容量扩容;
- 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
- 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。
1.18 的切片扩容优化策略,让底层数组大小的增长更加平滑: 通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从 2 到 1.25 的突变,该 commit 作者给出了几种原始容量下对应的“扩容系数”:
oldcap | 扩容系数 |
---|---|
256 | 2.0 |
512 | 1.63 |
1024 | 1.44 |
2048 | 1.35 |
4096 | 1.30 |
可以看到,Go1.18 的扩容策略中,随着容量的增大,其扩容系数是越来越小的,可以更好地节省内存。
我们可以试着求一个极限,当 oldcap 远大于 256 的时候,扩容系数将会变成 1.25。
Go 1.21
min、max 和 clear
builtin 包是一个特殊包,里面放置了 Go 语言预定义的标识符,用户层代码无需也不能导入 builtin 包。
builtin 增加了三个预定义函数:min、max 和 clear。
min、max
顾名思义,min 和 max 函数分别返回参数列表中的最小值和最大值,它们都是泛型函数,原型如下:
|
|
通过原型我们看到,使用这两个函数时,参数的类型要相同,且至少要传入一个参数:
|
|
clear
clear 函数的原型如下
|
|
从原型来看,clear 的操作对象是切片和 map 类型,不过其执行语义因依操作的对象类型而异。
- 针对 slice,clear 保持 slice 的长度和容量,但将所有 slice 内已存在的元素(len 个)都置为元素类型的零值;
- 针对 map,clear 则是清空所有 map 的键值对,clear 后,我们将得到一个 empty map。
|
|
这段代码的输出结果如下:
|
|
明确包初始化顺序算法
在 Go 中,包既是功能单元,也是构建单元,Go 代码通过导入其他包来复用导入包的导出功能(包括导出的变量、常量、函数、类型以及方法等)。G
o 程序启动时,程序会首先将依赖的包按一定顺序进行初始化,但长久以来,Go 语言规范并没有明确依赖包初始化的顺序,这可能会导致一些对包初始化顺序有依赖的 Go 程序在不同 Go 版本下出现行为的差异。
为了消除这些可能存在的问题,Go 核心团队在 Go 1.21 中明确了包初始化顺序的算法。
对包的初始化顺序有依赖,这本身就不是一种很好的设计,大家日常编码时应该注意避免。如果你的程序对包的初始化顺序存在依赖,那么升级到Go 1.21时你的程序行为可能会受到影响。
这个算法比较简单,其步骤如下:
- 将所有依赖包按照导入路径排序,放入一个 list;
- 从 list 中按顺序找出第一个自身尚未初始化,但其依赖包已经全部初始化了的包,然后初始化该包,并将该包从 list 中删除;
- 重新执行上面步骤,直到 list 为空。
上图的包导入顺序,即为 c, d, e, f, z, a, main
type inference 的增强
此次的类型推断增强主要包含以下三个方面:
- 部分实例化的泛型函数 (Partially instantiated generic functions)
- 接口赋值推断 (Interface assignment inference)
- 对无类型常量的类型推断 (Type inference for untyped constants)
TODO: 具体例子待补充
loop var per-loop -> loop var per-iteration
不过 Go 的 for 循环语句,尤其是 for range 语句有着很容易让程序出现错误的语义。
|
|
运行结果
|
|
我们看到:goroutine 中输出的 i、v 值都是 for range 循环结束后的 i、v 的最终值,而不是各个 goroutine 启动时的 i、v 值。这是因为
- goroutine 执行的闭包函数引用了它的外层包裹函数中的变量 i、v,这样变量 i、v 在主 goroutine 和新启动的 goroutine 之间实现了共享。
- 而 i, v 值在整个循环过程中是重用的,即仅有一份。在 for range 循环结束后,i = 4, v = 5,因此各个 goroutine 在等待 3 秒后进行输出的时候,输出的是 i, v 的最终值。
这里的 i 和 v 被称为 loop var per loop,即一个循环语句定义一次的变量
一种解决这个问题的典型方法是这样的:
|
|
我们在每个迭代中用短变量声明重新定义了在这次迭代中使用的 i 和 v,这里的 i 和 v 就是loop var per-iteration的了。不过这个方法也存在问题,比如不能解决所有场景下的 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)的前置条件表达式中使用短变量声明循环变量时才生效。
下面是 for range 的新语义的示例:
|
|
使用新语义运行该示例:
|
|
我们看到,新 loopvar 语义就相当于我们在每次迭代时手动重新定义 i := i 和 v := v。
对于经典的 3 段式 for 循环语句:
|
|
我们看到:新语义相当于 Go 编译器在每次 iteration 的前后各插入一行代码,在迭代(iteration)开始处插入 i’ := i,然后迭代过程中使用的是 i’,而在迭代的末尾则将 i’的最新值赋值给 i,后续 i 继续参与到 loop 是否继续的条件判定以及后置语句的操作中去。
新增的标准库
log/slog
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包。
有修改的标准库
context
新增 WithoutCancel、WithDeadlineCause、WithTimeoutCause 和 AfterFunc
新增的 WithoutCancel、WithDeadlineCause、WithTimeoutCause 函数可以让你通过 Cause 函数获得导致 cancel/timeout 的真因:
|
|
AfterFunc 函数是一个高级函数,与 time.AfterFunc 的机制和用法都类似,官方文档中有三个使用 AfterFunc 的例子
其他
sync:增加 OnceFunc, OnceValue 和 OnceValues 等语法糖函数
增加 errors.ErrUnsupported
testing: 新增 Testing 函数
runtime/trace:收集跟踪信息成本大幅降低
unicode: 升级到 Unicode 15.0.0 版本