Go 1.22 Release Notes 官方更新文档
虽然定位自己是个Go开发,但好像Go语言的文章也没写几篇。今天在公众号上看到了Go更新1.22版本的文章,看完决定自己也来体验一下。写博客有个好处就是,想要写出像样的文章,还真得对它有足够的了解,当然抄袭和AI生成除外。
更新版本
我个人/工作都是使用的windows平台开发,安装go是通过下载官网.msi安装包直接安装。更新也很简单,去下载最新的安装包,可以安装到新的目录,也可以覆盖安装。覆盖安装程序会提示将会删除原有版本,选择之前的安装目录即可。
切换版本
如果安装在新的目录,可以手动切换版本。比如上一个版本是go1.21.7,新安装了版本go1.22。
setx GOROOT "C:\Go\go1.22"
setx GOROOT "C:\Go\go1.21.7"
然后使用go version查看当前版本,或者go env查看go全部环境变量。
go env
go version
注意在VSCode中切换版本需要重启才能生效,同时注意gomod版本是否一致,接下来我们需要切换版本来测试输出。
loopvar 循环问题
每次大版本更新谷歌官方都会发布博客,Go 1.22 is released! 首先是解决了for循环陷阱,以下代码会按照某种顺序输出"a b c"
官方demo
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
期望输出
这段代码如果是在以前版本会全部输出最后一个值,这是我在公众号看到的,没想到一会儿就打脸了。
c
c
c
如果是以1.22版本会输出,顺序随机,但是每个值都会输出。
c
a
b
验证一下
我是看到别人的博客说这个是loopvar,我个人确实也遇到过这个问题,如果在for循环中开启携程,并且没有以参数传递的话,每次都会用循环的最后一个值。按我的理解,这个在别的语言里应该叫闭包,但显然现在go官方修复了他,我们来验证一下。
# 在go 1.21 刚才的代码会直接提示循环问题
loop variable i captured by func literal
前几次测试确实会全部输出c,但当我多测试了几次,我发现第一次输出可能不是c,后来发现第二位、第三位都可能不是c,几乎是没有规律的,只是c输出的概率比较大。原来这并不是闭包问题,那是什么问题呢?
// go1.21的输出
b
c
c
首先,我们写一个一定会全部输出a b c的方法:
// go1.21
func rightLoop() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func(str string) {
fmt.Println(str)
done <- true
}(v)
}
// wait for all goroutines to complete before exiting
for range values {
<-done
}
}
输出结果
a
b
c
两种写法有什么区别呢?首先第二种写法之所以不会输出意外的结果,是因为我们传递了值类型的参数,在goroutine内部的变量不再是一个来自外部的变量,所以一定会输出全部的a b c。
共享变量
比较确定的是在go1.22版本之前,这样的循环闭包变量应该是共享的一个变量,我们来测试一下,输出官方demo的地址
// go1.21
0xc00008a260
0xc00008a260
0xc00008a260
// go1.22
0xc00008a260
0xc00008a270
0xc00008a280
丝毫不意外,在go1.22版本不会再共享一个变量了,我们测试一下正常循环的变量地址
func normalLoop() {
values := []string{"a", "b", "c"}
for _, v := range values {
fmt.Println(v)
fmt.Println(&v)
}
}
测试输出
// go1.21
a
0xc0000302a0
b
0xc0000302a0
c
0xc0000302a0
// go1.22
a
0xc00008a260
b
0xc00008a280
c
0xc00008a2a0
总结
虽然还是没搞懂具体的实现上的区别,但我们需要知道的是
- go1.22版本之后解决了for循环变量共享的问题,注意必须go版本和gomod版本都>=1.22才会使用新的loopvar
- 虽然之前版本的变量共享,但在协程里可能会输出不同的值。我也不该想当然以为值永远是最后一个
for循环支持整形
新版本可以直接对一个int进行for循环输出了,我们来测试一下
// go1.22
func rangeInt() {
for v := range 5 {
fmt.Println(v)
}
}
// 输出
0
1
2
3
4
注意for range输出时,只能有一个迭代遍历,不能for k,v := range 5 这样的形式,会报错
range over 5 (untyped int constant) permits only one iteration variable
性能提升
Go 运行时中的内存优化可将 CPU 性能提高 1-3%,同时 还将大多数 Go 程序的内存开销降低约 1%。
继续优化PGO,添加了改进的去虚拟化,允许静态调度更多接口方法调用。约有2-14%的性能提升。
这部分可以看官方关于 PGO说明 这里就不讲了,因为咱也没搞明白
看这一部分的性能提升就像看影魔的版本改动,过几个版本加一点护甲,虽然看上去提升不大,加着加着说不定就质变了。(影魔:那为啥我现在还是挺地板的)
标准库添加
math/rand/v2 随机算法标准库
标准库第一次增加v2版本,math/rand/v2 提供更干净、一致的伪随机生成算法API,我们来体验一下:
Package rand 实现了适用于模拟等任务的伪随机数生成器,但不应用于安全敏感型工作。
随机数由 Source 生成,通常包装在 Rand 中。这两种类型都应该由一个 goroutine 同时使用:在多个 goroutine 之间共享需要某种同步。
顶级函数(如 Float64 和 Int)可以安全地供多个 goroutine 并发使用。
无论此包的种子如何,都可以轻松预测其输出。有关适用于安全敏感工作的随机数,请参阅 crypto/rand 包。
▾ 示例
// go1.22
// Create and seed the generator. 创建生成器并设定种子
// Typically a non-fixed seed should be used, such as Uint64(), Uint64(). 通常应使用非固定种子,例如 Uint64()、Uint64()。
// Using a fixed seed will produce the same output on every run. 使用固定种子将在每次运行时产生相同的输出。
r := rand.New(rand.NewPCG(1, 2))
// The tabwriter here helps us generate aligned output.
w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
defer w.Flush()
show := func(name string, v1, v2, v3 any) {
fmt.Fprintf(w, "%s\t%v\t%v\t%v\n", name, v1, v2, v3)
}
// Float32 and Float64 values are in [0, 1).
show("Float32", r.Float32(), r.Float32(), r.Float32())
show("Float64", r.Float64(), r.Float64(), r.Float64())
// ExpFloat64 values have an average of 1 but decay exponentially.
show("ExpFloat64", r.ExpFloat64(), r.ExpFloat64(), r.ExpFloat64())
// NormFloat64 values have an average of 0 and a standard deviation of 1.
show("NormFloat64", r.NormFloat64(), r.NormFloat64(), r.NormFloat64())
// Int32, Int64, and Uint32 generate values of the given width.
// The Int method (not shown) is like either Int32 or Int64
// depending on the size of 'int'.
show("Int32", r.Int32(), r.Int32(), r.Int32())
show("Int64", r.Int64(), r.Int64(), r.Int64())
show("Uint32", r.Uint32(), r.Uint32(), r.Uint32())
// IntN, Int32N, and Int64N limit their output to be < n.
// They do so more carefully than using r.Int()%n.
show("IntN(10)", r.IntN(10), r.IntN(10), r.IntN(10))
show("Int32N(10)", r.Int32N(10), r.Int32N(10), r.Int32N(10))
show("Int64N(10)", r.Int64N(10), r.Int64N(10), r.Int64N(10))
// Perm generates a random permutation of the numbers [0, n).
show("Perm", r.Perm(5), r.Perm(5), r.Perm(5))
// 输出结果
Float32 0.95955694 0.8076733 0.8135684
Float64 0.4297927436037299 0.797802349388613 0.3883664855410056
ExpFloat64 0.43463410545541104 0.5513632046504593 0.7426404617374481
NormFloat64 -0.9303318111676635 -0.04750789419852852 0.22248301107582735
Int32 2020777787 260808523 851126509
Int64 5231057920893523323 4257872588489500903 158397175702351138
Uint32 314478343 1418758728 208955345
IntN(10) 6 2 0
Int32N(10) 3 7 7
Int64N(10) 8 9 4
Perm [0 3 1 4 2] [4 1 2 0 3] [4 3 2 0 1]
说实话感觉好像变化不大,得出了几个结论:
- 随机数种子一般用uint64,固定种子生成固定的随机数,需要安全敏感型工作使用crypto/rand包
- v2版本的函数名有一些不一样,比如v1生成随机整数的是Intn,v2是IntN
- 跟v1版本一样可以不指定随机数种子,不指定时使用runtime随机种子
- v2版本使用Source生成,并非协程安全,顶级函数goroutine安全
// A Source is not safe for concurrent use by multiple goroutines.
type Source interface {
Uint64() uint64
}
// A Rand is a source of random numbers.
type Rand struct {
src Source
}
net/http SeverMux增强
Routing Enhancements 我觉得这个是本次更新中比较重要的,官方的net/http包增加了两个功能:方法匹配和通配符。我们来体验一下
// 过去最简单的http sever服务
package main
import "net/http"
type pastHandler struct{}
func (pastHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}
func pastServerMux() {
mux := http.NewServeMux()
mux.Handle("/", pastHandler{})
mux.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foo"))
})
http.ListenAndServe(":8080", mux)
}
以前的serverMux不支持通配符,甚至不支持get post等方法,但是现在支持了。
// 以前是以前
func newServerMux() {
mux := http.NewServeMux()
// 只匹配GET请求
mux.HandleFunc("GET /posts/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
w.Write([]byte("posts:" + id))
})
// 只会匹配主机为localhost的请求
mux.HandleFunc("POST localhost/index.html", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("localhost html"))
})
// 只会匹配主机为127.0.0.1/的请求,后续还有参数的话无法匹配
mux.HandleFunc("GET 127.0.0.1/{$}", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("127.0.0.1 html"))
})
http.ListenAndServe(":8080", mux)
}
输出结果
get http://localhost:8080/posts/123
posts:123
post http://localhost:8080/posts/456
Method Not Allowed
get http://localhost:8080/index.html
Method Not Allowed
post http://localhost:8080/index.html
localhost html
get http://127.0.0.1:8080/
127.0.0.1 html
get http://127.0.0.1:8080/index.html
404 page not found
这部分新增的内容还挺多的,更具体的通配符使用可以看具体的文档。我个人的感觉虽然提升挺多的,相比gin框架还是比较简单,当然也提供了使用官方http服务做业务的可能性。
增加连接多切片函数
func concatSlice() {
sliceA := []string{"a", "b", "c"}
sliceB := []string{"d", "e", "f"}
newValues := slices.Concat(sliceA, sliceB)
fmt.Println(newValues)
}
源码实现也比较简单,用到的是泛型,泛型可以看我之前的博客 gogeneric
// Concat returns a new slice concatenating the passed in slices.
func Concat[S ~[]E, E any](slices ...S) S {
size := 0
for _, s := range slices {
size += len(s)
if size < 0 {
panic("len out of range")
}
}
newslice := Grow[S](nil, size)
for _, s := range slices {
newslice = append(newslice, s...)
}
return newslice
}
总结时间
这也是我第一次比较完成的体验Go新版本,感觉挺好的,看着自己使用的语言一直在保持更新。加入一些新的有趣的功能,提升一些性能,老被社区锐评缺失的功能也在逐渐完善。
对我个人而言,认真查看官方文档,看看其他大牛的讲解博客,无意间也会发现自己没接触到的知识,又水了一篇博客也是蛮好的。