标签 runtime 下的文章

Go 1.17中值得关注的几个变化

本文永久链接 – https://tonybai.com/2021/08/17/some-changes-in-go-1-17

Go核心开发团队在去年GopherCon大会上给Go泛型的定调是在2022年2月份的Go 1.18版本中发布,那可是自Go诞生以来语法规范变动最大的一次,这让包括笔者在内的全世界的Gopher们都满怀期待。

不过别忘了,在Go 1.18这个“网红版本”发布前,还有一个“实力派”版本Go 1.17呢!美国当地时间2021年8月16日,Go 1.17版本在经过两个RC版本之后正式发布!并且值得庆幸的是Go 1.17版本并没有过多受到Go 1.18版本这个“网红”的影响,Go 1.17默默地加入和优化了着实不少的特性。在这一篇文章中,我们就来看看Go 1.17版本中有哪些值得关注的变化。

1. 语言特性变化

Go属于那种极简的语言,从诞生到现在语言自身特性变化很小,不会像其他主流语言那样走“你有的我也要有”的特性融合路线。因此新语言特性对于Gopher来说属于“稀缺品”,属于“供不应求”那类事物^_^。这也直接导致了每次Go新版本发布,我们都要首先看看语言特性是否有变更,每个新加入语言的特性都值得我们去投入更多关注,去深入研究。Go 1.17在语言特性层面做了两方面的小改动,下面我们来看看。

第一个是对语言类型转换规则的扩展,允许从切片到数组指针的转换,下面的代码在Go 1.17版本中是可以正常编译和运行的:

// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/slice2arrayptr/main.go
func slice2arrayptr() {
    var b = []int{11, 12, 13}
    var p = (*[3]int)(b)
    p[1] = p[1] + 10
    fmt.Printf("%v\n", b) // [11 22 13]
}

Go通过运行时对这类切片到数组指针的转换代码做检查,如果发现越界行为,就会通过运行时panic予以处理。Go运行时实施检查的一条原则就是“转换后的数组长度不能大于原切片的长度”,注意这里是切片的长度(len),而不是切片的容量(cap)。

第二个变动则是unsafe包增加了两个函数:Add与Slice。使用这两个函数可以让开发人员更容易地写出符合unsafe包使用的安全规则的代码。这两个函数原型如下:

// $GOROOT/src/unsafe.go
func Add(ptr Pointer, len IntegerType) Pointe
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

unsafe.Add允许更安全的指针运算,而unsafe.Slice允许更安全地将底层存储的指针转换为切片。

2. go module的变化

Go 1.11版本引入go module以来,每个Go大版本发布时,go module都会有不少的积极变化,这是Go核心团队与社区就go module深入互动的结果。

Go 1.17中go module同样有几处显著变化,其中最最重要的一个变化就是pruned module graph(修剪的module依赖图)。Go 1.17之前的版本某个module的依赖图由该module的直接依赖以及所有间接依赖组成,无论某个间接依赖是否真正为原module的构建做出贡献,这样go命令在解决依赖时会读取每个依赖的go.mod,包括那些没有被真正使用到的module,这样形成的module依赖图被称为完整module依赖图(complete module graph)

Go 1.17不再使用“完整module依赖图”,而是引入了pruned module graph(修剪的module依赖图)。修剪的module依赖图就是在完整module依赖图的基础上将那些“占着茅坑不拉屎”、对构建完全没有“贡献”的间接依赖module修剪后的依赖图。使用修剪后的module依赖图进行构建将有助于避免下载或阅读那些不必要的go.mod文件,这样Go命令可以不去获取那些不相关的依赖关系,从而在日常开发中节省时间。

但module依赖图修剪也带来了一个副作用,那就是go.mod文件size的变大。因为Go 1.17版本后,每次go mod tidy(当go.mod中的go版本为1.17时),go命令都会对main module的依赖做一次深度扫描(deepening scan),并将main module的所有直接和间接依赖都记录在go.mod中(之前说的版本只记录直接依赖)。考虑到内容较多,go 1.17将直接依赖和间接依赖分别放在两个不同的require块儿中。

3. 编译器与运行时的变化

Go 1.17增加了对Windows上64位ARM架构的支持,让开发者可以在更多设备上原生运行Go。但这个版本编译器最大的变化是在amd64架构下率先实现了从基于堆栈的调用惯例到基于寄存器的调用惯例切换

并且,切换到基于寄存器的调用惯例后,一组有代表性的Go包和程序的基准测试显示,Go程序的运行性能提高了约5%,二进制文件大小典型减少约2%。也就是说你的Go源码使用Go 1.17版本重新编译一下就能获得大约5%的性能提升,真希望这样的优化越多越好!对更多平台的基于寄存器调用惯例的支持将在未来的版本中出现。

