Go 1.19 相比 Go 1.18 有哪些值得注意的改动?

本系列旨在梳理 Go 的 release notes 与发展史,来更加深入地理解 Go 语言设计的思路。

go.dev/doc/go1.19

Go 1.19 值得关注的改动:

  1. 内存模型与原子操作 : Go 的 内存模型(memory model) 已更新,与 C、C++、Java 等语言的 模型 对齐,以确保持续一致性。同时,sync/atomic 包引入了新的 原子类型(atomic types),如 atomic.Int64atomic.Pointer[T],简化了原子操作并提高了类型安全性。
  2. go 命令 : 改进了构建信息的包含(-trimpath)、go generate/test 的环境一致性、go env 的输出处理,并增强了 go list -json 的灵活性和性能,同时缓存了部分模块加载信息以加速 go list
  3. vet 工具 : 新增检查,用于发现 errors.As 的第二个参数误用 *error 类型的常见错误。
  4. Runtime : 引入了 软内存限制(soft memory limit),可通过 GOMEMLIMIT 环境变量或 runtime/debug.SetMemoryLimit 函数进行设置,允许程序在接近内存上限时更有效地利用资源,并与 GOGC 协同工作。
  5. 编译器、汇编器与链接器 : 编译器通过 跳转表(jump table) 优化了大型 switch 语句(在 amd64arm64 架构下提速约 20%);编译器和汇编器现在强制要求 -p=importpath 标志来构建可链接的对象文件;链接器在 ELF 平台使用标准的压缩 DWARF 格式。

下面是一些值得展开的讨论:

Go 1.19 修订内存模型并引入新的原子类型

Go 1.19 对其内存模型进行了修订,主要目标是与 C, C++, Java, JavaScript, Rust, 和 Swift 等主流语言使用的内存模型保持一致。需要注意的是,Go 仅提供 顺序一致(sequentially consistent) 的原子操作,而不支持其他语言中可能存在的更宽松的内存排序形式。

伴随着内存模型的更新,sync/atomic 包引入了一系列新的原子类型,包括 atomic.Bool, atomic.Int32, atomic.Int64, atomic.Uint32, atomic.Uint64, atomic.Uintptr, 和 atomic.Pointer[T]

这些新类型的主要优势在于:

  1. 类型安全 :它们封装了底层的值,强制所有访问都必须通过原子 API 进行,避免了意外的非原子读写操作。
  2. 简化指针操作atomic.Pointer[T] 泛型类型避免了在调用点将指针转换为 unsafe.Pointer 的需要,使得代码更清晰、更安全。
  3. 自动对齐atomic.Int64atomic.Uint64 类型在结构体中或分配内存时,即使在 32 位系统上也会自动保证 64 位对齐。这对于保证原子操作的正确性至关重要。

旧方式(Go 1.18 及之前)

通常需要直接使用 sync/atomic 包提供的函数,并对基本类型进行操作。例如,对一个共享的 int64 变量进行原子更新:

go 复制代码
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int64
    var wg sync.WaitGroup

    // 启动多个 goroutine 并发增加 counter
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // 使用 atomic 函数进行原子增操作
            atomic.AddInt64(&counter, 1)
        }()
    }

    wg.Wait()

    // 使用 atomic 函数进行原子读操作
    finalValue := atomic.LoadInt64(&counter)
    fmt.Println("Final counter:", finalValue) // 输出: Final counter: 100
}

对于指针类型,之前可能需要 atomic.Value 或者结合 unsafe.Pointer 使用 atomic.LoadPointer/StorePointer

新方式(Go 1.19 及之后)

使用新的原子类型,代码更简洁,类型约束更强。

go 复制代码
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    // 使用新的 atomic.Int64 类型
    var counter atomic.Int64
    var wg sync.WaitGroup

    // 启动多个 goroutine 并发增加 counter
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // 直接调用类型的方法进行原子增操作
            counter.Add(1)
        }()
    }

    wg.Wait()

    // 直接调用类型的方法进行原子读操作
    finalValue := counter.Load()
    fmt.Println("Final counter:", finalValue) // 输出: Final counter: 100
}

对于指针,atomic.Pointer[T] 提供了类型安全的原子读写能力:

go 复制代码
package main

import (
    "fmt"
    "sync/atomic"
)

type Config struct {
    Version string
    Data    map[string]string
}

