Go1.22发布: for循环修改、标准库增强、性能优化

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新版本,感觉挺好的,看着自己使用的语言一直在保持更新。加入一些新的有趣的功能,提升一些性能,老被社区锐评缺失的功能也在逐渐完善。

对我个人而言,认真查看官方文档,看看其他大牛的讲解博客,无意间也会发现自己没接触到的知识,又水了一篇博客也是蛮好的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值