除了改为基于寄存器的调用惯例之外,Go 1.17编译器还支持包含闭包的函数的内联(inline)了!这样一来,一个带有闭包的函数可能会在函数被内联的每个地方产生一个不同的闭包代码指针,因此,
Go函数的值不能直接比较

Go编译器还在Go 1.17中引入了//go:build形式的构建约束指示符,以替代原先易错的// +build形式。

4. 其他变化

  • 保留龙芯架构GOARCH值

在Go 1.17版本中,Go编译器保留了中国龙芯cpu架构的GOARCH值 – loong64。关于龙心GOARCH值选用loong64还是loongarch64还有过一段激烈的争论,最终大多数都赞同的loong64取得了最后的胜利。

  • Go test变化

Go test引入-shuffle的洗牌标志位,用以控制单元测试或benchmark的执行顺序。

另外T和B两个类型分别都增加了Setenv方法用于在test和benchmark执行期间设置环境变量。

  • time包增加Time对象的GoString形式输出

我们使用%#v输出一个Time对象实例时,Go 1.17之前的版本输出内容如下面:

Go 1.16.5输出:

time.Time{wall:0xc03f08c0d06c9ed0, ext:83078, loc:(*time.Location)(0x11620e0)}

Go 1.17增加了GoString方法,该方法在Time对象以%#v格式输出时被自动调用,其输出结果如下:

time.Date(2021, time.August, 17, 20, 29, 42, 58245000, time.Local)

5. 小结

除上述变化之外,Go的其他标准库随着新版本的发布也都会有大量的小改动,但每个开发人员对标准库的关注点差别很大,因此,在这个系列中不会详细做说明了,大家还是参考Go 1.17的发布说明文档各取所需吧^_^。

与传统的“Go新版本值得关注的几个变化”系列有所不同,本期内容较为简单和概括,因为更多内容,我将在后续的Go 1.17新特性详解系列中针对上述值得关注的新特性做进一步说明。详解系列已经写好,不过首发在了本人运营的星球“Gopher部落”上了,如果你迫切想深入了解这些新特性,可以加入星球阅读。

本文所涉及的源码可以在这里 – https://github.com/bigwhite/experiments/tree/master/go1.17-examples/


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

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

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

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

Go中被闭包捕获的变量何时会被回收

本文永久链接 – https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go

1. Go函数闭包

Go语言原生提供了对闭包(closure)的支持。在Go语言中,闭包就是函数字面值。Go规范中是这样诠释闭包的:

函数字面值(function literals)是闭包:它们可以引用其包裹函数(surrounding function)中定义的变量。然后,这些变量在包裹函数和函数字面值之间共享,只要它们可以被访问,它们就会继续存在。

闭包在Go语言中有着广泛的应用,最常见的就是与go关键字一起联合使用创建一个新goroutine,比如下面标准库中net/http包中的一段代码:

// $GOROOT/src/net/http/fileTransport.go

00 func (t fileTransport) RoundTrip(req *Request) (resp *Response, err error) {
01     rw, resc := newPopulateResponseWriter()
02     go func() {
03         t.fh.ServeHTTP(rw, req)
04         rw.finish()
05     }()
06     return <-resc, nil
07 }

上面这段代码中的RoundTrip方法就是使用go关键字结合闭包创建了一个新的goroutine,并且在这个goroutine中运行的函数还引用了本属于其外部包裹函数的变量:t、rw和req,或者说两者共享这些变量。

原本仅在RoundTrip方法内部使用的变量一旦被“共享”给了其他函数,那么它就无法在栈上分配了,逃逸到堆上是确定性事件。

那么问题来了!这些被引用或叫被闭包捕获的分配在堆上的外部变量何时能被回收呢?也许上面的例子还十分容易理解,当新创建的goroutine执行完毕后,这些变量就可以回收了。那么下面的闭包函数呢?

func foo() func(int) int {
    i := []int{0: 10, 1: 11, 15: 128}
    return func(n int) int {
        n+=i[0]
        return n
    }
}

在这个foo函数中,被闭包函数捕获的长度为16的切片变量i何时可以被回收呢?

注:我们定义闭包时,喜欢用引用外部包裹函数的变量这种说法,但在Go编译器的实现代码中,使用的是capture var,翻译过来就是“被捕获的变量”,所以这里也用了“捕获”一词来表示那些被闭包共享使用的外部包裹函数甚至是更外层函数中的变量。

foo函数的返回值类型是一个函数,也就是说foo函数的本地变量i被foo返回的新创建的闭包函数所捕获,i不会被回收。通常一个堆上的内存对象有明确的引用它的对象或指向它的地址的指针,该对象才会继续存活,当其不可达(unreachable)时,即再没有引用它的对象或指向它的指针时才会被GC回收。