func main() {
    var currentConfig atomic.Pointer[Config]

    // 初始化配置
    initialConfig := &Config{Version: "v1", Data: map[string]string{"key": "value1"}}
    currentConfig.Store(initialConfig)

    // 读取配置
    cfg1 := currentConfig.Load()
    fmt.Printf("Config v%s: %v\n", cfg1.Version, cfg1.Data)

    // 原子地更新配置
    newConfig := &Config{Version: "v2", Data: map[string]string{"key": "value2", "new": "added"}}
    currentConfig.Store(newConfig)

    cfg2 := currentConfig.Load()
    fmt.Printf("Config v%s: %v\n", cfg2.Version, cfg2.Data)
}
bash 复制代码
Config vv1: map[key:value1]
Config vv2: map[key:value2 new:added]

这种方式相比旧的 atomic.Valueunsafe.Pointer 操作,类型更安全,意图更明确。

go 命令的改进

Go 1.19 对 go 命令进行了多项增强和调整,主要涉及构建过程、环境设置、信息查询等方面。

  • -trimpath 标志信息嵌入 :如果在 go build 时设置了 -trimpath 标志(用于从编译出的二进制文件中移除本地构建路径信息),这个设置现在会被记录在二进制文件的构建信息中。可以通过 go version -m <binary>debug.ReadBuildInfo 来查看。
txt 复制代码
# 编译时加入 -trimpath
go build -trimpath -o myapp main.go

# 查看二进制文件的构建信息
go version -m ./myapp
# 输出会包含类似 build -trimpath=true 的信息
  • go generate 明确设置 GOROOT :现在 go generate 会在其执行子进程的环境变量中明确设置 GOROOT。这确保了即使代码生成器本身是使用 -trimpath 构建的(可能导致其无法自动找到 GOROOT),它也能定位到正确的 Go 安装根目录。

  • go testgo generatePATH 调整 :这两个命令现在会将 $GOROOT/bin 放在子进程 PATH 环境变量的开头。这样做可以保证当测试代码或代码生成器需要执行 go 命令时,它们会调用到与父进程相同版本的 go 工具链,避免潜在的版本冲突。

  • go env 输出的空格处理 :对于包含空格的环境变量值(如 CGO_CFLAGS, CGO_CPPFLAGS, CGO_CXXFLAGS, CGO_FFLAGS, CGO_LDFLAGS, GOGCCFLAGS),go env 现在会在输出时给它们加上引号,使得输出结果更易于被脚本等其他工具解析。

  • go list -json 支持字段选择go list -json 命令现在可以接受一个逗号分隔的字段列表,用于指定需要输出的 JSON 字段。如果指定了列表,go list 只会计算和输出这些字段,这在某些情况下可以显著提高性能,因为它避免了计算不需要的字段。这也可能抑制某些只在计算未请求字段时才会出现的错误。

txt 复制代码
# 仅获取当前模块的导入路径和直接依赖
go list -json=ImportPath,Deps .
  • go list 模块加载缓存go 命令现在会缓存加载某些模块所需的信息,这有望加速部分 go list 命令的调用。

vet 工具新增 errors.As 使用检查

vet 工具的 errorsas 检查器现在增加了一项功能:检测 errors.As 函数的第二个参数是否被错误地传递了 *error 类型。这是一个常见的错误。

errors.As 函数用于检查错误链中是否存在特定类型的错误,并将其赋值给一个变量。其签名如下:

go 复制代码
func As(err error, target interface{}) bool

正确的用法是,target 参数必须是一个指向 具体错误类型 的指针,或者是一个指向实现了 error 接口的任意类型的指针。errors.As 会遍历 err 的错误链,如果找到一个错误可以赋值给 target 指向的变量,就进行赋值并返回 true

常见的错误用法

开发者有时可能会错误地传递一个 *error (指向 error 接口的指针)给 target

go 复制代码
package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    // 示例错误,包装了 *os.PathError
    err := fmt.Errorf("wrapping a file error: %w", &os.PathError{Op: "open", Path: "/no/such/file", Err: errors.New("file not found")})

    var genericErr error // 声明一个 error 接口类型的变量
    // 错误用法:将 *error 类型的指针传递给 errors.As
    // 这几乎永远不是你想要的,因为它只会检查错误链中是否有某个值可以赋给一个 error 接口
    // 这通常没有意义,因为链中的任何错误都可以赋值给 error 接口
    if errors.As(err, &genericErr) {
        // 这段代码几乎总会执行 (只要 err != nil),但 genericErr 会被赋值为链中的第一个错误
        // 这并不是 errors.As 的设计意图
        fmt.Printf("Found an error (incorrectly): %v\n", genericErr)
    } else {
        fmt.Println("No error found (incorrectly)")
    }
}
bash 复制代码
piperliu@go-x86:~/code/playground$ go run main.go 
Found an error (incorrectly): wrapping a file error: open /no/such/file: file not found
piperliu@go-x86:~/code/playground$ go vet main.go 
# command-line-arguments
./main.go:17:8: second argument to errors.As should not be *error

