Go语言1.22新特性速览

Go 1.22在2024年2月正式发布,几个改动虽然不算大但都挺实用,尤其是range over integers和路由增强,日常开发直接受益。

Range over integers

终于不用for i := 0; i < n; i++了。Go 1.22支持直接对整数range:

// 之前
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// Go 1.22
for i := range 10 {
    fmt.Println(i) // 0, 1, 2, ..., 9
}

range N产生从0到N-1的序列。看起来是小改动,但写起来确实舒服不少,尤其是嵌套循环或者简单的重复执行场景:

// 重试3次
for attempt := range 3 {
    err := doSomething()
    if err == nil {
        break
    }
    log.Printf("attempt %d failed: %v", attempt+1, err)
    time.Sleep(time.Second * time.Duration(attempt+1))
}

增强的HTTP路由

net/httpServeMux终于支持方法匹配和路径参数了,不用为了这点功能引入第三方路由库:

mux := http.NewServeMux()

// 方法匹配:只响应GET请求
mux.HandleFunc("GET /api/users", listUsers)

// 路径参数:{id}会被捕获
mux.HandleFunc("GET /api/users/{id}", getUser)

// POST方法
mux.HandleFunc("POST /api/users", createUser)

// 通配符:匹配剩余路径
mux.HandleFunc("GET /files/{path...}", serveFile)

获取路径参数用r.PathValue()

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
}

func serveFile(w http.ResponseWriter, r *http.Request) {
    path := r.PathValue("path") // 匹配剩余的完整路径
    // ...
}

路由优先级规则:更具体的模式优先。GET /api/users/{id}GET /api/users/{name...}更具体。如果两个模式有冲突且无法确定优先级,注册时会panic。

对于中小项目来说,标准库的路由能力终于够用了。不过如果需要中间件链、路由分组这些功能,chi或gin还是更方便。

for循环变量语义修复

这是Go历史上最臭名昭著的坑之一。Go 1.22之前,for循环变量在每次迭代中共享同一个地址:

// Go 1.21及之前的坑
values := []int{1, 2, 3}
var funcs []func()
for _, v := range values {
    funcs = append(funcs, func() {
        fmt.Println(v) // 全部输出3!
    })
}
for _, f := range funcs {
    f()
}

这是因为闭包捕获的是变量v的地址,而v在循环结束后的值是3。以前的workaround是在循环里加一行v := v

Go 1.22修复了这个问题,每次迭代的循环变量是独立的:

// Go 1.22
values := []int{1, 2, 3}
var funcs []func()
for _, v := range values {
    funcs = append(funcs, func() {
        fmt.Println(v) // 正确输出1, 2, 3
    })
}

同样适用于goroutine场景:

for _, url := range urls {
    go func() {
        resp, err := http.Get(url) // Go 1.22: 每个goroutine拿到独立的url
        // ...
    }()
}

这个修改通过go.mod中的Go版本控制。只有go.mod声明go 1.22或更高版本时才启用新语义,老项目不受影响。

slices包增强

slices包在1.21引入,1.22新增了几个实用函数:

import "slices"

// Concat:拼接多个切片
s1 := []int{1, 2}
s2 := []int{3, 4}
s3 := []int{5}
result := slices.Concat(s1, s2, s3)
// [1, 2, 3, 4, 5]

// Delete/Replace的边界处理改进
s := []int{0, 1, 2, 3, 4}
s = slices.Delete(s, 1, 3) // 删除索引1到2的元素
// [0, 3, 4]

math/nand/v2

新版随机数包,API更简洁:

import "math/nand/v2"

// 直接用顶层函数,不需要seed
n := rand.IntN(100)        // [0, 100)
f := rand.Float64()        // [0.0, 1.0)
n32 := rand.N[int32](100)  // 泛型版本

v2默认使用ChaCha8算法代替之前的Source,安全性更好。顶层函数自动使用随机seed,不需要手动rand.Seed(time.Now().UnixNano())了。

其他变化

  • go vet增强:能检测出更多defer中的常见错误
  • go test -count:默认值从0改为1,避免测试缓存问题
  • 编译器优化:PGO(Profile-Guided Optimization)改进,性能提升2-14%
  • trace工具重写go tool trace完全重写,处理大trace文件不再OOM

总结

Go 1.22没有什么革命性的变化,但几个改进都很实在:range over integers减少样板代码,路由增强减少依赖,循环变量修复消除了一个历史坑。升级成本低,值得尽早切换。