那么,变量i究竟是被谁引用了呢?变量i将在何时被回收呢?

我们先回头看一个非闭包的一般函数:

func f1() []int {
    i := []int{0: 10, 1: 11, 15: 128}
    return i
}

func f2() {
    sl := f1()
    sl[0] = sl[0] + 10
    fmt.Println(sl)
}

func main() {
    f2()
}

我们看到f1将自己的局部切片变量i返回后,该变量被f2函数中的sl所引用,f2函数执行完成后,切片变量i将变成unreachable,GC将回收该变量对应的堆内存。

如果换成闭包函数,比如前面的foo函数,我们很大可能是这么来用的:

// https://github.com/bigwhite/experiments/tree/master/closure/closure1.go

 1 package main
 2
 3 import "fmt"
 4
 5 func foo() func(int) int {
 6     i := []int{0: 10, 1: 11, 15: 128}
 7     return func(n int) int {
 8         n += i[0]
 9         return n
10     }
11 }
12
13 func bar() {
14     f := foo()
15     a := f(5)
16     fmt.Println(a)
17 }
18
19 func main() {
20     bar()
21     g := foo()
22     b := g(6)
23     fmt.Println(b)
24 }

在这里例子中,只要闭包函数中引用了foo函数的本地变量。这突然让我想起了“在Go中,函数也是一等公民的特性”。难道是闭包函数这一对象引用了foo函数的本地变量? 那么闭包函数在内存布局上是如何引用到foo函数的本地整型切片变量i的呢?闭包函数在内存布局中被映射为什么了呢?

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。

2. Go闭包函数对象

要解答这个问题,我们只能寻求Go汇编的帮助。我们生成上面的closure1.go的汇编代码(我们使用go 1.16.5版本Go编译器):

$go tool compile -S closure1.go > closure1.s

在汇编代码中,我们找到closure1.go中第7行创建一个闭包函数所对应的汇编代码:

// https://github.com/bigwhite/experiments/tree/master/closure/closure1.s

    0x0052 00082 (closure1.go:7)    LEAQ    type.noalg.struct { F uintptr; "".i []int }(SB), CX
    0x0059 00089 (closure1.go:7)    MOVQ    CX, (SP)
    0x005d 00093 (closure1.go:7)    PCDATA  $1, $1
    0x005d 00093 (closure1.go:7)    NOP
    0x0060 00096 (closure1.go:7)    CALL    runtime.newobject(SB)
    0x0065 00101 (closure1.go:7)    MOVQ    8(SP), AX
    0x006a 00106 (closure1.go:7)    LEAQ    "".foo.func1(SB), CX
    0x0071 00113 (closure1.go:7)    MOVQ    CX, (AX)
    0x0074 00116 (closure1.go:7)    MOVQ    $16, 16(AX)
    0x007c 00124 (closure1.go:7)    MOVQ    $16, 24(AX)
    0x0084 00132 (closure1.go:7)    PCDATA  $0, $-2
    0x0084 00132 (closure1.go:7)    CMPL    runtime.writeBarrier(SB), $0
    0x008b 00139 (closure1.go:7)    JNE 165
    0x008d 00141 (closure1.go:7)    MOVQ    ""..autotmp_7+16(SP), CX
    0x0092 00146 (closure1.go:7)    MOVQ    CX, 8(AX)
    0x0096 00150 (closure1.go:7)    PCDATA  $0, $-1
    0x0096 00150 (closure1.go:7)    MOVQ    AX, "".~r0+40(SP)
    0x009b 00155 (closure1.go:7)    MOVQ    24(SP), BP
    0x00a0 00160 (closure1.go:7)    ADDQ    $32, SP
    0x00a4 00164 (closure1.go:7)    RET
    0x00a5 00165 (closure1.go:7)    PCDATA  $0, $-2
    0x00a5 00165 (closure1.go:7)    LEAQ    8(AX), DI
    0x00a9 00169 (closure1.go:7)    MOVQ    ""..autotmp_7+16(SP), CX
    0x00ae 00174 (closure1.go:7)    CALL    runtime.gcWriteBarrierCX(SB)
    0x00b3 00179 (closure1.go:7)    JMP 150
    0x00b5 00181 (closure1.go:7)    NOP

汇编总是晦涩难懂。我们重点看第一行:

    0x0052 00082 (closure1.go:7)    LEAQ    type.noalg.struct { F uintptr; "".i []int }(SB), CX

我们看到对应到Go源码中创建闭包函数的第7行,这行汇编代码大致意思是将一个结构体对象的地址放入CX。我们把这个结构体对象摘录出来:

struct {
    F uintptr
    i []int
}

