标签 Package 下的文章

Go 1.22引入的包级变量初始化次序问题

本文永久链接 – https://tonybai.com/2024/03/29/the-issue-in-pkg-level-var-init-order-in-go-1-22

细心的朋友可能已经注意到,从春节后,我的博客就“停更”了!实际上,这一情况部分是因为工作上的事务繁忙,另一部分则是因为我将工作之外的闲暇时间更多地投入到一本即将于今年中下旬出版的书的撰写了:在之前的积累基础上,我花了两个多月的时间完成了初稿。

当然,我也深切地怀念博客写作所带来的乐趣和与读者的互动。正巧,今天一位学员在《Go语言第一课》专栏留言给了我一个恢复下笔的机会。借此,我也准备恢复一下博客写作的节奏。

另外预告一下:我和我的技术团队合作翻译的一本Go语言入门书最早也将于2024年4月份上市,敬请期待


《Go语言第一课》专栏第8讲中,我曾系统讲解了Go包的初始化次序,以及Go包内包级变量、常量、init函数等的初始化次序。讲这些的初衷就是希望Go初学者能先了解一下Go程序的执行次序,这样在后续阅读和理解Go代码的时候,就好比拥有了“通往宝藏的地图”,可以直接沿着Go代码执行次序这张“地图”去阅读和理解Go代码,而不会在庞大的代码库中迷失了。

相对于早期的Go版本,Go包的初始化次序在Go 1.21版本开始会有所变化,这个可以看我的《Go 1.21中值得关注的几个变化》一文了解详情。

不过除了Go包的初始化次序得以明确之外,Go在1.22版本中的包级变量初始化次序也发生了一些“变化”,但Go 1.22的Release Notes压根没提到Go包内的变量初始化次序会有变化。究竟这些变化是有意为之,还是由于代码变更而引入的新问题呢?我们还得从近期《Go语言第一课》专栏的一位读者提出的问题讲起!

1. Go 1.22的输出结果与专栏文章中不同!

原专栏中的代码较多,为方便起见我又写了一段简化版的代码,可以等价地反映问题。下面的代码用于演示包级变量、常量和init函数的初始化次序:

// initorder.go
package main

import (
    "fmt"
)

var (
    v0 = constInitCheck()
    v1 = variableInit("v1")
    v2 = variableInit("v2")
)

const (
    c1 = "c1"
    c2 = "c2"
)

func constInitCheck() string {
    if c1 != "" {
        fmt.Println("main: const c1 has been initialized")
    }
    if c1 != "" {
        fmt.Println("main: const c2 has been initialized")
    }
    return ""
}

func variableInit(name string) string {
    fmt.Printf("main: var %s has been initialized\n", name)
    return name
}

func init() {
    fmt.Println("main: first init func invoked")
}

func init() {
    fmt.Println("main: second init func invoked")
}

func main() {
    // do nothing
}

使用Go 1.22版本之前的版本,比如Go 1.21版本,运行该程序的输出结果如下:

$go run initorder.go
main: const c1 has been initialized
main: const c2 has been initialized
main: var v1 has been initialized
main: var v2 has been initialized
main: first init func invoked
main: second init func invoked

这个输出结果也是专栏文章中的输出结果,即包级元素的初始化顺序是:常量 -> 变量 -> init函数。三个变量的初始化次序是v0 -> v1 -> v2。

但专栏的一位读者在使用最新Go 1.22版本运行上述程序后,却提出了如下问题:

总结一下这个问题的两个关键点如下:

  • Go 1.22版本运行上述程序的输出结果与文章中的结果不一致
  • 将const声明block搬移到var声明block的前面后,使用Go 1.22版本的输出结果与文章中的一致

我们先来复现一下问题。我使用Go 1.22.0运行上面的initorder.go,得到下面结果:

$go run main.go
main: var v1 has been initialized
main: var v2 has been initialized
main: const c1 has been initialized
main: const c2 has been initialized
main: first init func invoked
main: second init func invoked