Go 1.19 的 vet 会对上述错误用法发出警告。

正确的用法

应该传递一个指向 具体错误类型 变量的指针。

go 复制代码
package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    // 示例错误,包装了 *os.PathError
    err := fmt.Errorf("wrapping a file error: %w", &os.PathError{Op: "open", Path: "/no/such/file", Err: errors.New("file not found")})

    var pathErr *os.PathError // 声明一个具体错误类型的指针变量
    // 正确用法:传递 *os.PathError 类型的指针
    if errors.As(err, &pathErr) {
        // 如果错误链中存在 *os.PathError 类型的错误,pathErr 会被赋值
        fmt.Printf("Successfully found PathError: Op=%s, Path=%s\n", pathErr.Op, pathErr.Path)
    } else {
        fmt.Println("PathError not found in chain")
    }
}
bash 复制代码
go run main.go 
Successfully found PathError: Op=open, Path=/no/such/file
piperliu@go-x86:~/code/playground$ go vet main.go 

这个新的 vet 检查有助于开发者及早发现并修正这种对 errors.As 的误用。

Runtime 的软内存限制及其他改进

Go 1.19 在 Runtime 层面引入了重要的 软内存限制(soft memory limit) 功能,并包含其他多项优化。

软内存限制

  • 目的 :提供一种机制来限制 Go 程序使用的总内存量,以提高在容器化环境等资源受限场景下的资源利用率。
  • 范围 :这个限制覆盖 Go 堆(heap)以及所有由 Runtime 管理的内存(例如 goroutine 栈、GC 元数据等)。它 不包括 程序二进制文件本身的内存映射、其他语言(如 C)管理的内存,以及操作系统为 Go 程序保留的内存(如某些内核缓冲区)。
  • 配置 :可以通过设置 GOMEMLIMIT 环境变量(例如 GOMEMLIMIT=1024MiB)或在代码中调用 runtime/debug.SetMemoryLimit(limit int64) 来管理。
  • GOGC 的关系 :软内存限制与 GOGC(或 runtime/debug.SetGCPercent)协同工作。即使设置 GOGC=off,只要设置了 GOMEMLIMIT,Runtime 仍然会尝试遵守这个内存限制,通过更频繁地触发 GC 来控制内存增长。这使得程序能够始终最大限度地利用其被分配的内存限额。
  • GC CPU 限制器 :当程序的活动堆大小接近软内存限制时,为了防止 GC 过于频繁(称为 GC 抖动/thrashing)而严重影响程序性能,Runtime 会尝试将 GC 的 CPU 利用率限制在 50% 以内(不包括空闲时间)。这意味着 Runtime 宁愿稍微超出内存限制,也不愿完全阻止应用程序的进展。可以通过新的 运行指标(runtime metric) /gc/limiter/last-enabled:gc-cycle 查看该限制器最后一次生效的 GC 周期。
  • 稳定性与限制 :对于较大的内存限制(数百 MB 或更多),该功能是稳定且生产就绪的。但对于非常小的限制(几十 MB 或更少),由于外部延迟因素(如操作系统调度)的影响,限制可能不那么精确(详见 issue 52433)。