这个结构体对象是哪里来的呢?显然是Go编译器根据闭包函数的“特征”创建出来的。其中的F就是闭包函数自身的地址,毕竟是函数,这个地址与一般函数的地址应该是在一个内存区域(比如rodata的只读数据区),那么整型切片变量i呢?难道这就是闭包函数所捕获的那个Foo函数本地变量i。没错!正是它。如果不信,我们可以再定义一个捕获更多变量的闭包函数来验证一下。

下面是一个捕获3个整型变量的闭包函数的生成函数:

// https://github.com/bigwhite/experiments/tree/master/closure/closure2.go

func foo() func(int) int {
    var a, b, c int = 11, 12, 13
    return func(n int) int {
        a += n
        b += n
        c += n
        return a + b + c
    }
}

其对应的汇编代码中那个闭包函数结构为:

0x0084 00132 (closure2.go:10)   LEAQ    type.noalg.struct { F uintptr; "".a *int; "".b *int; "".c *int }(SB), CX

将该结构体提取出来,即:

struct {
    F uintptr
    a *int
    b *int
    c *int
}

到这里,我们证实了引用了包裹函数本地变量的正是闭包函数自身,即编译器为其在内存中建立的闭包函数结构体对象。通过unsafe包,我们甚至可以输出这个闭包函数对象。以closure2.go为例,我们来尝试一下,如下面代码所示。

// https://github.com/bigwhite/experiments/tree/master/closure/closure2.go

func foo() func(int) int {
    var a, b, c int = 11, 12, 13
    return func(n int) int {
        a += n
        b += n
        c += n
        return a + b + c
    }
}

type closure struct {
    f uintptr
    a *int
    b *int
    c *int
}

func bar() {
    f := foo()
    f(5)
    pc := *(**closure)(unsafe.Pointer(&f))
    fmt.Printf("%#v\n", *pc)
    fmt.Printf("a=%d, b=%d,c=%d\n", *pc.a, *pc.b, *pc.c)
    f(6)
    fmt.Printf("a=%d, b=%d,c=%d\n", *pc.a, *pc.b, *pc.c)
}

在上面代码中,我们参考汇编的输出定义了closure这个结构体来对应内存中的闭包函数对象(每种闭包对象都是不同的,一个技巧就是参考汇编输出的对象来定义),通过unsafe的地址转换,我们将内存中的闭包对象映射到closure结构体实例上。运行上面程序,我们可以得到如下输出:

$go run closure2.go
main.closure{f:0x10a4d80, a:(*int)(0xc000118000), b:(*int)(0xc000118008), c:(*int)(0xc000118010)}
a=16, b=17,c=18
a=22, b=23,c=24

在上面的例子中,闭包函数捕获了外部变量a、b和c,这些变量实质上被编译器创建的闭包内存对象所引用。当我们调用foo函数时,闭包函数对象创建(其地址赋值给变量f)。这样,f对象一直引用着变量a、b和c。只有当f被回收,a、b和c才会因unreachable而被回收。

如果我们在闭包函数中仅仅是对捕获的外部变量进行只读操作,那么闭包函数对象不会存储这些变量的指针,而仅会做一份值拷贝。当然,如果某个变量被一个函数中创建的多个闭包所捕获,并且有的只读,有的修改,那么闭包函数对象还是会存储该变量的地址的。

了解了闭包函数的本质,我们再来看本文标题中的问题就容易多了。其答案就是在捕捉变量的闭包函数对象被回收后,如果这些被捕捉的变量没有其他引用,它们将变为unreachable的,后续就会被GC回收了

3. 小结

我们回顾一下文章开头引用的Go语言规范中对闭包诠释中提到的一句话:“只要它们可以被访问,它们就会继续存在”。现在看来,我们可以将其理解为:只要闭包函数对象存在,其捕获的那些变量就会存在,就不会被回收

闭包函数的这种机制决定了我们在日常使用过程中也要时刻考虑着闭包函数所捕获的变量可能的“延迟回收”。如果某个场景下,闭包引用的变量占用内存较大,且闭包函数对象被创建出的数量很多且因业务需要延迟很久才会被执行(比如定时器场景),这就会导致堆内存可能长期处于高水位,我们要考虑内存容量是否能承受这样的水位,如果不能,则要考虑更换实现方案了。

本文涉及的所有代码可以从这里下载:https://github.com/bigwhite/experiments/tree/master/closure

4. 参考资料

  • 深入理解函数闭包 – https://zhuanlan.zhihu.com/p/56750616
  • Go语言高级编程 – https://github.com/chai2010/advanced-go-programming-book/blob/master/ch3-asm/ch3-06-func-again.md#366-闭包函数

“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

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

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系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