该输出结果确如读者所说,与文中的输出顺序不一致了,变量的初始化次序变为了v1 -> v2 -> v0。这会让很多读者误以为包内元素的初始化次序变成了“变量 -> 常量 -> init函数”。是否真的如此了呢?我们下面来初步分析一下。

2. 原因初步分析

Go语言规范中对包内变量初始化次序的说明是这样的(截至2024.03):

Within a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables. More precisely, a package-level variable is considered ready for initialization if it is not yet initialized and either has no initialization expression or its initialization expression has no dependencies on uninitialized variables. Initialization proceeds by repeatedly initializing the next package-level variable that is earliest in declaration order and ready for initialization, until there are no variables ready for initialization. Multiple variables on the left-hand side of a variable declaration initialized by single (multi-valued) expression on the right-hand side are initialized together: If any of the variables on the left-hand side is initialized, all those variables are initialized in the same step. For the purpose of package initialization, blank variables are treated like any other variables in declarations.

粗略翻译后大致意思如下:

在包内,包级变量初始化逐步进行,每一步都会选择声明顺序中最早的且不依赖于未初始化变量的那个变量。更准确地说,如果包级变量尚未初始化并且没有初始化表达式或其初始化表达式不依赖于未初始化的变量,则认为该变量具备初始化条件。通过重复初始化声明顺序中最早且具备初始化条件的下一个包级变量来进行初始化,直到没有具备初始化条件的变量为止。由右侧单个(多值)表达式初始化的变量声明左侧的多个变量会一起初始化:如果左侧的任何变量被初始化,则所有这些变量都会被初始化在同一步骤中。出于包初始化的目的,空变量也被视为与声明中的任何其他变量一样。

按照Go语言规范的描述,我们来理论推导一下v0、v1和v2的初始化次序:

var (
    v0 = constInitCheck()
    v1 = variableInit("v1")
    v2 = variableInit("v2")
)
  • 第一轮:待初始化的包级变量集合{v0, v1, v2}。在这一轮,我们按声明顺序逐一看一下这三个变量。

v0未初始化,其声明语句的右侧有初始化表达式(initialization expression),且这个初始化表达式式(constInitCheck)不依赖未初始化的变量(仅仅依赖两个常量c1和c2),因此按照Spec描述,v0具备初始化条件,会先进行初始化,于是constInitCheck会被调用。

  • 第二轮:待初始化的包级变量集合{v1, v2}。

按声明顺序,先看v1。和v0一样,其声明语句的右侧有初始化表达式,且这个初始化表达式式(variableInit)不依赖未初始化的变量,因此按照Spec描述,v1具备初始化条件,会进行初始化,于是variableInit会被调用。

  • 第三轮:待初始化的包级变量集合{v2}。

这个没啥可推导的了,初始化v2就是了!

这样,包级变量的声明次序就应该是v0 -> v1 -> v2。这个理论推导结果显然与Go 1.22版之前的输出结果是一致的。但与Go 1.22版本的输出结果有悖。

那么Go 1.22版本为什么没有将v0作为第一个具备初始化条件的变量对其进行初始化呢?v0有初始化表达式constInitCheck,该函数没有依赖任何未初始化的包级变量,但该函数内部依赖了两个常量c1和c2:

func constInitCheck() string {
    if c1 != "" {
        fmt.Println("main: const c1 has been initialized")
    }
    if c1 != "" {
        fmt.Println("main: const c2 has been initialized")
    }
    return ""
}

我们大胆地猜测一下:Go 1.22版本将c1和c2当成了“未初始化的变量”了!还记得读者问题的第二个关键点吗:“将const声明block搬移到var声明block的前面后,使用Go 1.22版本的输出结果便与文章中的一致”。按照Go 1.22的逻辑,将常量声明放到前面后,按顺序常量先被初始化了。这样到v0时,v0具备初始化的条件就成立了,于是v0就可以先被初始化了。

3. “一波三折”的issue

为了证实上述推测,我在github.com/golang/go提了issue 66575,并对上述问题做了阐述,不过该issue被Go团队的年轻成员Sean Liao“闪电”关闭了。

