标签 gomodule 下的文章

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未来演进:基于共同目标和数据驱动的决策

本文永久链接 – https://tonybai.com/2023/12/10/go-changes

自从Go语言之父Rob Pike从Google退休并隐居澳洲后,Russ Cox便成为了Go语言团队的“带头大哥”,虽然其资历还无法与依旧奋战在一线的另外一位Go语言之父Robert Griesemer相比。如今,Russ Cox对Go语言未来的演化发展是很有“发言权”的,Go module的引入便是Russ Cox的重要决策之一。从Go社区来看,这些年来,以Russ Cox为首的Go团队对Go演进决策总体上是良性的、受欢迎的,比如Go module、Go泛型、Go对wasm的支持等,当然也有一些变化是受到质疑的,比如:Go 1.22版本很可能从试验特性到正式特性的loopvar等

注:我的极客时间《Go语言第一课》专栏中有对Go module和Go泛型的详细讲解,欢迎感兴趣的童鞋订阅阅读。

想必很多Gopher也和我一样,对Go团队就某一proposal的决策方式和依据很好奇 –到底他们是如何决定是否accept这个proposal的?Go语言后续该如何演化?向哪个方向发展演化?

今年9月份举办的GopherCon 2023上,Russ Cox代表Go团队做了名为“Go Changes”的主题演讲

在这个talk中,我们能找到一些答案。近期他重新录制了该演讲视频,并在其个人博客中放出。

本文就是基于这个视频内容进行整理加工后的文字稿,供国内广大gopher参考。


这是我在2023年GopherCon上做的一次演讲的重新录制视频。在这次演讲中,我和大家分享了三部分内容:为什么Go需要随着时间的推移而改变,我们如何应对Go的变化过程,以及为什么选择性遥测(opt-in telemetry)是这个过程中的一个重要且适当的部分。不过,这个演讲不是关于某个特定的Go特性变化,而是关于Go整体的变化过程,特别是我们是如何决定做出哪些改变的。

首先一个明显的问题是,为什么Go需要改变? 为什么我们不能对Go感到满意,然后将其束之高阁呢? 一个显而易见的答案是我们不可能一次就把事情做对,你对比一下上面图片中展示的第一版毛绒Go吉祥物和我们在GopherCon上发放的最终版本,你就能明白我的意思了。

但这里还有一个更深层次的答案:

我的一位前同事在他使用了多年的邮件签名中引用了生物学家兼科幻小说作家杰克·科恩(Jack Cohen)的一句名言。在这句名言中,科恩说:“我们生物学家使用的一个描述‘稳定(stable)’的专业词汇就是“死(dead)”。

所有的生命都在变化,适应新的环境,修复损伤等。编程环境也需要改变。除非我们想要Go死掉,否则它需要适应新的环境,比如新的协议、操作系统和重要用例。我们也需要发现并修复bug — 语言、库和生态系统的问题,这些问题只有随着时间的推移或Go发展到一定阶段和规模才会暴露出来。

Go必须改变,并与时俱进。这次演讲就是关于我们如何决定做出哪些改变

这次演讲分三个部分:

  • 第一部分是关于我们对Go的愿景和期望。
  • 第二部分是关于我们如何利用数据来决定做出哪些改变。
  • 第三部分是关于我们在Go工具链中增加选择性遥测的计划,以便更好地理解Go的使用情况和出现问题的地方。

到演讲结束时,你将了解我们考量和决定Go变化的过程,并了解数据在做出这些决定中的重要性,我希望你能理解为什么选择性遥测是一个很好的额外数据来源,甚至可能愿意在系统推出时就选择加入。

让我们从这个开始:我们希望Go发生什么样的变化?如果我们在这个基本问题上意见不一致,我们也就无法就具体的变化达成共识。

例如,我们是否应该在Go中添加一个Perl语句,让我们可以用Perl编写函数?

我认为我们不应该,但假设你有不同意见。为了解决这个问题,我们需要理解为什么我们持不同意见。

约翰·奥斯特豪特(John Ousterhout)写了一份名为“开放决策制定(Open Decision Making)”的好文档,内容虽然来自他在创业公司的经验,但它几乎完全适用于开源项目。

在这份文档中,他提出的最重要的观点之一是:如果一群聪明人面对同一个问题,并具有相同的信息,如果他们有相同的目标,那么他们很可能得出相同的结论

