找回密码
 立即注册
首页 业界区 安全 Go 1.17 相比 Go 1.16 有哪些值得注意的改动? ...

Go 1.17 相比 Go 1.16 有哪些值得注意的改动?

蔬陶 6 天前
本系列旨在梳理 Go 的 release notes 与发展史,来更加深入地理解 Go 语言设计的思路。
https://go.dev/doc/go1.17
Go 1.17 值得关注的改动:

  • 语言增强: 引入了从 切片(slice) 到数组指针的转换,并添加了 unsafe.Add 和 unsafe.Slice 以简化 unsafe.Pointer 的使用。
  • 模块图修剪: 对于指定 go 1.17 或更高版本的模块,go.mod 文件现在包含更全面的传递性依赖信息,从而启用模块图修剪和依赖懒加载机制。
  • go run 增强: go run 命令现在支持版本后缀(如 cmd@v1.0.0),允许在模块感知模式下运行指定版本的包,忽略当前模块的依赖。
  • Vet 工具更新: 新增了三项检查,分别针对 //go:build 与 // +build 的一致性、对无缓冲 channel 使用 signal.Notify 的潜在风险,以及 error 类型上 As/Is/Unwrap 方法的签名规范。
  • 编译器优化: 在 64 位 x86 架构上实现了新的基于寄存器的函数调用约定,取代了旧的基于栈的约定,带来了约 5% 的性能提升和约 2% 的二进制体积缩减。
下面是一些值得展开的讨论:
Go 1.17 语言层面引入了切片到数组指针的转换以及 unsafe 包的增强

Go 1.17 在语言层面带来了三处增强:

  • 切片到数组指针的转换
现在可以将一个 切片(slice) s(类型为 []T)转换为一个数组指针 a(类型为 *[N]T)。