好在几个小时后,Go大神Keith Randall看到了这个issue,并支持了我的猜测!他还闪电般地找出了导致Go 1.22版本出现此问题的commit,并给出了fix方案:cmd/compile: put constants before variables in initialization order。fix方案的思路就是将所有常量的初始化放到变量之前。

fix merge到主干后,Gobot自动关闭了该issue。

但严谨的Keith Randall随后reopen了该issue,并圈了Go语言之父的Robert Griesemer,希望后者确定一下是否需要更新一下Go spec。

目前该issue已经被加入Go 1.23 milestone,并会在Go 1.23 fix。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go 1.22新特性前瞻

本文永久链接 – https://tonybai.com/2023/12/25/go-1-22-foresight

美国时间2023年12月20日,Go官方宣布Go 1.22rc1发布,开启了为期2个多月的、常规的公测之旅,Go 1.22预计将于2024.2月份正式发布!

除了在官网下载Go 1.22rc1版本进行新特性体验之外,我们还可以通过在线的Go Playground选择“Go dev branch”来体验(相比下载安装,在线版本体验会有一些局限):

注:关于Go的多种安装方法,《Go语言第一课》专栏有系统全面的讲解,欢迎订阅阅读。

本文将和大家一起看看Go 1.22都会带来哪些新特性。不过由于目前为时尚早,下面列出的有些变化最终不一定能进入到Go 1.22的最终版本中,所以切记一切变更点要以最终Go 1.22版本发布时为准。

1. 语言变化

Go 1.22的语言特性变化主要是围绕for loop的

1.1 loopvar试验特性转正

Go 1.21版本中,作为试验特性loopvar在Go 1.22中正式转正。如果你还不知道这个特性是啥,我们来看一下下面这个最能说明问题的示例:

// go1.22-foresight/lang/for-range/for_range.go

package main

import (
    "fmt"
    "sync"
)

func main() {
    sl := []int{11, 12, 13, 14, 15}
    var wg sync.WaitGroup
    for i, v := range sl {
        wg.Add(1)
        go func() {
            fmt.Printf("%d : %d\n", i, v)
            wg.Done()
        }()
    }
    wg.Wait()
}

我们分别用Go 1.22rc1和Go 1.21.0来运行上面这段代码:

// 使用go 1.22rc1的运行结果:

$go run for_range.go
4 : 15
1 : 12
0 : 11
3 : 14
2 : 13

// 使用go 1.21.0的运行结果:
$go run for_range.go
4 : 15
4 : 15
4 : 15
4 : 15
4 : 15

之所以存在差异,是因为Go 1.22版本开始,for range语句中声明的循环变量(比如这里的i和v)不再是整个loop一份(loop var per loop),而是每次iteration都会有自己的变量(loop var per-iteration),这样在Go 1.22中,for range中的goroutine启动的闭包函数中捕获的变量是loop var per-iteration,这样才会输出5个不同的索引值和对应的切片值。

注:关于Go 1.22版本之前的for range的坑,《Go语言第一课》专栏有图文并茂的原理讲解,欢迎订阅阅读。

那传统的3-clause的for loop呢?其中的循环变量的语义是否也发生变化了呢?我们看下面示例:

// go1.22-foresight/lang/for-range/classic_for_loop.go

package main

import (
    "fmt"
    "sync"
)

func main() {
    sl := []int{11, 12, 13, 14, 15}
    var wg sync.WaitGroup
    for i := 0; i < len(sl); i++ {
        wg.Add(1)
        go func() {
            v := sl[i]
            fmt.Printf("%d : %d\n", i, v)
            wg.Done()
        }()
    }
    wg.Wait()
}

我们依然分别用Go 1.22rc1和Go 1.21.0版本运行这段代码,得到的结果如下:

// 使用go 1.22rc1的运行结果:

$go run classic_for_loop.go
0 : 11
4 : 15
2 : 13
3 : 14
1 : 12

// 使用go 1.21.0的运行结果:

$go run classic_for_loop.go
panic: runtime error: index out of range [5] with length 5

goroutine 20 [running]:
main.main.func1()
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9
created by main.main in goroutine 1
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f
panic: runtime error: index out of range [5] with length 5

goroutine 19 [running]:
main.main.func1()
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9
created by main.main in goroutine 1
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f
exit status 2

从输出结果来看,3-clause的for语句中声明的循环变量也变成了loop var per-iteration了。

在Go 1.22之前,go vet工具在遇到像上面代码那样在闭包中引用循环变量的情况时会给出警告,但由于Go 1.22的这个语义修正,go vet对于Go 1.22及以后版本(根据go.mod中的指示)的类似Go代码将不再报错。

不过就像Russ Cox在spec: less error-prone loop variable scoping这一issue中提及那样,该特性落地可能会带来不兼容问题,即对存量代码行为的破坏性改变。为此Go团队提供了一个名为bisect的工具,该工具可以检测出存量代码在for loop语义发生变更后是否会导致问题。不过该工具似乎只能与go test一起使用,也就是说你只能对那些被Go测试覆盖到的for loop进行检测。

目前spec: less error-prone loop variable scoping这一issue还处于open状态,也没有放入Go 1.22 milestone中,不知道后续是否还会存在变数!

1.2 range支持整型表达式

在Go 1.22版本中,for range后面的range表达式除了支持传统的像数组、切片、map、channel等表达式外,还支持放置整型表达式,比如下面这个例子:

// lang/range-expr-support-integer/main.go

func main() {
    n := 5
    for i := range n {
        fmt.Println(i)
    }
}

我们知道:for range会在执行伊始对range表达式做一次求值,这里对n求值结果为5。按照新增的for range后接整型表达式的语义,对于整数值n,for range每次迭代值会从0到n-1按递增顺序进行。上面代码中的for range会从0迭代到4(5-1),我们执行一下上述代码就可以印证这一点:

$go run main.go
0
1
2
3
4

如果n <= 0,则循环不运行任何迭代。

这个新语法特性,可以理解为是一种“语法糖”,是下面等价代码的“语法糖”:

for i := 0; i < 5; i++ {
    fmt.Println(i)
}

不过,迭代总是从0开始,似乎限制了该语法糖的使用范围。

1.3 试验特性:range-over-function iterators

在for range支持整型表达式的时候,Go团队也考虑了增加函数迭代器(iterator),不过前者语义清晰,实现简单。后者展现形式、语义和实现都非常复杂,于是在Go 1.22中,函数迭代器以试验特性提供,通过GOEXPERIMENT=rangefunc可以体验该功能特性。

在没有函数迭代器之前,我们实现一个通用的反向迭代切片的函数可能是像这样:

// lang/range-over-function-iterator/backward_iterate_slice_old.go

func Backward(s []E) func(func(int, E) bool) {
    return func(yield func(int, E) bool) {
        for i := len(s)-1; i >= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
        return
    }
}

下面是在Go 1.21.0版本中使用上面Backward函数的方式:

// lang/range-over-function-iterator/backward_iterate_slice_old.go

func main() {
    sl := []string{"hello", "world", "golang"}
    Backward(sl)(func(i int, s string) bool {
        fmt.Printf("%d : %s\n", i, s)
        return true
    })
}

我们用Go 1.21.0运行一下上述示例:

$go run backward_iterate_slice_old.go
2 : golang
1 : world
0 : hello

在以前版本中,这种对切片、数组或map中进行元素迭代的情况在实际开发中非常常见,也比较模式化,但基于目前语法,使用起来非常不便。于是Go团队提出将它们与for range结合在一起的提案。有了range-over-function iterator机制后,我们就可以像下面这样使用Backward泛型函数了:

// lang/range-over-function-iterator/backward_iterate_slice_new.go

func main() {
    sl := []string{"hello", "world", "golang"}
    for i, s := range Backward(sl) {
        fmt.Printf("%d : %s\n", i, s)
    }
}