如果你和我在Go中是否要嵌入Perl这个问题上存在分歧,根本原因肯定是我们对Go目标有不同的理解,所以我们必须建立明确Go的目标。

Go的目标是更好的软件工程,特别是大规模软件工程。Go的独特设计决策几乎全部针对这个目标。我们已经多次阐述过这一点,包括在上述截图中的这两篇文章中。再说一次,Go的目标是更好的软件工程

现在我们来说说Perl。20年前,当我很年轻、甚至有些天真、Go还不存在的时候,我编写并部署了一个完全用Perl编写的大型分布式系统。我热爱Perl所擅长的东西,但它并不是以更好的软件工程为目标。如果我们在这一点上有分歧,那么我可能应该定义一下我所说的软件工程是什么意思。

注:如果要理解Go以更好软件工程为目标,或是Google的软件工程理念,可以阅读一下《Software Engineering at Google》这本佳作。

我喜欢说,当你给编程加入时间和其他程序员时,软件工程就出现了。编程意味着让一个程序工作。你有一个要解决的问题,你编写一些代码,运行它,调试它,得到答案,完成。这就是编程,这已经够难的了。

但是当那段代码不得不日复一日地继续工作时会发生什么,甚至和其他人一起对它进行维护?那么你需要添加测试,以确保你修正后的bug不会在6个月后由你自己或是一个不熟悉这段代码的新团队成员重新引入。这就是为什么Go从第一天开始就内置了对测试的支持,并建立了一种文化,那就是对任何bug的修复或新增代码都要添加测试。

那么随着时间的流逝,当代码必须在Go本身发生改变的情况下继续工作时会发生什么?那么我们需要强调兼容性,这是Go1版本以来一直在做的。事实上,Go 1.21版本发布了许多兼容性改进,我在2022年的GopherCon上对此有过介绍。

随着代码量的增长,如果需要某种全局清理时该怎么办?你需要工具,而不可避免的第一个绊脚石是那些工具需要模仿代码的格式化风格来编辑,以避免出现无关的差异。gofmt的存在是为了支持goimports、gorename、go fix和gopls等工具,以及你自己可能使用我们提供的包编写的自定义工具。

既然提到了软件包,当你使用其他人提供的软件包时,不可避免的第一个绊脚石是多个人会用相同的名字(比如sqlite或yaml)编写软件包。那么我们如何在一个给定的程序中识别究竟使用哪个了呢?为了在一个去中心化的方式无歧义地回答这个问题,Go使用URL作为包导入路径。

随着时间的推移,下一个问题是挑选使用特定软件包的哪个版本,并决定该版本是否与所有其他依赖项兼容。这就是为什么Go提供了modules、workspaces、Go modules mirror镜像和Go module校验和数据库。

接下来的问题是每个人的代码都有bug,包括安全bug。你需要了解关于最重要bug的信息,这样你就知道需要更新到已修复的版本。这就是为什么我们添加了Go漏洞数据库和govulncheck,Julie也在GopherCon上谈到了这一点,当有视频链接时我会在下面添加。

以上是较大的例子,但也有小的例子,比如添加新的协议如HTTP/3,移除对过时平台的支持,以及修复或废弃容易出错的API,以避免大型代码库中的常见错误。

这把我们带到了Go提案过程(Proposal Process),这是我们对是否接受(accept)和拒绝(decline)哪些变更做出决定的方式:

当我们考虑这些决定时,使用数据非常重要,这可以帮助我们达成共识。

简单地说,任何人都可以在Go的GitHub问题跟踪器上提出Go更改提案(Change Proposal)。然后,在该问题上进行讨论,我们试图在参与者之间就是否接受或拒绝该建议达成共识,或者该建议需要做出什么修改才能被接受。

随着时间的推移,我们越来越欣赏约翰·奥斯特豪特在他的观察中提出的第二句话的重要性:如果面对问题的人不仅共同的目标,还有共同的信息,他们很可能会达成共识。

在Go的早期,只有我们几个人做决定。我们根据技术判断和直觉做出决定,这些判断和直觉是基于我们过去的经验。那些经验就是我们使用的信息。由于我们的过去经验有足够的重叠,我们大多数时候能达成共识。大多数小项目都是这种工作方式。