这种转换的语法是 (*[N]T)(s)。转换后的数组指针 a 和原始切片 s 在有效索引范围内(0 = 数组大小    arrPtr1 := (*[3]int)(s)    fmt.Printf("arrPtr1: %p, %v\n", arrPtr1, *arrPtr1) // 输出指针地址和 {1 2 3}    fmt.Printf("&arrPtr1[0]: %p, &s[0]: %p\n", &arrPtr1[0], &s[0]) // 输出相同的地址    arrPtr2 := (*[5]int)(s)    fmt.Printf("arrPtr2: %p, %v\n", arrPtr2, *arrPtr2) // 输出指针地址和 {1 2 3 4 5}    // 修改通过指针访问的元素,会影响原切片    arrPtr1[0] = 100    fmt.Printf("s after modification: %v\n", s) // 输出 [100 2 3 4 5]    // 失败转换:切片长度 < 数组大小    defer func() {        if r := recover(); r != nil {            fmt.Println("Recovered from panic:", r) // 输出 Recovered from panic: runtime error: cannot convert slice with length 5 to pointer to array with length 6        }    }()    arrPtr3 := (*[6]int)(s) // 这行会引发 panic    fmt.Println("This line will not be printed", arrPtr3)}[/code]
  1. package main
  2. import "fmt"
  3. func main() {
  4.     s := []int{1, 2, 3, 4, 5}
  5.     // 成功转换:切片长度 >= 数组大小
  6.     arrPtr1 := (*[3]int)(s)
  7.     fmt.Printf("arrPtr1: %p, %v\n", arrPtr1, *arrPtr1) // 输出指针地址和 {1 2 3}
  8.     fmt.Printf("&arrPtr1[0]: %p, &s[0]: %p\n", &arrPtr1[0], &s[0]) // 输出相同的地址
  9.     arrPtr2 := (*[5]int)(s)
  10.     fmt.Printf("arrPtr2: %p, %v\n", arrPtr2, *arrPtr2) // 输出指针地址和 {1 2 3 4 5}
  11.     // 修改通过指针访问的元素,会影响原切片
  12.     arrPtr1[0] = 100
  13.     fmt.Printf("s after modification: %v\n", s) // 输出 [100 2 3 4 5]
  14.     // 失败转换:切片长度 < 数组大小
  15.     defer func() {
  16.         if r := recover(); r != nil {
  17.             fmt.Println("Recovered from panic:", r) // 输出 Recovered from panic: runtime error: cannot convert slice with length 5 to pointer to array with length 6
  18.         }
  19.     }()
  20.     arrPtr3 := (*[6]int)(s) // 这行会引发 panic
  21.     fmt.Println("This line will not be printed", arrPtr3)
  22. }
复制代码

  • unsafe.Add 函数
unsafe 包新增了 Add 函数:unsafe.Add(ptr unsafe.Pointer, len IntegerType) unsafe.Pointer。
它的作用是将一个非负的整数 len(必须是整数类型,如 int, uintptr 等)加到 ptr 指针上,并返回更新后的指针。其效果等价于 unsafe.Pointer(uintptr(ptr) + uintptr(len)),但意图更清晰,且有助于静态分析工具理解指针运算。
这个函数的目的是为了简化遵循 unsafe.Pointer 安全规则的代码编写,但它 并没有改变 这些规则。使用 unsafe.Add 仍然需要确保结果指针指向的是合法的内存分配。
例如,在没有 unsafe.Add 之前,如果要访问结构体中某个字段的地址,可能需要这样做:
  1. arrPtr1: 0xc0000b2000, [1 2 3]
  2. &arrPtr1[0]: 0xc0000b2000, &s[0]: 0xc0000b2000
  3. arrPtr2: 0xc0000b2000, [1 2 3 4 5]
  4. s after modification: [100 2 3 4 5]
  5. Recovered from panic: runtime error: cannot convert slice with length 5 to pointer to array with length 6
复制代码
虽然效果相同,但 unsafe.Add 更明确地表达了“指针加偏移量”的意图。

  • unsafe.Slice 函数
unsafe 包新增了 Slice 函数:unsafe.Slice(ptr *T, len IntegerType) []T。
对于一个类型为 *T 的指针 ptr 和一个非负整数 len,unsafe.Slice(ptr, len) 会返回一个类型为 []T 的切片。这个切片的底层数组从 ptr 指向的地址开始,其长度(length)和容量(capacity)都等于 len。
同样,这个函数的目的是简化遵循 unsafe.Pointer 安全规则的代码,尤其是从一个指针和长度创建切片时,避免了之前需要构造 reflect.SliceHeader 或 reflect.StringHeader 的复杂步骤,但规则本身不变。使用者必须保证 ptr 指向的内存区域至少包含 len * unsafe.Sizeof(T) 个字节,并且这块内存在切片的生命周期内是有效的。
例如,从一个 C 函数返回的指针和长度创建 Go 切片:
  1. package main
  2. import (
  3.     "fmt"
  4.     "unsafe"
  5. )
  6. type MyStruct struct {
  7.     A int32
  8.     B float64 // B 相对于结构体起始地址的偏移量是 8 (在 64 位系统上,int32 占 4 字节,需要 4 字节对齐填充)
  9. }
  10. func main() {
  11.     data := MyStruct{A: 1, B: 3.14}
  12.     ptr := unsafe.Pointer(&data)
  13.     // 旧方法:使用 uintptr 进行计算
  14.     offsetB_old := unsafe.Offsetof(data.B) // 获取字段 B 的偏移量,类型为 uintptr
  15.     ptrB_old := unsafe.Pointer(uintptr(ptr) + offsetB_old)
  16.     *(*float64)(ptrB_old) = 6.28 // 修改 B 的值
  17.     fmt.Println("Old method result:", data)
  18.     // 新方法:使用 unsafe.Add
  19.     data = MyStruct{A: 1, B: 3.14} // 重置数据
  20.     ptr = unsafe.Pointer(&data)
  21.     offsetB_new := unsafe.Offsetof(data.B)
  22.     ptrB_new := unsafe.Add(ptr, offsetB_new) // 使用 unsafe.Add 进行指针偏移
  23.     *(*float64)(ptrB_new) = 9.42             // 修改 B 的值
  24.     fmt.Println("New method result:", data)
  25. }
复制代码
  1. package main
  2. /*
  3. #include <stdlib.h>
  4. int create_int_array(int size, int** out_ptr) {
  5.     int* arr = (int*)malloc(size * sizeof(int));
  6.     if (arr == NULL) {
  7.         *out_ptr = NULL;
  8.         return 0;
  9.     }
  10.     for (int i = 0; i < size; i++) {
  11.         arr[i] = i * 10;
  12.     }
  13.     *out_ptr = arr;
  14.     return size;
  15. }
  16. */
  17. import "C"
  18. import (
  19.     "fmt"
  20.     "unsafe"
  21. )
  22. func main() {
  23.     var cPtr *C.int
  24.     cSize := C.create_int_array(5, &cPtr)
  25.     defer C.free(unsafe.Pointer(cPtr)) // 必须记得释放 C 分配的内存
  26.     if cPtr == nil {
  27.         fmt.Println("Failed to allocate C memory")
  28.         return
  29.     }
  30.     // 使用 unsafe.Slice 创建 Go 切片
  31.     // 注意:这里的 cSize 类型是 C.int,需要转换为 Go 的整数类型 int32
  32.     goSlice := unsafe.Slice((*int32)(unsafe.Pointer(cPtr)), int(cSize))
  33.     fmt.Printf("Go slice: %v, len=%d, cap=%d\n", goSlice, len(goSlice), cap(goSlice))
  34.     // 输出: Go slice: [0 10 20 30 40], len=5, cap=5
  35.     // 可以像普通 Go 切片一样使用
  36.     goSlice[0] = 100
  37.     fmt.Printf("Modified C data via Go slice: %d\n", *cPtr) // 输出: Modified C data via Go slice: 100
  38. }
复制代码
使用 unsafe.Slice 比手动设置 SliceHeader 更简洁且不易出错。
总的来说,unsafe 包的这两个新函数是为了让开发者在需要进行底层操作时,能够更容易地编写出符合 unsafe.Pointer 安全约定的代码,而不是放宽这些约定。
Go 1.17 模块管理与 go 命令的诸多改进

Go 1.17 对 Go 命令及其模块管理机制进行了多项重要改进,核心目标是提升构建性能、依赖管理的准确性和用户体验。

  • 模块图修剪 (Module Graph Pruning) 与 依赖懒加载 (Lazy Loading)


  • 之前行为 :当构建一个模块时,Go 命令需要加载该模块所有直接和间接依赖的 go.mod 文件,构建一个完整的 模块依赖图(module dependency graph)。即使某些间接依赖对于当前构建并非必需,它们的 go.mod 文件也可能被下载和解析。
  • Go 1.17 行为 (go 1.17 或更高)

    • go.mod 文件内容变化 :如果一个模块在其 go.mod 文件中声明 go 1.17 或更高版本,运行 go mod tidy 时,go.mod 文件会包含更详细的传递性依赖信息。具体来说,它会为 每一个 提供了被主模块(main module)传递性导入(transitively-imported)的包的模块添加显式的 require 指令。这些新增的间接依赖通常会放在一个单独的 require 块中,以区别于直接依赖。
    • 模块图修剪 :有了更完整的依赖信息后,当 Go 命令处理一个 go 1.17 模块时,其构建的模块图可以被“修剪”。对于其他同样声明了 go 1.17 或更高版本的依赖模块,Go 命令只需要考虑它们的 直接 依赖,而不需要递归地探索它们的完整传递性依赖。
    • 懒加载 :由于 go.mod 文件包含了构建所需的所有依赖信息,Go 命令现在可以实行 懒加载 。它不再需要读取(甚至下载)那些对于完成当前命令并非必需的依赖项的 go.mod 文件。

  • 示例理解 :假设你的项目 A 依赖 B (go 1.17),B 依赖 C (go 1.17),A 直接导入了 B 中的包,间接导入了 C 中的包。

    • 在 Go 1.16 中,A 的 go.mod 可能只写 require B version。Go 命令会加载 A, B, C 的 go.mod。
    • 在 Go 1.17 中,运行 go mod tidy 后,A 的 go.mod 会包含 require B version 和 require C version(在间接依赖块)。当处理 A 时,Go 命令看到 B 和 C 都是 go 1.17 模块,并且 A 的 go.mod 已包含所需信息,可能就不再需要去下载和解析 B 或 C 的 go.mod 文件了。

  • 设计理念 :提高构建性能(减少文件下载和解析),提高依赖解析的准确性和稳定性。
  • 实践

    • 升级现有模块:go mod tidy -go=1.17
    • 保持与旧版本兼容:默认 go mod tidy 会保留 Go 1.16 需要的 go.sum 条目。
    • 仅为 Go 1.17 整理:go mod tidy -compat=1.17 (旧版 Go 可能无法使用此模块)。
    • 查看特定版本的图:go mod graph -go=1.16。


  • 模块弃用注释 (Module Deprecation Comments)


  • 之前行为 :没有标准的机制来标记一个模块版本已被弃用。
  • Go 1.17 行为 :模块作者可以在 go.mod 文件顶部添加 // Deprecated: 弃用信息 格式的注释,然后发布一个包含此注释的新版本。
  • 效果

    • go get :如果需要构建的包依赖了被弃用的模块,会打印警告。
    • go list -m -u :会显示所有依赖的弃用信息(使用 -f 或 -json 查看完整消息)。

  • 示例
  1. piperliu@go-x86:~/code/playground$ go env | grep CGO
  2. GCCGO="gccgo"
  3. CGO_ENABLED="1"
  4. CGO_CFLAGS="-g -O2"
  5. CGO_CPPFLAGS=""
  6. CGO_CXXFLAGS="-g -O2"
  7. CGO_FFLAGS="-g -O2"
  8. CGO_LDFLAGS="-g -O2"
  9. piperliu@go-x86:~/code/playground$ go run main.go
  10. Go slice: [0 10 20 30 40], len=5, cap=5
  11. Modified C data via Go slice: 100
复制代码

  • 设计理念 :为模块维护者提供一个标准化的方式,向用户传达模块状态和迁移建议(例如,迁移到新的主版本 V2)。

  • go get 行为调整


  • -insecure 标志移除 :该标志已被废弃和移除。应使用环境变量 GOINSECURE 来允许不安全的协议,使用 GOPRIVATE 或 GONOSUMDB 来跳过校验和验证。
  • 安装命令推荐 go install :使用 go get 安装命令(即不带 -d 标志)现在会产生弃用警告。推荐使用 go install cmd@version(如 go install example.com/cmd@latest 或 go install example.com/cmd@v1.2.3)来安装可执行文件。在 Go 1.18 中,go get 将只用于管理 go.mod 中的依赖。
  • 示例 :安装最新的 stringer 工具
  1. // Deprecated: use example.com/mymodule/v2 instead. See migration guide at ...
  2. module example.com/mymodule
  3. go 1.17
  4. require (...)
复制代码

  • 设计理念 :明确区分 go get(管理依赖)和 go install(安装命令/二进制文件)的职责。提高安全性配置的清晰度。

  • 处理缺少 go 指令的 go.mod 文件


  • 主模块 go.mod :如果主模块的 go.mod 没有 go 指令且 Go 命令无法更新它,现在假定为 go 1.11(之前是当前 Go 版本)。
  • 依赖模块 :如果依赖模块没有 go.mod 文件(GOPATH 模式开发)或其 go.mod 文件没有 go 指令,现在假定为 go 1.16(之前是当前 Go 版本)。
  • 设计理念 :为缺失版本信息的旧代码提供更稳定和可预测的行为。

  • vendor 目录内容调整 (go 1.17 或更高)


  • vendor/modules.txt :go mod vendor 现在会在 vendor/modules.txt 中记录每个 vendored 模块在其自身 go.mod 中指定的 go 版本。这个版本信息会在从 vendor 构建时使用。
  • 移除 go.mod/go.sum :go mod vendor 现在会省略 vendored 依赖目录下的 go.mod 和 go.sum 文件,因为它们可能干扰 Go 命令在 vendor 树内部正确识别模块根。
  • 设计理念 :确保使用 vendor 构建时能应用正确的语言版本特性,并避免路径解析问题。

  • 密码提示抑制


  • 使用 SSH 拉取 Git 仓库时,Go 命令现在默认禁止弹出 SSH 密码输入提示和 Git Credential Manager 提示(之前已对其他 Git 密码提示这样做)。建议使用 ssh-agent 进行密码保护的 SSH 密钥认证。
  • 设计理念:提高在自动化环境(如 CI/CD)中使用 Go 命令的便利性和安全性。

  • go mod download (无参数)


  • 不带参数调用 go mod download 时,不再将下载内容的校验和保存到 go.sum(恢复到 Go 1.15 的行为)。要保存所有模块的校验和,请使用 go mod download all。
  • 设计理念:减少无参数 go mod download 对 go.sum 的意外修改。

  • //go:build 构建约束 (Build Constraints)


  • 新语法引入 :Go 命令现在理解新的 //go:build 构建约束行,并 优先于 旧的 // +build 行。新语法使用类似 Go 的布尔表达式(如 //go:build linux && amd64 或 //go:build !windows),更易读写,不易出错。
  • 过渡与同步 :目前两个语法都支持。gofmt 工具现在会自动同步同一文件中的 //go:build 和 // +build 行,确保它们的逻辑一致。建议所有 Go 文件都更新为同时包含两种形式,并保持同步。
  • 示例
  1. go install golang.org/x/tools/cmd/stringer@latest
复制代码
  1. // 旧语法
  2. // +build linux darwin
  3. // 新语法 (由 gofmt 自动添加或同步)
  4. //go:build linux || darwin
  5. package mypkg
复制代码

  • 设计理念 :引入一种更现代、更清晰、更不易出错的构建约束语法,并提供平滑的迁移路径。
总结与最佳实践
Go 1.17 在模块管理方面带来了显著的性能和健壮性改进。最佳实践包括:

  • 使用 go mod tidy -go=1.17 将项目升级到新的模块管理机制。
  • 使用 go install cmd@version 来安装和运行特定版本的 Go 程序。
  • 开始采用 //go:build 语法,并利用 gofmt 来保持与旧语法的同步。
  • 弃用模块时,使用 // Deprecated: 注释。
  • 使用环境变量(GOINSECURE, GOPRIVATE, GONOSUMDB)替代 -insecure 标志。
  • 理解 go.mod 中新的间接依赖 require 块的含义。
这些改动共同体现了 Go 团队持续优化开发者体验、构建性能和依赖管理可靠性的设计理念。
go run 在 Go 1.17 中获得了在模块感知模式下运行指定版本包的能力

在 Go 1.17 之前,go run 命令主要用于快速编译和运行当前目录或指定 Go 源文件。如果在一个模块目录下运行,它会使用当前模块的依赖;如果在模块之外,它可能工作在 GOPATH 模式下。要想运行一个特定版本的、非当前模块依赖的 Go 程序,通常需要先用 go get(可能会修改当前 go.mod 或安装到 GOPATH)或者 go install 来获取对应版本的源码或编译好的二进制文件。
Go 1.17 对 go run 进行了增强,允许直接运行指定版本的包,即使这个包不在当前模块的依赖中,也不会修改当前模块的 go.mod 文件。
新特性
go run 命令现在接受带有版本后缀的包路径参数,例如 example.com/cmd@v1.0.0 或 example.com/cmd@latest。
行为
当使用这种带版本后缀的语法时,go run 会:

  • 在模块感知模式下运行 :它会像处理模块依赖一样去查找和下载指定版本的包及其依赖。
  • 忽略当前目录的 go.mod :它不会使用当前项目(如果在项目目录下运行)的 go.mod 文件来解析依赖,而是为这个临时的运行任务构建一个独立的依赖集。
  • 不安装 :它只编译并运行程序,不会将编译结果安装到 GOPATH/bin 或 GOBIN。
  • 不修改当前 go.mod :当前项目的 go.mod 和 go.sum 文件不会被这次 go run 操作修改。
这个特性非常适合以下情况:

  • 临时运行特定版本的工具 :比如,你想用最新版本的 stringer 工具生成代码,但你的项目依赖的是旧版本。
  • 在 CI/CD 或脚本中运行工具 :无需先 go install,可以直接 go run 指定版本的构建工具或代码生成器。
  • 测试不同版本的命令 :快速尝试一个库提供的命令的不同版本,而无需切换项目依赖。
示例
假设你想运行 golang.org/x/tools/cmd/stringer 的最新版本来为当前目录下的 mytype.go 文件中的 MyType 生成代码,但你的项目 go.mod 可能没有依赖它,或者依赖了旧版。
  1. // 旧语法
  2. // +build !windows,!plan9
  3. // 新语法
  4. //go:build !windows && !plan9
  5. package mypkg
复制代码
这避免了先 go get golang.org/x/tools/cmd/stringer(可能污染 go.mod 或全局 GOPATH)或者 // Deprecated: use example.com/mymodule/v2 instead. See migration guide at ...
module example.com/mymodule

go 1.17

require (...)(需要写入 GOBIN)的步骤。
设计理念 :提升 go run 的灵活性和便利性,使其成为一个更强大的临时执行 Go 程序的工具,特别是在需要版本控制和隔离依赖的场景下。
Go 1.17 的 vet 工具增加了对构建标签、信号处理和错误接口方法签名的静态检查

Go 1.17 版本中的 go vet 工具(一个用于发现 Go 代码中潜在错误的静态分析工具)新增了三项有用的检查,旨在帮助开发者避免一些常见的陷阱和错误。

  • 检查不匹配的 //go:build 和 // +build 行


  • 背景 :Go 1.17 正式引入了新的 //go:build 构建约束语法,并推荐使用它替代旧的 // +build 语法。在过渡期间,推荐两者并存且保持逻辑一致。
  • 问题 :如果开发者手动修改了其中一个,或者放置的位置不正确(比如 //go:build 必须在文件顶部,仅前面可以有空行或注释),可能会导致两个约束的实际效果不一致,根据使用的 Go 版本不同,编译结果可能出乎意料。
  • Vet 检查 :vet 现在会验证同一个文件中的 //go:build 和 // +build 行是否位于正确的位置,并且它们的逻辑含义是否同步。
  • 修复 :如果检查出不一致,可以使用 gofmt 工具自动修复,它会根据 //go:build 的逻辑(如果存在)来同步 // +build,或者反之。
  • 示例
[code]// BAD: Logic mismatch//go:build linux && amd64// +build linux,arm64
您需要登录后才可以回帖 登录 | 立即注册