其他 Runtime 改进

  • 空闲状态下的 GC 工作线程 :当应用程序足够空闲以至于触发周期性 GC 时,Runtime 现在会调度更少的 GC 工作 goroutine 在空闲的操作系统线程上运行,以减少不必要的资源消耗。
  • 初始 Goroutine 栈大小 :Runtime 现在会根据 goroutine 历史平均栈使用量来分配初始栈大小。这旨在减少平均情况下早期栈增长和复制的开销,代价是对于栈使用量远低于平均值的 goroutine 可能会浪费最多 2 倍的空间。
  • Unix 文件描述符限制(RLIMIT_NOFILE) :在 Unix 操作系统上,导入了 os 包的 Go 程序现在会自动将进程的打开文件描述符软限制(soft limit)提高到硬限制(hard limit)允许的最大值。这是为了解决某些系统上为了兼容旧的 C 程序(使用 select 系统调用)而设置的过低的人为限制。Go 程序(尤其是并发处理大量文件时,如 gofmt)经常因此耗尽文件描述符。此更改的一个潜在影响是,如果 Go 程序再启动旧的 C 程序作为子进程,这些子进程可能会以过高的文件描述符限制运行。这可以通过在启动 Go 程序之前设置较低的硬限制来解决。
  • 简化不可恢复错误的回溯信息 :对于不可恢复的致命错误(如并发 map 写入、解锁未锁定的互斥锁),现在默认打印更简洁的回溯信息,不包含 Runtime 的元数据(类似于 panic 的致命错误)。除非设置了 GOTRACEBACK=systemcrash,才会打印包含完整元数据的详细回溯信息。Runtime 内部的致命错误总是包含完整元数据。
  • 调试器注入函数调用(ARM64) :在 ARM64 架构上增加了对调试器注入函数调用的支持。这使得开发者在使用支持此功能的更新版调试器时,可以在交互式调试会话中调用程序中的函数。
  • 地址消毒器(Address Sanitizer)改进 :Go 1.18 中引入的 地址消毒器(address sanitizer) 支持现在能更精确地处理函数参数和全局变量。

编译器、汇编器与链接器的更新

Go 1.19 在构建工具链的底层组件方面也有一些重要的变化。

编译器 (Compiler)

  • 大型 switch 语句优化 :编译器现在使用 跳转表(jump table) 来实现包含大量 case 的整数和字符串 switch 语句。这可以带来显著的性能提升,根据具体情况,速度可能提高约 20%。此优化目前仅适用于 GOARCH=amd64GOARCH=arm64 架构。

例如,一个有许多字符串 case 的 switch

go 复制代码
func handleCommand(cmd string) {
    switch cmd {
    case "START":
        // ...
    case "STOP":
        // ...
    case "RESTART":
        // ...
    // ... 很多其他 case ...
    case "STATUS":
        // ...
    default:
        // ...
    }
}

在 Go 1.19 中,如果这个 switch 足够大,编译器(在支持的架构上)会生成更高效的跳转表代码,而不是一系列的比较和跳转。

  • 强制要求 -p=importpath 标志 :Go 编译器现在要求必须提供 -p=importpath 标志才能构建一个可链接的对象文件 (.o 文件)。go build 命令和 Bazel 构建系统已经会自动提供这个标志。如果你有自定义的构建系统直接调用 Go 编译器(compile),你需要确保传递了这个标志。importpath 通常是包的导入路径。

  • 移除 -importmap 标志 :Go 编译器不再接受 -importmap 标志。直接调用编译器的构建系统必须改为使用 -importcfg 标志来提供导入路径到实际文件路径的映射。go build 会自动处理这个。

汇编器 (Assembler)

  • 强制要求 -p=importpath 标志 :与编译器类似,Go 汇编器(asm)现在也要求必须提供 -p=importpath 标志才能构建可链接的对象文件。同样,go build 会处理好,但直接调用汇编器的系统需要自行添加。

链接器 (Linker)

  • ELF 平台使用标准压缩 DWARF 格式 :在 ELF 格式的目标平台(如 Linux)上,链接器现在默认使用标准的 gABI 格式(SHF_COMPRESSED)来压缩 DWARF 调试信息段,取代了之前使用的非标准的 .zdebug 格式。这有助于提高与其他工具链(如 GDB、objdump 等)的兼容性。

这些改变主要影响性能(switch 优化)、构建系统的维护者(需要调整对编译器/汇编器的直接调用)以及调试信息格式的标准化。对于大多数使用 go build 的开发者来说,后两项更改是透明的。

相关推荐
raoxiaoya1 小时前
同时安装多个版本的golang
开发语言·后端·golang
考虑考虑3 小时前
go使用gorilla/websocket实现websocket
后端·程序员·go
李少兄3 小时前
解决Spring Boot多模块自动配置失效问题
java·spring boot·后端
码农BookSea4 小时前
不用Mockito写单元测试?你可能在浪费一半时间
后端·单元测试
codingandsleeping5 小时前
Express入门
javascript·后端·node.js
ss2735 小时前
基于Springboot + vue + 爬虫实现的高考志愿智能推荐系统
spring boot·后端·高考
专注API从业者6 小时前
《Go 语言高并发爬虫开发:淘宝商品 API 实时采集与 ETL 数据处理管道》
开发语言·后端·爬虫·golang
Asthenia04126 小时前
Netty writeAndFlush与Pipeline深入分析
后端
欧先生^_^7 小时前
Scala语法基础
开发语言·后端·scala