随着决策涉及的人数大大增加,共享经验就会减少。我们需要一个新的共享信息来源。我们发现的最好信息来源是收集实际数据,然后将这些数据作为共享信息来做决策。但是我们从哪里获得这些数据呢?对Go来说,我们有许多潜在的来源,每一个都适合具体的决策类型。在这里,我将向你展示其中的一些。

一个数据来源是与Go用户交谈。我们以各种方式做到这一点:

首先是Go用户调查,我们从2016年开始每年做一次,最近开始一年做两次。调查非常适合了解Go最流行的用途以及人们面临的最常见问题。多年来,最常见的问题是缺乏依赖管理和泛型。我们使用这些信息将开发Go模块和泛型作为优先事项。

另一个数据来源是我们可以在VSCode中使用VSCode Go插件运行的调查。这些调查可以帮助我们了解VSCode Go体验的实效性。

来自用户的最后一个直接数据来源是我们全年进行的研究访谈和用户体验研究。这些研究允许我们从小规模的用户群体中识别模式或获取更多关于特定主题的信息。

调查和访谈通过与用户交谈来收集数据。另一个数据来源是阅读代码:我们可以分析已发布的开源Go module代码。

例如,在添加新的“go vet”检查之前,我们会在开源代码库的一个子集上运行它,然后读取一些随机样本的结果,看检查是否指出了真实的问题,以及它是否有太多的假阳性。

在Go 1.22版本,我们计划添加一个go vet检查,检查对append的调用是否没有append任何内容。这里有检查器标记的两段代码:

顶部的一段代码表明开发人员可能认为append总是复制其输入slice。底部的一段代码可能是正确的,但难于措辞来描述。

这里还有另外两段代码:

在顶部的一段中,或者for循环从未运行,或者它永远不会完成,因为e.Sigs的长度永远不会改变。底部的代码也似乎是一个清晰的bug:代码正在仔细决定将消息追加到哪个列表中,然后它没有将其追加到任何一个列表中。

由于我们对样本代码段进行的所有采样都是可疑的或完全错误的,我们决定添加该检查。在这里,数据比直觉更好。

所有这些方法都是在少量样本上工作。对于典型的代码分析,我喜欢手动检查100个样本,与世界上所有Go代码的量相比,这只是一个微小的比例。最后一份Go开发者调查有不到6000名受访者,而全世界可能有300万Go开发者,样本比例不到1%。

一个很好的问题是为什么这些极小的样本能告诉我们有关更大人群的信息?答案是抽样精度只依赖于样本数量,而不依赖于总体规模。

这乍一看似乎反直觉,但假设我有一个装有100万只Go吉祥物的大箱子,我随机拿出两个。首先我拿到一个蓝色的,然后我拿到一个粉红色的。根据这两个样本,我估计箱子中的吉祥物大约一半是蓝色的,一半是粉红色的。但如果我告诉你箱子里有粉红色、蓝色和灰色的吉祥物,你是否会感到十分惊讶? 不会非常惊讶!如果箱子正好分三分之一粉红色、蓝色和灰色,那么这9对颜色组合中的每一对都同样可能:

得到一个非灰色吉祥物的机会是2/3,得到两个的机会就是2/3的平方,即4/9。没看到灰色的情况出现概率将近一半。这就是为什么我们不会非常惊讶的原因。

现在假设我取出100只,有48只蓝色和52只粉红色。我再次估计箱子大约一半是蓝色,一半是粉红色。现在如果我告诉你箱子里有粉红色、蓝色和灰色的吉祥物,你会有多惊讶?你应该会非常惊讶。

事实上,你完全不应该相信我。如果那是真的,得到100只连续的非灰色吉祥物的机会是2/3的100次方,约等于10的负48次方:

随机出现这种情况的可能性为零。要么我在说谎,要么我没有随机抽取。可能所有的灰色吉祥物都在箱子底部,我没有抽取到足够深的地方。

请注意:这都不依赖于箱子中有多少只Go吉祥物,它只取决于我们取出了多少只。用于特定预测精度的数学更复杂,但具有相同的效果:只有样本数量重要,箱子中的吉祥物数目不重要

一般来说,手工计算这些数学太困难了,所以这里有一个表格,你可以在我的博客上找到:

它说明,如果你提取100个样本并根据这些样本估计百分比,那么90%的时间你的估计将在真实百分比的正负8%之内。99%的时间它们将在13%之内。如果像Go调查中那样有5000个样本,那么90%的时间估计误差在正负1%之内,99%的时间在正负2%之内。超过这个数量,我们实际上不需要更多样本。

有一个注意事项是样本需要是随机的, 或者至少与你正在估计的内容不相关。你不能只从箱子的顶部抽取吉祥物,然后对整个箱子做出断言。

如果你避免了这个错误, 那么当你试图估计一个新的API是否有用或者某个特定的vet check是否值得的时候, 花一个小时左右手动检查100个样本是合理的。如果是一个坏主意, 那将很快显现出来。而如果看起来是一个好主意, 再花几个小时检查更多的样本, 无论是手动检查还是用程序检查,都会大大提高你的估计准确性。与做出错误决策的代价相比,这是一个非常小的成本。

简而言之,采样的魔力在于将许多一次性估计转变为可以手动或用少量数据完成的工作。这就是为什么我们已经看到的所有数据来源都能够相当好地代表整个Go开发者群体的原因。

现在进入演讲的第三部分:Go工具链中的遥测(Telemetry):

遥测也将是Go开发者使用的一个小样本,但它应该是一个有代表性的样本,并且回答不同的问题,而不是调查和代码分析所做的问题。

遥测始终是一个有争议的话题,特别是对于开源项目来说,所以让我从最重要的细节开始说起:上传遥测报告是完全自愿和选择加入的:

除非你运行一个显式命令选择加入数据收集,否则不会上传任何数据。而且,这不是那种上传你的全部活动的详细跟踪的遥测系统。这种遥测也只适用于我们作为Go发行版的一部分分发的命令,比如gopls、go命令和编译器(compiler),它不会涉及你构建的任何程序

在我更详细地描述完这个系统之后,我希望你会发现你会愿意选择加入这个遥测系统。实际上,我们给自己设定的主要设计限制是,即使由其他人运行,我们也愿意选择加入该系统。

在我以2023年11月的录制这个内容时,该系统刚刚开始运行,只有少数人被要求在VSCode Go中选择加入gopls遥测。所以总体来说,你现在还不能选择加入。但希望很快你就可以了。

在我们深入了解细节之前,遥测的动机是它提供了与调查和代码分析不同的信息。它主要提供的两个类别是使用信息(Usage Information)和故障信息(Breakage Information)。调查让我们能够询问关于Go使用的广泛问题,但对于详细的使用信息来说并不好。那将是太多问题,对于调查对象来说,90%的问题要回答”no”是一种浪费时间。

这个幻灯片显示了我们在之前的版本中警告过即将删除的Go功能列表。列表中的最后一项,buildmode=shared,是我们试图移除的功能,但在事先警告后,至少有一个用户提出了异议,我们将其保留了下来。即便如此,buildmode=shared与Go module基本不兼容,所以它的使用可能非常有限。但我们没有数据,所以它仍然存在于代码库中。遥测可以为我们提供基本的使用信息,以便我们可以基于数据而不是猜测做出这些决策。

另一个重要的类别是故障信息:

如果Go工具链明显有问题,我们希望在GitHub上收到错误报告。但是Go工具链也可能以用户注意不到的微妙方式出现问题。一个例子是,在macOS上的Go 1.14到Go 1.19的版本中,标准库包的二进制文件在预先构建时使用了非默认的编译标志,这是一个意外,这使得它们看起来像是过时了,Go命令在运行时会重新编译它们,这意味着如果你的程序导入了net包,你需要安装Xcode中的C编译器来构建程序。我们希望Go能够自行构建纯Go程序,而无需其他工具链。因此,要求安装Xcode是一个bug。但是我们没有注意到这个问题,也没有用户在GitHub上报告它。遇到这个问题的人似乎只是安装了Xcode并继续进行了工作。遥测可以提供基本的性能指标,比如标准库缓存命中率,这样Go工具链的开发人员即使用户没有意识到这个问题,也能注意到这个问题。

另一个例子是编译器的内部崩溃:

Go编译器在程序的第一个错误处不会停止。它会继续进行,尽可能多地查找和报告不同的错误。但是有时,继续分析已知错误的程序会导致意外的panic。我们不希望向用户显示这样的崩溃。相反,编译器会从panic中恢复,并且仅报告已经发现的错误。这样,Go用户可以纠正这些错误,这也可能纠正隐藏的panic。用户的工作不会因为看到编译器崩溃而中断。这对用户来说是好的,但是Go工具链的开发人员仍然希望了解这个崩溃并修复这个错误。遥测可以确保即使用户不知道这个错误,但我们还能了解到这个错误。

为了收集使用情况和故障信息,Go遥测设计记录“计数器和崩溃”:

像go命令、Go编译器或gopls这样的Go工具链程序可以定义命名事件计数器,并在事件发生时递增计数器。事件还可以按堆栈跟踪单独计数。这些计数器在本地的磁盘文件中维护,每次保留一周的时间。在幻灯片上,gopls和其他工具正在将计数器写入每周的文件中。

每周一次,Go工具链中的上传程序(uploader)将从遥测服务器获取一个“上传配置”,其中列出了该周收集的特定事件名称。只有在遥测特定的提案审查过程达成共识后,才会更改该配置。该配置作为一个模块(module)提供,以保护下载的完整性,并保留过去配置的公共记录。然后,上传程序仅上传上传配置中列出的计数器。在幻灯片上,上传程序仅为gopls发送一份报告,仅包含少量计数器,即使磁盘上可能还有更多计数器。报告中包含关于使用gopls的编辑器的统计信息,以及关于完成请求的延迟的信息,还有一个发生了一次的gopls/bug事件,其中包含一个栈跟踪。

请注意,上传的数据中没有事件跟踪或任何用户数据,只有计数器、已在公共上传配置中列出的事件名称,以及Go工具链程序中的函数名称。还要注意,栈跟踪不包括任何函数的参数,只有函数名称,因此没有用户数据。

开源中的遥测可能会在拥有数据访问权限和没有数据访问权限的人之间产生信息失衡。我们希望避免这种情况。请记住奥斯特豪特规则:为了达成共识,我们需要每个人拥有相同的信息。由于Go的遥测上传不包含任何敏感数据,并且是在明确的选择同意的情况下收集的,我们可以完整地重新发布这些报告,以便任何人都可以进行任何数据分析。我们还将发布一些基本的图表,用于做出决策。我们唯一可能看到但没有重新发布的是报告来自哪些IP地址,我们的服务器会将这些信息与报告一起记录。

一个明显的问题是,是否有足够多的人选择启用遥测,以使数据足够准确以做出决策。幸运的是,采样的神奇之处在于可以帮助解决这个问题。

全球大约有300w Go开发者。当系统准备就绪并要求人们启用遥测时,即使只有千分之一的开发者选择参与,也会有3000名开发者,根据我们的图表显示,误差不到3%,置信度为99%。如果全球三分之二的Go开发者启用了遥测,那将是20000个样本,误差不到1%,置信度为99%。除此之外,我们实际上不需要更多的样本。如果我们持续获得更多的报告,我们可以调整上传配置,告诉系统在某个特定的周选择随机不上传任何东西。例如,如果有20万个系统选择了参与,我们可以告诉每个系统在任何给定的周上传的概率为10%。因此,即使我们预计选择参与率会很低,系统应该能够运行得很好,随着选择参与率的提高,Go遥测将从任何给定系统收集更少的数据。当然,这使得每个选择参与的人对我们来说更加重要。目前来说,Go遥测对于你们中的任何人来说都还没有准备好,但当准备好时,我希望你们会选择参与。

在结束之前,我希望你们从演讲中获得以下几点:

首先,Go需要不断变化,特别是随着计算世界的变化。

其次,任何改变的目标都是为了使Go在软件工程中变得更好,尤其是在规模化(scaling)方面。

第三,一旦我们确定了目标,达成共识的下一个最重要的部分是拥有共享数据来做出决策。

第四,Go工具链遥测是增补我们现有调查和代码分析数据的重要数据来源。

最后,在整个演讲中,虽然涉及到了数据和适当的统计,但我们评估的想法、假设和潜在的变化始终始于个人故事和对话。我们喜欢听到这些故事,并与你们所有人讨论如何使用Go,关于什么有效和什么无效。所以,请无论在什么情况下,无论是在会议上、邮件列表上还是在问题跟踪器上,请确保让我们知道Go对你们的工作情况以及存在的问题。我们总是很乐意听到这些。非常感谢。


“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