一文帶你看懂 Golang 最新特性
作者:騰訊 PCG 代碼委員會
經過十餘年的迭代,Go 語言逐漸成爲雲計算時代主流的編程語言。下到雲計算基礎設施,上到微服務,越來越多的流行產品使用 Go 語言編寫。可見其影響力已經非常強大。
一、Go 語言發展歷史介紹
Go 語言起源於 2007 年的 Google;創始人有三位,分別是 Ken Thompson、Rob Pike、Robert Griesemer;他們可謂是大佬中的大佬。最初的構想是新語言能夠匹配未來硬件發展趨勢、適合開發大規模網絡服務程序、程序員能夠更加專注業務邏輯的開發。Ian Lance Tyalor 和 Russ Cox 是 Go 核心開發團隊的第四、五位成員。
Go 語言 2009 年正式對外公佈,2012 年 1.0 版本正式發佈。2012 年到 2015 年是 Go 語言的築底成長期;從實現自引導的 Go1.5 版本開始,到 Go 1.9 版本,業界對 Go 的期望先是提升到峯值接着又開始跌落;隨着 Go 1.11 版本引入 Go Module,包依賴問題得到很好的解決,Go 進入穩步爬升階段。
Go 語言每年發佈兩次升級版本,二月和八月。1.11 版本之後 Go 發佈的版本如下:
二、Go1.22 新特性
1.22 是 24 年的第一個版本。主要變化分佈在工具鏈、運行時以及標準庫。堅持 “Go 1 兼容性” 承諾:只要符合 Go 1 語言規範的代碼,Go 編譯器保證向前兼容。
2.1 語言改進
1. 循環變量不再共享
Go1.22 之前版本 for 循環聲明的變量只創建一次,並在每次迭代中進行更新,這會導致遍歷時訪問 value 時實際上都是訪問的同一個地址的值。循環體內編寫並行代碼容易踩坑。比如:
● Go1.22 之前版本的踩坑代碼
// Go1.22之前版本的踩坑代碼,Go1.22版本可以正常運行。
package main
import (
"fmt"
"sync"
)
func main() {
group := sync.WaitGroup{}
list := []string{"a", "b", "c", "d"}
for i, s := range list {
group.Add(1)
go func() {
defer group.Done()
fmt.Println(i, s) // 這裏訪問的都是同一個地址
}()
}
group.Wait()
}
Go1.22 之前版本的運行結果如下所示,不符合預期:
// Go1.22之前版本的運行結果,不符合預期
3 d
3 d
3 d
3 d
可以看到,上面代碼輸出都是同樣。爲了避免 Go1.22 之前版本的這個坑點,需要每次重新給 for 變量賦值,比如下面兩種解決方法:
● 方法一:
// Go1.22之前版本的解決方法一
package main
import (
"fmt"
"sync"
)
func main() {
group := sync.WaitGroup{}
list := []string{"a", "b", "c", "d"}
for i, s := range list {
group.Add(1)
i, s := i, s // 重新賦值
go func() {
defer group.Done()
fmt.Println(i, s)
}()
}
group.Wait()
}
● 方法二:
// Go1.22之前版本的解決方法二
package main
import (
"fmt"
"sync"
)
func main() {
group := sync.WaitGroup{}
list := []string{"a", "b", "c", "d"}
for i, s := range list {
group.Add(1)
go func(i int, s string) {
defer group.Done()
fmt.Println(i, s)
}(i, s) // 作爲參數傳入
}
group.Wait()
}
這兩種方式本質都是對 for 循環變量重新賦值。
而對於 Go1.22 及之後的版本,for 循環的每次迭代都會創建新變量,每次循環迭代各自的變量。從此再也不用擔心循環內併發導致的問題。對於最初的踩坑代碼,在 Go1.22 及之後版本運行的結果如下:
// Go1.22版本的運行結果
3 d
1 b
0 a
2 c
2. 支持對整數進行循環迭代
在 Go1.22 之前版本,for range 僅支持對 array、slice、string、map 、以及 channel 類型進行迭代。如果希望循環 N 次執行,可能需要如下編寫代碼:
for i := 0; i < 10; i++ {
// do something
}
Go1.22 及之後版本,新增了對整數類型的迭代。如下示例,代碼寫起來更簡潔。這裏需要注意的是 range 的範圍前閉後開 [0, N)。
for i := range 10 {
// do something
}
3. 支持函數類型範圍遍歷
在 for range 支持整型表達式的時候,Go 團隊也考慮了增加函數迭代器 (iterator),不過前者語義清晰,實現簡單。後者展現形式、語義和實現都非常複雜,於是在 Go1.22 中,函數迭代器以試驗特性提供,通過 GOEXPERIMENT=rangefunc 可以體驗該功能特性。而在最新的 Go 1.23 版本中,已經正式支持此功能。目前支持的函數類型爲:
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
在我們開發代碼過程中,經常會對自定義的 container 進行變量,比如下面的 Set:
// Set 保存一組元素。
type Set[E comparable] struct {
m map[E]struct{}
}
而在 Go1.22 之前的版本,我們要對 Set 中元素就行某種操作時,可能需要寫如下函數:
func (s *Set[E]) Handle(f func(E) bool) {
for v := range s.m {
if !f(v) {
return
}
}
}
當我們需要打印 Set 中所有元素時,可能需要如下調用 Handle 函數:
s := Set[int]{m: make(map[int]struct{})}
s.Handle(func(e int) bool {
fmt.Println(e)
return true
})
而在 Go1.22 打開實驗特性及 Go1.23 版本,可以支持如下寫法:
s := Set[int]{m: make(map[int]struct{})}
for e := range s.Handle {
fmt.Println(e)
}
從實現層面看,其實 range func 是一個語法糖。對於新版的 Go,for range 的迭代體在編譯的時候會被改寫成 func(e int) bool 形式,同時在函數的最後一行返回 true。
2.2 標準庫
1. 第一個 v2 標準庫: math/rand/v2
變動原因:
● 標準庫裏 math/rand 存在較多的問題,包括:生成器版本過舊、算法性能不高,以及與 crypto/rand.Read 存在衝突等問題;
● Go 1 要求保障兼容性,要解決上述問題無法直接對原庫進行變更,需要把標準庫升級到 v2 版本;
● 通過用 math/rand 試水,爲標準庫升級到 V2 積累經驗,例如:解決工具生態的問題 (gopls、goimports 等工具對 v2 包的支持), 後續再對風險更高的包(如:sync/v2 或 encoding/json/v2) 進行新版本迭代;
重要變更:
● 刪除 Rand.Read 和頂層的 Read: 原因是由於 math 庫和 crypto 的 Read 相近,導致本來該使用 crypto/rand.Read 的地方被誤用了 math/rand.Read,引入安全問題;
● 移除 Source.Seed、Rand.Seed 和頂層 Seed:它們假設底層隨機數生成器 (Source) 採用 int64 作爲種子,這個假設不具有普適性,不適合定義爲一個通用接口;
● 隨機數生成器接口增加 Uint64 方法,替換 Int63 方法,這個變更更符合新的隨機數生成器, 同時移除頂層 Source64 函數,原因是隨機數生成器提供了 Uint64 方法;
● Float32 和 Float64 使用了更直接的實現方式:以 Float64 爲例,之前版本的實現是 float64(r.Int63()) / (1<<63),它偶爾會出現四捨五入到 1.0 的問題,現在的實現改成了 float64(r.Int63n(1<<53)) / (1<<53) 來解決上面的問題;
● 使用 Rand.Shuffle 實現 Rand.Perm。Shuffle 實現效率更高,這樣可以確保只有一個實現;
● 將 Int31、Int31n、Int63、Int64n 函數更名爲 Int32、Int32n、Int64、Int64n;
● 新增 Uint32、Uint32N、Uint64、Uint64N、Uint、UintN 頂層函數以及 Rand 方法;
● 在 N、IntN、UintN 等中使用 Lemire 算法。初步基準測試顯示,與 v1 Int31n 相比節省了 40%,與 v1 Int63n 相比節省了 75%。
● 新增 PCG-DXSM(Permuted Congruential Generator) 和 ChaCha8 兩種隨機數生成器。刪除 Mitchell & Reeds LFSR 生成器。
示例:
// 列舉了部分math/rand庫的使用
package main
import (
"fmt"
"math/rand/v2"
"os"
"strings"
"text/tabwriter"
"time"
)
func main() {
// 創建並設置生成器的種子。
// 通常應使用非固定的種子,例如 Uint64(), Uint64()。
// 使用固定種子會在每次運行時產生相同的輸出。
r := rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64()))
// Float32 和 Float64 的值在 [0, 1) 範圍內。
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 和 Float64 的值在 [0, 1) 範圍內。
show("Float32", r.Float32(), r.Float32(), r.Float32())
show("Float64", r.Float64(), r.Float64(), r.Float64())
// ExpFloat64 值的平均值爲 1,但呈指數衰減。
show("ExpFloat64", r.ExpFloat64(), r.ExpFloat64(), r.ExpFloat64())
// NormFloat64 值的平均值爲 0,標準差爲 1。
show("NormFloat64", r.NormFloat64(), r.NormFloat64(), r.NormFloat64())
// Int32、Int64 和 Uint32 生成給定寬度的值。
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 和 Int64N 將它們的輸出限制爲 < n。
// 它們比使用 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 生成 [0, n) 範圍內的隨機排列。
show("Perm", r.Perm(5), r.Perm(5), r.Perm(5))
// 打印一個位於半開區間 [0, 100) 內的 int64。
fmt.Println("rand.N(): ", rand.N(int64(100)))
// 打印一個位於半開區間 [0, 100) 內的 uint32
fmt.Println("rand.N(): ", rand.N(uint32(100)))
// 睡眠一個在 0 到 100 毫秒之間的隨機時間。
time.Sleep(rand.N(100 * time.Millisecond))
// Shuffle 使用默認的隨機源對元素的順序進行僞隨機化
words := strings.Fields("ink runs from the corners of my mouth")
rand.Shuffle(len(words), func(i, j int) {
words[i], words[j] = words[j], words[i]
})
fmt.Println(words)
}
2. 增強 http.ServerMux 路由能力
現有的多路複用器(http.ServeMux)只能提供基本的路徑匹配,很多時候要藉助於第三方庫來完成實際需求的功能。Go 1.22 基於提案《net/http: enhanced ServeMux routing》,增強了 http.ServerMux 的路由匹配能力,增加了對模式匹配、路徑變量的支持。
●匹配方法
模式匹配將支持以 HTTP 方法開頭,後跟空格,如 GET /eddycjy 或 GET eddycjy.com/ 中。帶有方法的模式僅用於匹配具有該方法的請求。對照到代碼中,也就是 Go1.22 起,代碼可以這麼寫:
mux.HandleFunc("POST /eddycjy/create", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello world")
})
mux.HandleFunc("GET /eddycjy/update", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello world")
})
●通配符
模式匹配將支持 {name} 或 {name...},例如:/b/{bucket}/o/{objectname...}。該名稱必須是有效的 Go 標識符和符合完整路徑元素的標準。它們前面必須有斜槓,後面必須有斜槓或字符串末尾。例如:/b_{bucket} 不是有效的通配模式。Go1.22 起,http.ServeMux 可以這麼寫:
mux.HandleFunc("/items/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "id 值爲 %s", id)
})
mux.HandleFunc("/items/{path...}", func(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path")
fmt.Fprintf(w, "path 值爲 %s", path)
})
通常,通配符僅匹配單個路徑元素,以請求 URL 中的下一個斜槓結束。如果... 存在,則通配符與 URL 路徑的其餘部分匹配,包括斜槓。
○ /items/{id}: 正常情況下一個通配符只匹配一個路徑段,比如匹配 / items/123,但是不匹配 / items/123/456。
○ /items/{path...}: 但是如果通配符後面跟着...,那麼它就會匹配多個路徑段,比如 / items/123、/items/123/456 都會匹配這個模式。
○ /items/{$}: 正常情況下以 / 結尾的模式會匹配所有以它爲前綴的路徑,比如 / items/、/items/123、/items/123/456 都會匹配這個模式, 但是如果以 /{$} 爲後綴,那麼表示嚴格匹配路徑,不會匹配帶後綴的路徑,比如這個例子只會匹配 / items/,不會匹配 / items/123、/items/123/456。
●優先級
單一的優先規則:
○ 如果兩個模式重疊 (有一些共同的請求), 那麼更具體的模式優先:如果 P1 符合 P2 請求的一個(嚴格)子集,也就是說:如果 P2 符合 P1 的所有請求及更多請求,那麼 P1 就比 P2 更具體
○ 如果兩者都不更具體,那麼模式就會發生衝突。 這條規則有一個例外:如果兩個模式發生衝突,而其中一個有 HOST ,另一個沒有,那麼有 HOST 的模式優先
具體的例子:
-
example.com/ 比 / 更具體,因爲第一個僅匹配主機 example.com 的請求,而第二個匹配任何請求。
-
GET / 比 / 更具體,因爲第一個僅匹配 GET 和 HEAD 請求,而第二個匹配任何請求。
-
/b/{bucket}/o/default 比 /b/{bucket}/o/{noun} 更具體,因爲第一個僅匹配第四個元素是文字 “default” 的路徑,而在第二個中,第四個元素可以是任何內容。
3. 新增 go/version 庫
新增 go/version 庫用於識別、校驗 go version 的正確性,以及比較版本大小。功能比較簡單,可直接參考函數簽名:
// 返回 version x的go語言版本,比如x=go1.21rc2返回go版本爲go1.21
func Lang(x string) string
// 校驗版本的正確性
func IsValid(x string) bool
// 比較版本的大小: -1,0,1 分別代表 x < y, x == y, or x > y
// x,y必須是已go爲前綴,比如go1.22,不能使用1.22
func Compare(x, y string) int
4. 其他小變化
1.22 中還包含了許多小更新,此處列舉其中一些(小標題是庫名)
archive/tar、archive/zip
兩個庫都新增了 Writer.AddFS 函數,允許將 fs.FS 對象打包,內部會對 fs.FS 進行遍歷,打包時保持樹狀結構不變。
bufio
Scanner 的 SplitFunc 函數在返回 err 爲 ErrFinalToken 時,如果 token 爲 nil,之前會返回最後一個空的 token 再結束,現在會直接結束。如果需要返回空 token 再結束可以在返回 ErrFinalToken 時同時返回 []byte{} 數組,而不是 nil。舉個例子:
package main
import (
"bufio"
"bytes"
"fmt"
)
// 自定義的SplitFunc函數,按空格分割輸入,並在遇到"STOP"時停止掃描; 另外,爲了和標準庫函數對齊,採用了命名返回值
func splitBySpaceAndStop(data []byte, atEOF bool) (advance int, token []byte, err error) {
if i := bytes.IndexByte(data, ' '); i >= 0 {
// 找到空格,返回前面的數據作爲token
token = data[:i]
advance = i + 1
// 如果token是"STOP",返回ErrFinalToken
if string(token) == "STOP" {
return 0, nil, bufio.ErrFinalToken
}
return advance, token, nil
}
// 如果到達輸入末尾且有剩餘數據,返回剩餘數據作爲token
if atEOF && len(data) > 0 {
return len(data), data, nil
}
return 0, nil, nil
}
func main() {
input := "apple banana cherry STOP date"
scanner := bufio.NewScanner(bytes.NewReader([]byte(input)))
scanner.Split(splitBySpaceAndStop) // 設置自定義的SplitFunc
// 掃描並打印每個token
for scanner.Scan() {
fmt.Printf("Token: %s\n", scanner.Text())
}
// 1.22以前會打印以下4個token,其中第4個爲空字符串。而1.22則只會打印前3個token,如果也想打印4個token,則把return 0, nil, bufio.ErrFinalToken改爲return 0, []byte{}, bufio.ErrFinalToken即可
// Token: apple
// Token: banana
// Token: cherry
// Token:
}
cmp
新增了 Or 函數,入參是 comparable 類型的一組參數,返回第一個非零值,如果入參全部是零值則返回零值。這個函數可以簡化一些寫法,例如:
xxxConfigValue := cmp.Or(remoteConfig.Get("config_key"), "default_config_value")
Or 函數源碼如下:
// Or returns the first of its arguments that is not equal to the zero value.
// If no argument is non-zero, it returns the zero value.
func Or[T comparable](vals ...T) T {
var zero T
for _, val := range vals {
if val != zero {
return val
}
}
return zero
}
database/sql
在支持泛型之前,sql 庫定義了 NullInt64、NullBool、NullString 等結構體用於表示各種類型的 null 值。在泛型得到支持後,Null[T] 也就應運而生了,不過目前原有的各 NullXxx 結構體還沒有標爲 deprecated。
encoding
在 base32、base64、hex 包裏,原有的 Encode 和 Decode 函數在使用時需要提前初始化適當長度的 dst 數組,如下:
src := []byte("abc")
dst := make([]byte, base64.StdEncoding.EncodedLen(len(src)))
base64.StdEncoding.Encode(dst, src)
fmt.Printf("dst:%s\n", string(dst))
現在新增了 AppendEncode 和 AppendDecode 函數,可將 dst 追加到給定數組之後,給定數組無需提前初始化,也可以方便地進行拼接:
src := []byte("abc")
dst := base64.StdEncoding.AppendEncode([]byte{}, src)
fmt.Printf("dst:%s\n", string(dst))
go/types
新增了 Alias 類型,用於表示類型別名。之前類型別名並不會有單獨的類型,它本質上還是對應的原類型,只是代碼看起來會有些差異,在運行期類型別名信息其實是不存在的。新增了 Alias 類型後,在錯誤上報時,我們可以看到別名信息而不是原類型,有助於我們定位問題。
但 Alias 類型可能會破壞已有的類型判斷代碼,所以在 GODEBUG 環境變量下新增了一個參數 gotypesalias 用於開啓或關閉 Alias 類型。默認情況下是不開啓的,以兼容舊代碼。但在未來可能會默認爲開啓,所以我們已有的類型判斷相關代碼建議還是考慮到類型別名的情況,否則將來升級更新的 Go 版本可能會出現問題。舉個例子:
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
)
func main() {
// 要解析和檢查的Go代碼
src := `
package main
type intAlias = int
func main() {
var x intAlias
println(x)
}
`
// 創建文件集
fset := token.NewFileSet()
// 解析Go代碼
file, err := parser.ParseFile(fset, "example.go", src, 0)
if err != nil {
fmt.Println("解析錯誤:", err)
return
}
// 創建類型檢查器
conf := types.Config{Importer: nil}
info := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
}
// 進行類型檢查
_, err = conf.Check("example", fset, []*ast.File{file}, info)
if err != nil {
fmt.Println("類型檢查錯誤:", err)
return
}
// 打印變量x的類型
for ident, obj := range info.Defs {
if ident.Name == "x" {
// 如果環境變量配置爲GODEBUG=gotypesalias=0,則這裏打印的類型是int,和1.22以前的版本一樣,目前gotypesalias=0是默認值
// 如果環境變量配置爲GODEBUG=gotypesalias=1,則這裏打印的類型是example.intAlias,未來gotypesalias=1可能會變成默認值,因此判斷類型別名的類型時需要額外注意
fmt.Printf("變量x的類型是: %s\n", obj.Type())
}
}
}
net
Go DNS 解析器在使用 “-tags=netgo” 構建時,會先在 windows 的 hosts 文件中先查找是否匹配,即 %SystemRoot%\System32\drivers\etc\hosts 文件。
net/http
請求或響應頭中如果包含空的 Content-Length 頭,則 HTTP server 和 client 會拒絕請求或響應。GODEBUG 的 httplaxcontentlength 參數設置爲 1 可關閉這一特性,即不拒絕請求或響應。
net/netip
增加了 AddrPort.Compare 函數用於對比兩個 AddrPorts。
reflect
● Value.IsZero 方法現在對於浮點零和複數零都會返回 true,如果結構體的空字段(即_命名的字段)有非零值,Value.IsZero 方法也會返回 true。這些改動使得 IsZero 和 == 比較符在判零方面表現一致。
● PtrTo 函數標爲 deprecated,應該改用 PointerTo。
● 增加了新函數 TypeFor 用於獲取類型,之前獲取反射類型通常是 reflect.TypeOf((*T)(nil)).Elem(),比較麻煩,現在直接調用 TypeFor 就可以了,其實內部實現還是一樣的:
// TypeFor returns the [Type] that represents the type argument T.
func TypeFor[T any]() Type {
return TypeOf((*T)(nil)).Elem()
}
runtime/metrics
● 新增了四個關於 stop-the-world(下稱 stw)的 histogram metrics:
○ /sched/pauses/stopping/gc:seconds
○ /sched/pauses/stopping/other:seconds
○ /sched/pauses/total/gc:seconds
○ /sched/pauses/total/other:seconds
其中前兩個用於上報從決定 stw 到所有協程全部停止的時間,後兩個用於上報從決定 stw 到協程恢復運行的時間。
● /gc/pauses:seconds 標記爲 deprecated,被 / sched/pauses/total/gc:second 取代了。
● /sync/mutex/wait/total:seconds 之前僅包含 sync.Mutex 和 sync.RWMutex 的競爭時間,現在還包含了 runtime-internal 鎖的競爭時間。
runtime/pprof
● mutex 的 profile 現在會根據阻塞的協程數來計算競爭情況,更容易找出瓶頸 mutex 了。舉個例子,有 100 個協程在一個 mutex 上阻塞了 10 毫秒,那這個 mutex 的 profile 會顯示有 1s 延遲,以前是顯示 10 毫秒延遲。
● mutex 的 profile 除了包含 sync.Mutex 和 sync.RWMutex 的競爭時間,現在也會包含 runtime-internal 鎖的競爭時間。
● 在 Darwin 系統上,CPU 的 profile 現在會包含進程的內存映射,使得在 pprof 中可以啓用反彙編視圖。
runtime/trace
1.22 的 trace 改動很大,詳細內容可參考官方的 blog:More powerful Go execution traces
slices
● 新增了 Concat 函數,可將多個 slice 連接成一個。在此之前需要多次 append 實現同樣的效果,性能也差一些。
array1 := []int{1, 2, 3}
array2 := []int{4, 5, 6}
array3 := []int{7, 8, 9}
fmt.Println(append(append(array1, array2...), array3...)) // 1.22之前的寫法,要拼接的slice越多寫起來越麻煩,可能還需要for循環
fmt.Println(slices.Concat(array1, array2, array3)) // 1.22新增的Concat函數,使用起來更便利,性能也更好
● 會減少 slice 長度的函數會將新長度和原長度之間的元素值置爲零值,包括 Delete、DeleteFunc、Compact、CompactFunc、Replace,來看個例子:
array1 := []int{1, 2, 3}
array2 := slices.Delete(array1, 1, 2) // 移除[1, 2)區間的元素,即元素2,slice內還剩下元素1和3
fmt.Println(array1) // 以前是[1 3 3],現在是[1 3 0]
fmt.Println(array2) // 始終是[1 3]
● Insert 函數以前如果插入的位置超出 slice 範圍但沒有真的插入任何值的時候不會 panic,現在會 panic 了,也就是無論如何不能傳入越界的插入位置。
array := []int{1, 2, 3}
slices.Insert(array, 100, 3) // 向下標100的位置插入元素3,因爲越界,任何版本都會panic
slices.Insert(array, 100) // 向下標100的位置宣稱要插入但沒有傳待插入的元素參數。在1.22版本之前因爲沒有實際插入任何元素所以沒有問題,從1.22版本開始這樣做會被認爲是越界而導致panic
2.3 編譯器
Go 在 1.20 版本中,首次引入了 PGO。Go 1.22 對這一特性進行了優化,支持將更多的接口方法轉換爲直接調用,從而提高性能。大多數啓用 PGO 的程序將會有 2-14% 的性能提升。我們以一個示例說明優化算法原理:
type Animal interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
func main() {
var a Animal = Dog{}
fmt.Println(a.Speak())
}
在這個例子中,a.Speak() 是一個接口方法調用,Go 運行時需要查找 Dog 類型的 Speak 方法並調用它。通過開啓 PGO 編譯優化,編譯器可以在某些情況下將這種動態調用轉換爲直接調用,從而減少運行時的開銷,提高性能。
此外,1.22 版本還添加了一個編譯器的內聯算法優化,當前爲預覽版本,需要在編譯時通過 GOEXPERIMENT=newinliner 參數來啓用。內聯是編譯器優化的一種技術,它將函數調用展開爲函數體,從而消除函數調用的開銷,提高性能。具體優化點是,在重要調用點(比如循環場景)通過啓發式算法提高內聯能力,同時在不重要(比如,panic 調用鏈)的調用點上不阻止內聯。
2.4 鏈接器
Go 在 1.22 中的改進點主要如下:
1. 改進 -s 和 -w 標誌:
● -w 標誌:抑制 DWARF 調試信息的生成。
● -s 標誌:抑制符號表的生成,並且隱含 -w 標誌(即同時抑制 DWARF 調試信息的生成)。
● -w=0:可以用來取消 -s 標誌隱含的 -w 標誌。例如,-s -w=0 將生成一個包含 DWARF 調試信息但沒有符號表的二進制文件。
2. 新增 ELF 平臺上的 -B 標誌:
-B gobuildid:鏈接器將生成一個基於 Go build ID 的 GNU build ID(ELF NT_GNU_BUILD_ID 註釋)。這意味着最終的 ELF 文件將包含一個基於 Go build ID 的 GNU build ID(存儲在 ELF 文件的 .note.gnu.build-id 部分中)。
3. 改進 Windows 平臺上的 -linkmode=internal 標誌:
當使用 -linkmode=internal 構建時,鏈接器現在會保留 C 對象文件中的 SEH(結構化異常處理)信息,通過將 .pdata 和 .xdata 部分複製到最終的二進制文件中。這有助於使用本地工具(如 WinDbg)進行調試和分析。
需要留意的是,在 Go1.22 以前,C 函數的 SEH 異常不會被 go 程序處理。所以,升級新版本後可能因爲錯誤被正確處理而導致程序出現一些與之前不同的行爲。
總結來說,這些改進使得鏈接器在不同平臺上的行爲更加一致,並且在 Windows 平臺上改進了程序調試和分析能力。
2.5 工具鏈
1.Go command
Go 1.22 版本對 Go command 工具鏈進行了一些改造升級,包括 go work vendor、go mod init、go test 覆蓋率彙總等,總體上通過 go work vendor 功能完善了對 go work 的支持,而 go mod init 與 go get 等工具上的調整則逐漸停止了對 GOPATH 模式的支持,符合工具的迭代變化演進規律。
● 新增 go work vendor 功能
我們都知道 go module 是 Go 1.11 版本之後官方推出的版本管理工具,並且從 Go 1.13 版本開始,go module 是 Go 語言默認的依賴管理工具,但是當本地有很多 module,且這些 module 存在相互依賴,本地開發就會存在諸多不便,因此 Go 1.18 引入了 workspace 工作區模式,可以讓開發者不用修改 go module 的 go.mod,就能同時對多個有依賴的 go module 進行本地開發。不過當時可能爲了簡化 workspace 模式的實現,並沒有支持 vendor 模式,而 vendor 模式對於需要依賴隔離、離線部署、可重複等場景還是很有必要。
因而 Go 1.22 版本,支持了 go work vendor 功能,可以將 workspace 模式中的依賴放到 vendor ⽬錄下,使用方法上與 go mod vendor 一致,只是將 vendor 能力支持到了 workspace 工作區模式下,詳情可通過 go help work vendor 查看幫忙文檔。
● go test 調整了單測覆蓋率摘要展示
Go 1.22 版本之前,當某個包下沒有單測時,顯示如下:
? mymod/mypack [no test files]
Go 1.22 版本調整了單測覆蓋率摘要展示,當某個包下沒有單測時,會被當作是無覆蓋:
mymod/mypack coverage: 0.0% of statements
● 其它一些變化
在傳統的 GOPATH 模式下(即 GO111MODULE=off 時),不再支持在 Go Module 之外使用 go get。不過其它的構建編譯命令,例如:go build 和 go test,則可以繼續適用於傳統的 GOPATH 程序。
此外,初始化命令 go mod init 將不再嘗試從其他依賴工具(如 Gopkg.lock)的配置文件中導入模塊依賴,從中也可以看出,GOPATH 時代的工具正逐漸退出併成爲歷史。
2.Trace
Go 1.5 版本推出了 trace 工具,用於支持對整個週期內發生的事件進行性能分析,在之後的幾個版本中逐漸完善成熟,不過也一直存在追蹤的開銷很大及追蹤的擴展性不強等問題。
Go 1.21 版本 runtime/trace 基於 Frame Pointer Unwinding 技術大幅降低了收集運行時 trace 的 CPU 開銷,需要注意的是,目前該項技術僅在 GOARCH 架構爲 amd64 和 arm64 時生效,在 1.21 版本之前,許多應用開銷大約在 10-20% 的 CPU 之間,而該版本只需要 1-2%。
得益於 1.21 版本中 runtime/trace 收集效率的大幅改進,Go 1.22 版本中的 trace 工具也進行了適應性地調整以支持新的 trace 收集工具,解決了一些問題並且提高了各個子頁面的可讀性,以進一步提升開發者們基於 trace 定位問題的效率與便捷性,如下基於 trace 報告中的 goroutines 追蹤頁面可以感受到可讀性上的變化:
舊頁面:
新頁面:
此外 Web UI 新增支持了面向線程的 trace 追蹤展示:
隨着 trace 收集開銷的大幅降低,trace 工具的應用場景可能也會越來越廣,催生更多的生態工具,例如 Flight recording,值得有興趣的同學進一步關注。
3.Vet
go vet 工具是 Go 代碼靜態診斷器,可以用於檢查代碼中可能存在的各種問題,例如無法訪問的代碼、錯誤的鎖使用、不必要的賦值、布爾運算錯誤等。工具檢查選擇的規則集主要考量正確性、頻率和準確性等:
● 正確性:工具定位於檢查代碼潛在的編譯或執行問題,而不是代碼風格問題
● 頻率:工具定位於希望開發人員頻繁地執行,因此納入的檢查規則是需要能真正發現問題的規則
● 準確性:工具定位於極高的準確性,儘可能地不漏報也不錯報,因此納入的檢查規則需要極高的準確性
因此,建議未使用的開發人員安排上該工具,納入日常 CI 流程或者 precommit 時進行檢查,詳情可以通過 go tool vet help 查看已註冊的檢查器及其用法
Go1.22 版本的 go vet 工具,總的來說延續既有風格,並未做出特別大的調整,主要是基於 Go1.22 自身語言的一些變化,適應性地調整了對相應問題的檢查:
● 增加了對 slice 進行 append 操作卻未追加額外值時的告警
示例:
// 錯誤代碼
func testAppend() {
var s []string
s = append(s)
}
當使用 go vet 檢查時,將會報告類似如下告警:
# [command-line-arguments]
./main.go:3:6: append with no values
● 增加了當對 time.Since 進行錯誤 defer 時的告警
示例:
func testTimeSince() {
t := time.Now()
defer log.Println(time.Since(t)) // non-deferred call to time.Since
tmp := time.Since(t)
defer log.Println(tmp) // equivalent to the previous defer
defer func() {
log.Println(time.Since(t)) // a correctly deferred call to time.Since
}()
}
當使用 go vet 檢查時,將會報告類似如下告警:
# [command-line-arguments]
./main.go:3:20: call to time.Since is not deferred
● 增加了當使用 slog 打印日誌卻未正確傳入鍵值對時的告警
func testSlog() {
slog.Info("hello", 3, 3)
}
當使用 go vet 檢查時,將會報告類似如下告警:
./main.go:2:21: slog.Info arg "3" should be a string or a slog.Attr (possible missing key or value)
2.6 運行時
運行時的變化主要在 GC 算法。在 Go 1.22 中,運行時會將基於類型的垃圾回收元數據保持在每個堆對象附近,從而可以將 Go 程序的 CPU 性能提升 1~3%。此外,通過減少重複的元數據信息,大多數程序的內存開銷也會下降約 1%。
這一改進涉及到對象地址對齊問題,以前部分對象的地址是按 16(甚至更大)字節邊界對齊,現在只與 8 字節對齊。內存的分配更爲精細化,大多數應用程序的內存開銷也因此而變小。但是,如果某些對象的大小剛好在新的大小類邊界上,那麼這些對象可能會被移動到更大的內存塊中,這些對象可能會佔用更多的內存。由於這種應用程序佔比較少,總體而言,內存開銷是有小幅優化下降的。
此外,一些使用匯編指令的程序,由於要求內存地址對齊超過 8 字節,且依賴之前內存分配器的地址對齊功能,可能會在這個版本遇到兼容性問題。Go 團隊給出了構建參數 GOEXPERIMENT=noallocheaders ,通過這個參數可以關閉新變化,回退到之前版本的對齊特性。這個參數僅用於臨時過渡,後續 Go 版本會將其移除,因此如果有依賴的話需要儘快兼容適配。
三、小結
Go 1.22 版本對語言、編譯器、工具連、運行時、標準庫都有一定程度的優化。既有代碼通過本版本重新編譯後性能上能有一定的提升。8 月份 “穩重求變” 的 Go 語言如期發佈了 1.23 版本,相關的新特性報告敬請期待。
參考資料
Profile-guided optimization - The Go Programming Language
math/rand/v2: revised API for math/rand · Issue #61716 · golang/go · GitHub
More powerful Go execution traces
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2poQ_ZUi5zcP4xmOTxqjNg