相比于上面的老版本代码,这也的代码更简洁清晰了,使用Go 1.22rc1的运行结果也与老版本别无二致:

$GOEXPERIMENT=rangefunc  go run backward_iterate_slice_new.go
2 : golang
1 : world
0 : hello

但代价就是要理解什么样原型的函数才能与for range一起使用实现函数迭代,这的确有些复杂,本文就不展开说了,有兴趣的童鞋可以先看看有关range-over-function iterator的wiki先行了解一下。

2. 编译器、运行时与工具链

2.1 继续增强PGO优化

Go 1.20版本引入PGO(profile-guided optimization)后,PGO这种优化技术带来的优化效果就得到了持续的提升:Go 1.20实测性能提升仅为1.05%;Go 1.21版本发布时,官方的数据是2%~7%,而Go 1.21编译器自身在PGO优化过后编译速度提升约6%。

在Go 1.22中,官方给出的数字则是2%~14%,这14%的提升想必是来自Google内部的某个实际案例。

2.2 inline和devirtualize

在Go 1.22中,Go编译器可以更灵活的运用devirtualize和inline对代码进行优化了。

在面向对象的编程中,虚拟函数是一种在运行时动态确定调用的函数。当调用虚拟函数时,编译器通常会为其生成一段额外的代码,用于在运行时确定要调用的具体函数。这种动态调度的机制使得程序可以根据实际对象类型来执行相应的函数,但也带来了一定的性能开销。通过devirtualize优化技术,编译器会尝试在编译时确定调用的具体函数,而不是在运行时进行动态调度。这样可以避免运行时的开销,并允许编译器进行更多的优化

对应到Go来说,就是在编译阶段将使用接口进行的方法调用转换为通过接口的实际类型的实例直接调用该方法

注:我的《Go语言精进之路》一书中有对通过接口调用方法的原理的详尽说明,欢迎阅读。

关于内联优化,今年Austin Clements发起了inline大修项目,对Go编译器中的内联优化过程进行全面调整,目标是在Go 1.22中拥有更有效的、具有启发能力的内联,为后续内联的进一步增强奠定基础。该大修的成果目前以GOEXPERIMENT=newinliner试验特性的形式在Go 1.22中提供。

2.3 运行时

运行时的变化主要还是来自GC

Go 1.22中,运行时会将基于类型的垃圾回收的元数据放在每个堆对象附近,从而可以将Go程序的CPU性能提高1-3%。同时,通过减少重复的元数据的优化,内存开销也将降低约1%。不确定减少重复元数据(metadata)这一优化是否来自对unique包的讨论

2.4 工具链

在Go工具链改善方面,首当其冲的要数go module相关工具了。

在Go 1.22中,go work增加了一个与go mod一致的特性:支持vendor。通过go work vendor,可以将workspace中的依赖放到vendor目录下,同时在构建时,如果module root下有vendor目录,那么默认的构建是go build -mod=vendor,即基于vendor的构建。

go mod init在Go 1.22中将不再考虑GOPATH时代的包依赖工具的配置文件了,比如Gopkg.lock。在Go 1.22版本之前,如果go module之前使用的是类似dep这样的工具来管理包依赖,go mod init会尝试读取dep配置文件来生成go.mod。

go vet工具取消了对loop变量引用的警告,增加了对空append的行为的警告(比如:slice = append(slice))、增加了deferring time.Since的警告以及在log/slog包的方法调用时key-value pair不匹配的警告。

3. 标准库

最后,我们来看看标准库的变化。每次Go发布新版本,标准库都是占更新的大头儿,这里无法将所有变更点一一讲解,仅说说几个重要的变更点。

3.1 增强http.ServerMux表达能力

Go内置电池,从诞生伊始就内置了强大的http库,不过长期以来http原生的ServeMux表达能力比较单一,不支持通配符等,这也是Go社区长期以来一直使用像gorilla/muxhttprouter等第三方路由库的原因。

