Go 1.19 相比 Go 1.18 有哪些值得注意的改動(dòng)?
Go 1.19 值得關(guān)注的改動(dòng):
- 內(nèi)存模型與原子操作 : Go 的 內(nèi)存模型(memory model) 已更新,與 C、C++、Java 等語(yǔ)言的 模型 對(duì)齊,以確保持續(xù)一致性。同時(shí),
sync/atomic
包引入了新的 原子類型(atomic types),如atomic.Int64
和atomic.Pointer[T]
,簡(jiǎn)化了原子操作并提高了類型安全性。 go
命令 : 改進(jìn)了構(gòu)建信息的包含(-trimpath
)、go generate
/test
的環(huán)境一致性、go env
的輸出處理,并增強(qiáng)了go list -json
的靈活性和性能,同時(shí)緩存了部分模塊加載信息以加速go list
。vet
工具 : 新增檢查,用于發(fā)現(xiàn)errors.As
的第二個(gè)參數(shù)誤用*error
類型的常見(jiàn)錯(cuò)誤。- Runtime : 引入了 軟內(nèi)存限制(soft memory limit),可通過(guò)
GOMEMLIMIT
環(huán)境變量或runtime/debug.SetMemoryLimit
函數(shù)進(jìn)行設(shè)置,允許程序在接近內(nèi)存上限時(shí)更有效地利用資源,并與GOGC
協(xié)同工作。 - 編譯器、匯編器與鏈接器 : 編譯器通過(guò) 跳轉(zhuǎn)表(jump table) 優(yōu)化了大型
switch
語(yǔ)句(在amd64
和arm64
架構(gòu)下提速約 20%);編譯器和匯編器現(xiàn)在強(qiáng)制要求-p=importpath
標(biāo)志來(lái)構(gòu)建可鏈接的對(duì)象文件;鏈接器在 ELF 平臺(tái)使用標(biāo)準(zhǔn)的壓縮 DWARF 格式。
下面是一些值得展開(kāi)的討論:
Go 1.19 修訂內(nèi)存模型并引入新的原子類型
Go 1.19 對(duì)其內(nèi)存模型進(jìn)行了修訂,主要目標(biāo)是與 C, C++, Java, JavaScript, Rust, 和 Swift 等主流語(yǔ)言使用的內(nèi)存模型保持一致。需要注意的是,Go 僅提供 順序一致(sequentially consistent) 的原子操作,而不支持其他語(yǔ)言中可能存在的更寬松的內(nèi)存排序形式。
伴隨著內(nèi)存模型的更新,sync/atomic
包引入了一系列新的原子類型,包括 atomic.Bool
, atomic.Int32
, atomic.Int64
, atomic.Uint32
, atomic.Uint64
, atomic.Uintptr
, 和 atomic.Pointer[T]
。
這些新類型的主要優(yōu)勢(shì)在于:
- 類型安全 :它們封裝了底層的值,強(qiáng)制所有訪問(wèn)都必須通過(guò)原子 API 進(jìn)行,避免了意外的非原子讀寫操作。
- 簡(jiǎn)化指針操作 :
atomic.Pointer[T]
泛型類型避免了在調(diào)用點(diǎn)將指針轉(zhuǎn)換為unsafe.Pointer
的需要,使得代碼更清晰、更安全。 - 自動(dòng)對(duì)齊 :
atomic.Int64
和atomic.Uint64
類型在結(jié)構(gòu)體中或分配內(nèi)存時(shí),即使在 32 位系統(tǒng)上也會(huì)自動(dòng)保證 64 位對(duì)齊。這對(duì)于保證原子操作的正確性至關(guān)重要。
舊方式(Go 1.18 及之前)
通常需要直接使用 sync/atomic
包提供的函數(shù),并對(duì)基本類型進(jìn)行操作。例如,對(duì)一個(gè)共享的 int64
變量進(jìn)行原子更新:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
// 啟動(dòng)多個(gè) goroutine 并發(fā)增加 counter
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 使用 atomic 函數(shù)進(jìn)行原子增操作
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
// 使用 atomic 函數(shù)進(jìn)行原子讀操作
finalValue := atomic.LoadInt64(&counter)
fmt.Println("Final counter:", finalValue) // 輸出: Final counter: 100
}
對(duì)于指針類型,之前可能需要 atomic.Value
或者結(jié)合 unsafe.Pointer
使用 atomic.LoadPointer
/StorePointer
。
新方式(Go 1.19 及之后)
使用新的原子類型,代碼更簡(jiǎn)潔,類型約束更強(qiáng)。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
// 使用新的 atomic.Int64 類型
var counter atomic.Int64
var wg sync.WaitGroup
// 啟動(dòng)多個(gè) goroutine 并發(fā)增加 counter
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 直接調(diào)用類型的方法進(jìn)行原子增操作
counter.Add(1)
}()
}
wg.Wait()
// 直接調(diào)用類型的方法進(jìn)行原子讀操作
finalValue := counter.Load()
fmt.Println("Final counter:", finalValue) // 輸出: Final counter: 100
}
對(duì)于指針,atomic.Pointer[T]
提供了類型安全的原子讀寫能力:
package main
import (
"fmt"
"sync/atomic"
)
type Config struct {
Version string
Data map[string]string
}
func main() {
var currentConfig atomic.Pointer[Config]
// 初始化配置
initialConfig := &Config{Version: "v1", Data: map[string]string{"key": "value1"}}
currentConfig.Store(initialConfig)
// 讀取配置
cfg1 := currentConfig.Load()
fmt.Printf("Config v%s: %v\n", cfg1.Version, cfg1.Data)
// 原子地更新配置
newConfig := &Config{Version: "v2", Data: map[string]string{"key": "value2", "new": "added"}}
currentConfig.Store(newConfig)
cfg2 := currentConfig.Load()
fmt.Printf("Config v%s: %v\n", cfg2.Version, cfg2.Data)
}
Config vv1: map[key:value1]
Config vv2: map[key:value2 new:added]
這種方式相比舊的 atomic.Value
或 unsafe.Pointer
操作,類型更安全,意圖更明確。
go
命令的改進(jìn)
Go 1.19 對(duì) go
命令進(jìn)行了多項(xiàng)增強(qiáng)和調(diào)整,主要涉及構(gòu)建過(guò)程、環(huán)境設(shè)置、信息查詢等方面。
-trimpath
標(biāo)志信息嵌入 :如果在go build
時(shí)設(shè)置了-trimpath
標(biāo)志(用于從編譯出的二進(jìn)制文件中移除本地構(gòu)建路徑信息),這個(gè)設(shè)置現(xiàn)在會(huì)被記錄在二進(jìn)制文件的構(gòu)建信息中。可以通過(guò)go version -m <binary>
或debug.ReadBuildInfo
來(lái)查看。
# 編譯時(shí)加入 -trimpath
go build -trimpath -o myapp main.go
# 查看二進(jìn)制文件的構(gòu)建信息
go version -m ./myapp
# 輸出會(huì)包含類似 build -trimpath=true 的信息
go generate
明確設(shè)置GOROOT
:現(xiàn)在go generate
會(huì)在其執(zhí)行子進(jìn)程的環(huán)境變量中明確設(shè)置GOROOT
。這確保了即使代碼生成器本身是使用-trimpath
構(gòu)建的(可能導(dǎo)致其無(wú)法自動(dòng)找到GOROOT
),它也能定位到正確的 Go 安裝根目錄。go test
和go generate
的PATH
調(diào)整 :這兩個(gè)命令現(xiàn)在會(huì)將$GOROOT/bin
放在子進(jìn)程PATH
環(huán)境變量的開(kāi)頭。這樣做可以保證當(dāng)測(cè)試代碼或代碼生成器需要執(zhí)行go
命令時(shí),它們會(huì)調(diào)用到與父進(jìn)程相同版本的go
工具鏈,避免潛在的版本沖突。go env
輸出的空格處理 :對(duì)于包含空格的環(huán)境變量值(如CGO_CFLAGS
,CGO_CPPFLAGS
,CGO_CXXFLAGS
,CGO_FFLAGS
,CGO_LDFLAGS
,GOGCCFLAGS
),go env
現(xiàn)在會(huì)在輸出時(shí)給它們加上引號(hào),使得輸出結(jié)果更易于被腳本等其他工具解析。go list -json
支持字段選擇 :go list -json
命令現(xiàn)在可以接受一個(gè)逗號(hào)分隔的字段列表,用于指定需要輸出的 JSON 字段。如果指定了列表,go list
只會(huì)計(jì)算和輸出這些字段,這在某些情況下可以顯著提高性能,因?yàn)樗苊饬擞?jì)算不需要的字段。這也可能抑制某些只在計(jì)算未請(qǐng)求字段時(shí)才會(huì)出現(xiàn)的錯(cuò)誤。
# 僅獲取當(dāng)前模塊的導(dǎo)入路徑和直接依賴
go list -json=ImportPath,Deps .
go list
模塊加載緩存 :go
命令現(xiàn)在會(huì)緩存加載某些模塊所需的信息,這有望加速部分go list
命令的調(diào)用。
vet
工具新增 errors.As
使用檢查
vet
工具的 errorsas
檢查器現(xiàn)在增加了一項(xiàng)功能:檢測(cè) errors.As
函數(shù)的第二個(gè)參數(shù)是否被錯(cuò)誤地傳遞了 *error
類型。這是一個(gè)常見(jiàn)的錯(cuò)誤。
errors.As
函數(shù)用于檢查錯(cuò)誤鏈中是否存在特定類型的錯(cuò)誤,并將其賦值給一個(gè)變量。其簽名如下:
func As(err error, target interface{}) bool
正確的用法是,target
參數(shù)必須是一個(gè)指向 具體錯(cuò)誤類型 的指針,或者是一個(gè)指向?qū)崿F(xiàn)了 error
接口的任意類型的指針。errors.As
會(huì)遍歷 err
的錯(cuò)誤鏈,如果找到一個(gè)錯(cuò)誤可以賦值給 target
指向的變量,就進(jìn)行賦值并返回 true
。
常見(jiàn)的錯(cuò)誤用法
開(kāi)發(fā)者有時(shí)可能會(huì)錯(cuò)誤地傳遞一個(gè) *error
(指向 error
接口的指針)給 target
:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// 示例錯(cuò)誤,包裝了 *os.PathError
err := fmt.Errorf("wrapping a file error: %w", &os.PathError{Op: "open", Path: "/no/such/file", Err: errors.New("file not found")})
var genericErr error // 聲明一個(gè) error 接口類型的變量
// 錯(cuò)誤用法:將 *error 類型的指針傳遞給 errors.As
// 這幾乎永遠(yuǎn)不是你想要的,因?yàn)樗粫?huì)檢查錯(cuò)誤鏈中是否有某個(gè)值可以賦給一個(gè) error 接口
// 這通常沒(méi)有意義,因?yàn)殒溨械娜魏五e(cuò)誤都可以賦值給 error 接口
if errors.As(err, &genericErr) {
// 這段代碼幾乎總會(huì)執(zhí)行 (只要 err != nil),但 genericErr 會(huì)被賦值為鏈中的第一個(gè)錯(cuò)誤
// 這并不是 errors.As 的設(shè)計(jì)意圖
fmt.Printf("Found an error (incorrectly): %v\n", genericErr)
} else {
fmt.Println("No error found (incorrectly)")
}
}
piperliu@go-x86:~/code/playground$ go run main.go
Found an error (incorrectly): wrapping a file error: open /no/such/file: file not found
piperliu@go-x86:~/code/playground$ go vet main.go
# command-line-arguments
./main.go:17:8: second argument to errors.As should not be *error
Go 1.19 的 vet
會(huì)對(duì)上述錯(cuò)誤用法發(fā)出警告。
正確的用法
應(yīng)該傳遞一個(gè)指向 具體錯(cuò)誤類型 變量的指針。
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// 示例錯(cuò)誤,包裝了 *os.PathError
err := fmt.Errorf("wrapping a file error: %w", &os.PathError{Op: "open", Path: "/no/such/file", Err: errors.New("file not found")})
var pathErr *os.PathError // 聲明一個(gè)具體錯(cuò)誤類型的指針變量
// 正確用法:傳遞 *os.PathError 類型的指針
if errors.As(err, &pathErr) {
// 如果錯(cuò)誤鏈中存在 *os.PathError 類型的錯(cuò)誤,pathErr 會(huì)被賦值
fmt.Printf("Successfully found PathError: Op=%s, Path=%s\n", pathErr.Op, pathErr.Path)
} else {
fmt.Println("PathError not found in chain")
}
}
go run main.go
Successfully found PathError: Op=open, Path=/no/such/file
piperliu@go-x86:~/code/playground$ go vet main.go
這個(gè)新的 vet
檢查有助于開(kāi)發(fā)者及早發(fā)現(xiàn)并修正這種對(duì) errors.As
的誤用。
Runtime 的軟內(nèi)存限制及其他改進(jìn)
Go 1.19 在 Runtime 層面引入了重要的 軟內(nèi)存限制(soft memory limit) 功能,并包含其他多項(xiàng)優(yōu)化。
軟內(nèi)存限制
- 目的 :提供一種機(jī)制來(lái)限制 Go 程序使用的總內(nèi)存量,以提高在容器化環(huán)境等資源受限場(chǎng)景下的資源利用率。
- 范圍 :這個(gè)限制覆蓋 Go 堆(heap)以及所有由 Runtime 管理的內(nèi)存(例如 goroutine 棧、GC 元數(shù)據(jù)等)。它 不包括 程序二進(jìn)制文件本身的內(nèi)存映射、其他語(yǔ)言(如 C)管理的內(nèi)存,以及操作系統(tǒng)為 Go 程序保留的內(nèi)存(如某些內(nèi)核緩沖區(qū))。
- 配置 :可以通過(guò)設(shè)置
GOMEMLIMIT
環(huán)境變量(例如GOMEMLIMIT=1024MiB
)或在代碼中調(diào)用runtime/debug.SetMemoryLimit(limit int64)
來(lái)管理。 - 與
GOGC
的關(guān)系 :軟內(nèi)存限制與GOGC
(或runtime/debug.SetGCPercent
)協(xié)同工作。即使設(shè)置GOGC=off
,只要設(shè)置了GOMEMLIMIT
,Runtime 仍然會(huì)嘗試遵守這個(gè)內(nèi)存限制,通過(guò)更頻繁地觸發(fā) GC 來(lái)控制內(nèi)存增長(zhǎng)。這使得程序能夠始終最大限度地利用其被分配的內(nèi)存限額。 - GC CPU 限制器 :當(dāng)程序的活動(dòng)堆大小接近軟內(nèi)存限制時(shí),為了防止 GC 過(guò)于頻繁(稱為 GC 抖動(dòng)/thrashing)而嚴(yán)重影響程序性能,Runtime 會(huì)嘗試將 GC 的 CPU 利用率限制在 50% 以內(nèi)(不包括空閑時(shí)間)。這意味著 Runtime 寧愿稍微超出內(nèi)存限制,也不愿完全阻止應(yīng)用程序的進(jìn)展??梢酝ㄟ^(guò)新的 運(yùn)行指標(biāo)(runtime metric)
/gc/limiter/last-enabled:gc-cycle
查看該限制器最后一次生效的 GC 周期。 - 穩(wěn)定性與限制 :對(duì)于較大的內(nèi)存限制(數(shù)百 MB 或更多),該功能是穩(wěn)定且生產(chǎn)就緒的。但對(duì)于非常小的限制(幾十 MB 或更少),由于外部延遲因素(如操作系統(tǒng)調(diào)度)的影響,限制可能不那么精確(詳見(jiàn) issue 52433)。
其他 Runtime 改進(jìn)
- 空閑狀態(tài)下的 GC 工作線程 :當(dāng)應(yīng)用程序足夠空閑以至于觸發(fā)周期性 GC 時(shí),Runtime 現(xiàn)在會(huì)調(diào)度更少的 GC 工作 goroutine 在空閑的操作系統(tǒng)線程上運(yùn)行,以減少不必要的資源消耗。
- 初始 Goroutine 棧大小 :Runtime 現(xiàn)在會(huì)根據(jù) goroutine 歷史平均棧使用量來(lái)分配初始棧大小。這旨在減少平均情況下早期棧增長(zhǎng)和復(fù)制的開(kāi)銷,代價(jià)是對(duì)于棧使用量遠(yuǎn)低于平均值的 goroutine 可能會(huì)浪費(fèi)最多 2 倍的空間。
- Unix 文件描述符限制(RLIMIT_NOFILE) :在 Unix 操作系統(tǒng)上,導(dǎo)入了
os
包的 Go 程序現(xiàn)在會(huì)自動(dòng)將進(jìn)程的打開(kāi)文件描述符軟限制(soft limit)提高到硬限制(hard limit)允許的最大值。這是為了解決某些系統(tǒng)上為了兼容舊的 C 程序(使用select
系統(tǒng)調(diào)用)而設(shè)置的過(guò)低的人為限制。Go 程序(尤其是并發(fā)處理大量文件時(shí),如gofmt
)經(jīng)常因此耗盡文件描述符。此更改的一個(gè)潛在影響是,如果 Go 程序再啟動(dòng)舊的 C 程序作為子進(jìn)程,這些子進(jìn)程可能會(huì)以過(guò)高的文件描述符限制運(yùn)行。這可以通過(guò)在啟動(dòng) Go 程序之前設(shè)置較低的硬限制來(lái)解決。 - 簡(jiǎn)化不可恢復(fù)錯(cuò)誤的回溯信息 :對(duì)于不可恢復(fù)的致命錯(cuò)誤(如并發(fā) map 寫入、解鎖未鎖定的互斥鎖),現(xiàn)在默認(rèn)打印更簡(jiǎn)潔的回溯信息,不包含 Runtime 的元數(shù)據(jù)(類似于
panic
的致命錯(cuò)誤)。除非設(shè)置了GOTRACEBACK=system
或crash
,才會(huì)打印包含完整元數(shù)據(jù)的詳細(xì)回溯信息。Runtime 內(nèi)部的致命錯(cuò)誤總是包含完整元數(shù)據(jù)。 - 調(diào)試器注入函數(shù)調(diào)用(ARM64) :在 ARM64 架構(gòu)上增加了對(duì)調(diào)試器注入函數(shù)調(diào)用的支持。這使得開(kāi)發(fā)者在使用支持此功能的更新版調(diào)試器時(shí),可以在交互式調(diào)試會(huì)話中調(diào)用程序中的函數(shù)。
- 地址消毒器(Address Sanitizer)改進(jìn) :Go 1.18 中引入的 地址消毒器(address sanitizer) 支持現(xiàn)在能更精確地處理函數(shù)參數(shù)和全局變量。
編譯器、匯編器與鏈接器的更新
Go 1.19 在構(gòu)建工具鏈的底層組件方面也有一些重要的變化。
編譯器 (Compiler)
- 大型
switch
語(yǔ)句優(yōu)化 :編譯器現(xiàn)在使用 跳轉(zhuǎn)表(jump table) 來(lái)實(shí)現(xiàn)包含大量 case 的整數(shù)和字符串switch
語(yǔ)句。這可以帶來(lái)顯著的性能提升,根據(jù)具體情況,速度可能提高約 20%。此優(yōu)化目前僅適用于GOARCH=amd64
和GOARCH=arm64
架構(gòu)。
例如,一個(gè)有許多字符串 case 的 switch
:
func handleCommand(cmd string) {
switch cmd {
case "START":
// ...
case "STOP":
// ...
case "RESTART":
// ...
// ... 很多其他 case ...
case "STATUS":
// ...
default:
// ...
}
}
在 Go 1.19 中,如果這個(gè) switch
足夠大,編譯器(在支持的架構(gòu)上)會(huì)生成更高效的跳轉(zhuǎn)表代碼,而不是一系列的比較和跳轉(zhuǎn)。
- 強(qiáng)制要求
-p=importpath
標(biāo)志 :Go 編譯器現(xiàn)在要求必須提供-p=importpath
標(biāo)志才能構(gòu)建一個(gè)可鏈接的對(duì)象文件 (.o
文件)。go build
命令和 Bazel 構(gòu)建系統(tǒng)已經(jīng)會(huì)自動(dòng)提供這個(gè)標(biāo)志。如果你有自定義的構(gòu)建系統(tǒng)直接調(diào)用 Go 編譯器(compile
),你需要確保傳遞了這個(gè)標(biāo)志。importpath
通常是包的導(dǎo)入路徑。 - 移除
-importmap
標(biāo)志 :Go 編譯器不再接受-importmap
標(biāo)志。直接調(diào)用編譯器的構(gòu)建系統(tǒng)必須改為使用-importcfg
標(biāo)志來(lái)提供導(dǎo)入路徑到實(shí)際文件路徑的映射。go build
會(huì)自動(dòng)處理這個(gè)。
匯編器 (Assembler)
- 強(qiáng)制要求
-p=importpath
標(biāo)志 :與編譯器類似,Go 匯編器(asm
)現(xiàn)在也要求必須提供-p=importpath
標(biāo)志才能構(gòu)建可鏈接的對(duì)象文件。同樣,go build
會(huì)處理好,但直接調(diào)用匯編器的系統(tǒng)需要自行添加。
鏈接器 (Linker)
- ELF 平臺(tái)使用標(biāo)準(zhǔn)壓縮 DWARF 格式 :在 ELF 格式的目標(biāo)平臺(tái)(如 Linux)上,鏈接器現(xiàn)在默認(rèn)使用標(biāo)準(zhǔn)的 gABI 格式(
SHF_COMPRESSED
)來(lái)壓縮 DWARF 調(diào)試信息段,取代了之前使用的非標(biāo)準(zhǔn)的.zdebug
格式。這有助于提高與其他工具鏈(如 GDB、objdump 等)的兼容性。
這些改變主要影響性能(switch
優(yōu)化)、構(gòu)建系統(tǒng)的維護(hù)者(需要調(diào)整對(duì)編譯器/匯編器的直接調(diào)用)以及調(diào)試信息格式的標(biāo)準(zhǔn)化。對(duì)于大多數(shù)使用 go build
的開(kāi)發(fā)者來(lái)說(shuō),后兩項(xiàng)更改是透明的。