今年log/slog的作者Jonathan Amsterdam又创建了新的提案:net/http: enhanced ServeMux routing,提高http.ServeMux的表达能力。在新提案中,新的ServeMux将支持如下路由策略(来自http.ServeMux的官方文档):

  • “/index.html”路由将匹配任何主机和方法的路径”/index.html”;
  • “GET /static/”将匹配路径以”/static/”开头的GET请求;
  • “example.com/”可以与任何指向主机为”example.com”的请求匹配;
  • “example.com/{$}”会匹配主机为”example.com”、路径为”/”的请求,即”example.com/”;
  • “/b/{bucket}/o/{objectname…}”匹配第一段为”b”、第三段为”o”的路径。名称”bucket”表示第二段,”objectname”表示路径的其余部分。

下面就是基于上面的规则编写的示例代码:

// lib/servemux/main.go

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/index.html", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match /index.html`)
    })
    mux.HandleFunc("GET /static/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "GET /static/"`)
    })
    mux.HandleFunc("example.com/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "example.com/"`)
    })
    mux.HandleFunc("example.com/{$}", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "example.com/{$}"`)
    })
    mux.HandleFunc("/b/{bucket}/o/{objectname...}", func(w http.ResponseWriter, req *http.Request) {
        bucket := req.PathValue("bucket")
        objectname := req.PathValue("objectname")
        fmt.Fprintln(w, `match /b/{bucket}/o/{objectname...}`+":"+"bucket="+bucket+",objectname="+objectname)
    })

    http.ListenAndServe(":8080", mux)
}

我们使用curl对上述示例进行一个测试(前提是在/etc/hosts中设置example.com为127.0.0.1):

$curl localhost:8080/index.html
match /index.html

$curl example.com:8080/static/abc
match "example.com/"

$curl localhost:8080/static/abc
match "GET /static/"

$curl example.com:8080/
match "example.com/{$}"

$curl example.com:8080/b/mybucket/o/myobject/tonybai
match "example.com/"

$curl localhost:8080/b/mybucket/o/myobject/tonybai
match /b/{bucket}/o/{objectname...}:bucket=mybucket,objectname=myobject/tonybai

从测试情况来看,不同路由设置之间存在交集,这就需要路由匹配优先级规则。新版Go ServeMux规定:如果一个请求有两个或两个以上的模式匹配,则更具体(specific)的模式优先。如果P1符合P2请求的严格子集,也就是说,如果P2符合P1及更多的所有请求,那么P1就比P2更具体。

举个例子:”/images/thumbnails/”比”/images/”更具体,因此两者都可以注册。前者匹配以”/images/thumbnails/”开头的路径,后者则匹配”/images/”子树中的任何其他路径。

如果两者都不更具体,那么模式就会发生冲突。为了向后兼容,这一规则有一个例外:如果两个模式发生冲突,而其中一个模式有主机(host),另一个没有,那么有主机的模式优先(比如上面测试中的第二次curl执行)。如果通过ServeMux.Handle或ServeMux.HandleFunc设置的模式与另一个已注册的模式发生冲突,这些函数就会panic。

增强后的ServeMux可能会影响向后兼容性,使用GODEBUG=httpmuxgo121=1可以保留原先的ServeMux行为。

3.2 增加math/rand/v2包

在日常开发中,我们多会在生成随机数的场景下使用math/rand包,其他时候使用的较少。但Go 1.22中新增了math/rand/v2包,我之所以将这个列为Go 1.22版本标准库的一次重要变化,是因为这是标准库第一次为某个包建立v2版本包,按照Russ Cox的说法,这次v2包的创建,为标准库中的其他可能的v2包树立了榜样。创建math/rand/v2可以使Go团队能够在一个相对不常用且风险较低的包中解决工具问题(如gopls、goimports等对v2包的支持),然后再转向更常用、风险更高的包,如sync/v2或encoding/json/v2等。

新增rand/v2包的直接原因是清理math/rand并修复其中许多悬而未决的问题,特别是使用过时的生成器、慢速算法以及与crypto/rand冲突的问题,这里就不针对v2包举具体的示例了,对该包感兴趣的同学可以自行阅读该包的在线文档,并探索如何使用v2包。

同时,该提案也为标准库中的v2包的创建建立了一种模式,即v2包是原始包的子目录,并且以原始包的API为起点,每个偏离点都要有明确的理由。

想当初,go module刚落地到Go中时,Go module支持两种识别major的两种方式,一种是通过branch或tag号来识别,另外一种就是利用vN目录来定义新包。当时还不是很理解为什么要有vN目录这种方式,现在从math/rand/v2包的增加来看,足以体现出当初module设计时的前瞻性考量了。

3.3 大修Go execution tracer

Go Execution Tracer是解决Go应用性能方面“疑难杂症”的杀手锏级工具,它可以提供Go程序在一段时间内发生的情况的即时视图。这些信息对于了解程序随时间推移的行为非常宝贵,可辅助开发人员对应用进行性能改进。我曾在《通过实例理解Go Execution Tracer》中对其做过系统的说明。

不过当前版本的Go Execution Tracer在原理和使用方面还存在诸多问题,Google的Michael Knyszek在年初发起了Execution tracer overhaul的提案,旨在对Go Execution Tracer进行改进,使Go Execution Tracer可扩展到大型Go部署的Go执行跟踪。具体目标如下:

  • 使跟踪解析所需的内存占用量仅为当前的一小部分。
  • 支持可流式传输的跟踪,以便在无需存储的情况下进行分析。
  • 实现部分自描述的跟踪,以减少跟踪消费者的升级负担。
  • 修复长期存在的错误,并提供一条清理实现的路径。

在近一年的时间里,Knyszek与Felix Geisendorfer、Nick Ripley、Michael Pratt等一起实现了该提案的目标。

鉴于篇幅,这里就不对新版Tracer的使用做展开说明,有兴趣的童鞋可结合《通过实例理解Go Execution Tracer》中的使用方法自行体验新版Tracer。

注:新版Tracer的设计文档 – https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md

3.4 其他

Go 1.4版本以来,syscall包新特性就已经被冻结,并在Go 1.11版本中被标记为不推荐使用(deprecate)。Go团队推荐gopher使用golang.org/x/sys/unix或golang.org/x/sys/windows。syscall包的大多数功能都能被golang.org/x/sys包替代,除了下面这几个:

syscall.SysProcAttr(类型os/exec.Cmd.SysProcAttr)
syscall.Signal(参考文献os.Signal)
syscall.WaitStatus(参考文献os.(*ProcessState).Sys)
syscall.Stat_t
... ...

由于syscall包已经弃用,IDE等工具在开发人员使用上述内容时总是得到警告!这引发了众多开发人员的抱怨。为此,在Go 1.22版本中,syscall取消了弃用状态,但其功能特性依旧保持冻结,不再添加新特性。

  • TCPConn to UnixConn:支持zerocopy

gnet作者Andy Pan的提案:TCPConn to UnixConn:支持zerocopy在Go 1.22落地,具体内容可以看一下原始提案issue

  • 新增go/version包

在Go 1.21版本发布后,Go团队对Go语言的版本规则做了调整,并明确了Go语言的向前兼容性和toolchain规则,Go 1.22中增加go/version包实现了按照上述版本规则的Go version判断,这个包既用于go工具链,也可以用于Gopher自行开发的工具中。

4. 小结

Go 1.22版本具有至少两点重要的里程碑意义:

  • 通过对loopvar语义的修正,开启了Go已有“语法坑”的fix之路
  • 通过math/rand/v2包树立了Go标准库建立vN版本的模式

“语法坑”fix是否能得到社区正向反馈还是一个未知数,其导致的兼容性问题势必会成为Go社区在升级到Go 1.22版本的重要考虑因素,即便决定升级到Go 1.22,严格的代码审查和测试也是必不可少的。

最后,感谢Go团队以及所有Go 1.22贡献者做出的伟大工作!

文本涉及的源码可以在这里下载。

5. 参考资料

-Go 1.22 Milestone – https://github.com/golang/go/milestone/298


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats