Go 1.13 相比 Go 1.12 有哪些值得注意的改動(dòng)?
https://go.dev/doc/go1.13
Go 1.13 帶來了一系列語言、工具鏈、運(yùn)行時(shí)和標(biāo)準(zhǔn)庫(kù)的改進(jìn)。以下是一些值得開發(fā)者關(guān)注的重點(diǎn)改動(dòng):
- 語言特性 : 引入了更統(tǒng)一和現(xiàn)代化的數(shù)字字面量表示法,包括二進(jìn)制 (
0b
)、八進(jìn)制 (0o
) 前綴、十六進(jìn)制浮點(diǎn)數(shù)、數(shù)字分隔符 (_
) 等,并取消了移位操作計(jì)數(shù)必須為無符號(hào)數(shù)的限制。 - Go Modules 與 Go 命令 :
GO111MODULE=auto
在檢測(cè)到go.mod
文件時(shí)將默認(rèn)啟用模塊感知模式,即使在GOPATH
內(nèi);引入GOPRIVATE
等環(huán)境變量更好地管理私有模塊和代理配置;go get -u
的更新邏輯有所調(diào)整;go
命令增加了如go env -w
、go version <executable>
、go build -trimpath
等新功能。 - Runtime 運(yùn)行時(shí) : 優(yōu)化了切片越界時(shí)的 panic 信息,使其包含越界索引和切片長(zhǎng)度;
defer
的性能在大多數(shù)場(chǎng)景下提升了約 30%;運(yùn)行時(shí)會(huì)更積極地將不再使用的內(nèi)存歸還給操作系統(tǒng)。 - 錯(cuò)誤處理 : 正式引入了 錯(cuò)誤包裝(error wrapping)機(jī)制,通過
fmt.Errorf
的新%w
動(dòng)詞和errors
包新增的Unwrap
、Is
、As
函數(shù),可以創(chuàng)建和檢查包含原始錯(cuò)誤上下文的錯(cuò)誤鏈。 sync
包 : 通過內(nèi)聯(lián)優(yōu)化,sync.Mutex
、sync.RWMutex
和sync.Once
在非競(jìng)爭(zhēng)情況下的性能得到提升(鎖操作約 10%,Once.Do
約 2 倍);sync.Pool
對(duì) GC 暫停時(shí)間(STW)的影響減小,并且能在 GC 后保留部分對(duì)象,減少 GC 后的冷啟動(dòng)開銷。
下面是一些值得展開的討論:
語言特性:更現(xiàn)代化的數(shù)字字面量與有符號(hào)位移
Go 1.13 在語言層面引入了幾項(xiàng)旨在提升代碼可讀性和易用性的改進(jìn)。
首先是數(shù)字字面量的增強(qiáng):
- 二進(jìn)制字面量 (Binary Literals) : 使用前綴
0b
或0B
表示二進(jìn)制整數(shù),例如0b1011
代表十進(jìn)制的 11。 - 八進(jìn)制字面量 (Octal Literals) : 使用前綴
0o
或0O
表示八進(jìn)制整數(shù),例如0o660
代表十進(jìn)制的 432。需要注意的是,舊式的以0
開頭的八進(jìn)制表示法(如0660
)仍然有效,但推薦使用新的0o
前綴以避免歧義。 - 十六進(jìn)制浮點(diǎn)數(shù)字面量 (Hexadecimal Floating-point Literals) : 允許使用
0x
或0X
前綴表示浮點(diǎn)數(shù)的尾數(shù)部分,但必須帶有一個(gè)以p
或P
開頭的二進(jìn)制指數(shù)。例如0x1.0p-2
表示 ,即 0.25。 - 虛數(shù)字面量后綴 (Imaginary Literals) : 虛數(shù)后綴
i
現(xiàn)在可以用于任何整數(shù)或浮點(diǎn)數(shù)字面量(二進(jìn)制、八進(jìn)制、十進(jìn)制、十六進(jìn)制),如0b1011i
、0o660i
、3.14i
、0x1.fp+2i
。 - 數(shù)字分隔符 (Digit Separators) : 可以使用下劃線
_
來分隔數(shù)字,以提高長(zhǎng)數(shù)字的可讀性,例如1_000_000
、0b_1010_0110
或3.1415_9265
。下劃線可以出現(xiàn)在任意兩個(gè)數(shù)字之間,或者前綴和第一個(gè)數(shù)字之間。
package main
import "fmt"
func main() {
binaryNum := 0b1101 // 13
octalNum := 0o755 // 493
hexFloat := 0x1.Fp+2 // 1.9375 * 2^2 = 7.75
largeNum := 1_000_000_000
complexNum := 0xAp1 + 1_2i // (10 * 2^1) + 12i = 20 + 12i
fmt.Println(binaryNum)
fmt.Println(octalNum)
fmt.Println(hexFloat)
fmt.Println(largeNum)
fmt.Println(complexNum)
// 13
// 493
// 7.75
// 1000000000
// (20+12i)
}
其次,Go 1.13 取消了移位操作(<<
和 >>
)的移位計(jì)數(shù)(右操作數(shù))必須是無符號(hào)整數(shù)的限制?,F(xiàn)在可以直接使用有符號(hào)整數(shù)作為移位計(jì)數(shù)。
這消除了之前為了滿足類型要求而進(jìn)行的許多不自然的 uint
轉(zhuǎn)換。
package main
import "fmt"
func main() {
var signedShift int = 2
var value int64 = 100
// Go 1.12 及之前: 需要顯式轉(zhuǎn)換為 uint
// shiftedValueOld := value << uint(signedShift)
// Go 1.13 及之后: 可以直接使用 signed int
shiftedValueNew := value << signedShift
// fmt.Println(shiftedValueOld) // 輸出 400
fmt.Println(shiftedValueNew) // 輸出 400
var negativeShift int = -2 // 負(fù)數(shù)移位也是允許的,但行為依賴于具體實(shí)現(xiàn)和架構(gòu),通常不建議
fmt.Println(value >> negativeShift) // 行為可能非預(yù)期,輸出可能為 0 或 panic,取決于 Go 版本和具體情況
}
400
panic: runtime error: negative shift amount
goroutine 1 [running]:
main.main()
/home/piperliu/code/playground/main.go:19 +0x85
exit status 2
需要注意的是,要使用這些新的語言特性,你的項(xiàng)目需要使用 Go Modules,并且 go.mod
文件中聲明的 Go 版本至少為 1.13
。你可以手動(dòng)編輯 go.mod
文件,或者運(yùn)行 go mod edit -go=1.13
來更新。
Go Modules 與 Go 命令:模塊化體驗(yàn)改進(jìn)與工具增強(qiáng)
Go 1.13 在 Go Modules 和 go
命令行工具方面帶來了重要的改進(jìn),旨在簡(jiǎn)化開發(fā)流程和模塊管理。
模塊行為與環(huán)境變量
GO111MODULE=auto
的行為變化:現(xiàn)在,只要當(dāng)前工作目錄或其任何父目錄包含go.mod
文件,auto
設(shè)置就會(huì)激活模塊感知模式。這意味著即使項(xiàng)目位于傳統(tǒng)的GOPATH/src
目錄下,只要存在go.mod
,go
命令也會(huì)優(yōu)先使用模塊模式。這極大地簡(jiǎn)化了從GOPATH
遷移到 Modules 的過程以及混合管理兩種模式項(xiàng)目的場(chǎng)景。- 新的環(huán)境變量
GOPRIVATE
、GONOPROXY
、GONOSUMDB
:為了更好地處理私有模塊(例如公司內(nèi)部的代碼庫(kù)),引入了GOPRIVATE
環(huán)境變量。它用于指定一組不應(yīng)通過公共代理 (GOPROXY
) 下載或通過公共校驗(yàn)和數(shù)據(jù)庫(kù) (GOSUMDB
) 驗(yàn)證的模塊路徑模式(支持通配符)。GOPRIVATE
會(huì)作為GONOPROXY
和GONOSUMDB
的默認(rèn)值,提供更細(xì)粒度的控制。 GOPROXY
默認(rèn)值與配置:GOPROXY
環(huán)境變量現(xiàn)在支持逗號(hào)分隔的代理 URL 列表,以及特殊值direct
(表示直接連接源倉(cāng)庫(kù))。其默認(rèn)值更改為https://proxy.golang.org,direct
。go
命令會(huì)按順序嘗試列表中的每個(gè)代理,直到成功下載或遇到非 404/410 錯(cuò)誤。GOSUMDB
:用于指定校驗(yàn)和數(shù)據(jù)庫(kù)的名稱和可選的公鑰及 URL。默認(rèn)值為sum.golang.org
。如果模塊不在主模塊的go.sum
文件中,go
命令會(huì)查詢GOSUMDB
以驗(yàn)證下載模塊的哈希值,確保依賴未被篡改??梢栽O(shè)置為off
來禁用此檢查。
對(duì)于無法訪問公共代理或校驗(yàn)和數(shù)據(jù)庫(kù)的環(huán)境(如防火墻內(nèi)),可以使用 go env -w
命令設(shè)置全局默認(rèn)值:
# 僅直接從源倉(cāng)庫(kù)下載,不使用代理
go env -w GOPROXY=direct
# 禁用校驗(yàn)和數(shù)據(jù)庫(kù)檢查
go env -w GOSUMDB=off
# 配置私有模塊路徑 (示例)
go env -w GOPRIVATE=*.corp.example.com,github.com/my-private-org/*
go get
行為調(diào)整
go get -u
的更新范圍:在模塊模式下,go get -u
(不帶包名參數(shù)時(shí))現(xiàn)在只更新當(dāng)前目錄包的直接和間接依賴。這與GOPATH
模式下的行為更一致。如果要更新go.mod
中定義的所有依賴(包括測(cè)試依賴)到最新版本,應(yīng)使用go get -u all
。go get -u <package>
的更新范圍:當(dāng)指定包名時(shí),go get -u <package>
會(huì)更新指定的包及其導(dǎo)入的包所在的模塊,而不是這些模塊的所有傳遞依賴。@patch
版本后綴:go get
支持了新的@patch
版本后綴。例如go get example.com/mod@patch
會(huì)將example.com/mod
更新到當(dāng)前主版本和次版本下的最新補(bǔ)丁版本。@upgrade
和@latest
:@upgrade
明確要求將模塊升級(jí)到比當(dāng)前更新的版本(如果沒有新版本則保持不變,防止意外降級(jí)預(yù)發(fā)布版本)。@latest
則總是嘗試獲取最新的發(fā)布版本,無論當(dāng)前版本如何。
版本校驗(yàn)增強(qiáng)
go
命令在處理模塊版本時(shí)增加了更嚴(yán)格的校驗(yàn):
+incompatible
版本:如果一個(gè)倉(cāng)庫(kù)使用了+incompatible
標(biāo)記(通常用于 Modules 出現(xiàn)之前的 v2+ 版本),go
命令現(xiàn)在會(huì)驗(yàn)證該版本對(duì)應(yīng)的代碼樹中 不能 包含go.mod
文件。- 偽版本 (Pseudo-versions):對(duì)形如
vX.Y.Z-yyyymmddhhmmss-abcdefabcdef
的偽版本格式進(jìn)行了更嚴(yán)格的校驗(yàn),確保版本前綴、時(shí)間戳和 commit 哈希與版本控制系統(tǒng)的元數(shù)據(jù)一致。如果go.mod
中有無效的偽版本,通??梢酝ㄟ^將其簡(jiǎn)化為 commit 哈希(如require example.com/mod abcdefabcdef
)然后運(yùn)行go mod tidy
或go list -m all
來自動(dòng)修正。對(duì)于傳遞依賴中的無效版本,可以使用replace
指令強(qiáng)制替換為有效的版本或 commit 哈希。
其他 go
命令改進(jìn)
go env -w
和-u
:允許設(shè)置和取消設(shè)置go
命令環(huán)境變量的用戶級(jí)默認(rèn)值,存儲(chǔ)在用戶配置目錄下的go/env
文件中。go version <executable>
或<directory>
:可以查看 Go 二進(jìn)制文件是用哪個(gè) Go 版本編譯的(使用-m
標(biāo)志可查看嵌入的模塊信息),或查看目錄及其子目錄下所有 Go 二進(jìn)制文件的版本信息。go build -trimpath
:一個(gè)新的構(gòu)建標(biāo)志,用于從編譯出的二進(jìn)制文件中移除所有本地文件系統(tǒng)路徑信息,有助于提高構(gòu)建的可復(fù)現(xiàn)性。
錯(cuò)誤處理:官方錯(cuò)誤包裝(Error Wrapping)機(jī)制
Go 1.13 引入了一個(gè)重要的原生機(jī)制來處理錯(cuò)誤: 錯(cuò)誤包裝 (error wrapping) 。這個(gè)特性解決了長(zhǎng)期以來在 Go 中處理錯(cuò)誤時(shí)的一個(gè)痛點(diǎn):如何在添加上下文信息的同時(shí),保留底層原始錯(cuò)誤以便進(jìn)行程序化檢查。
問題背景
在 Go 1.13 之前,當(dāng)一個(gè)函數(shù)遇到來自底層調(diào)用的錯(cuò)誤,并想添加更多關(guān)于當(dāng)前操作的上下文信息時(shí),通常的做法是使用 fmt.Errorf
創(chuàng)建一個(gè)新的錯(cuò)誤字符串,包含原始錯(cuò)誤的信息(通過 %v
或 err.Error()
)。
// Go 1.13 之前的常見做法
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
// 創(chuàng)建了新錯(cuò)誤,丟失了原始 err 的類型信息 (如 *os.PathError)
return fmt.Errorf("failed to open file %q: %v", path, err)
}
// ...
defer f.Close()
return nil
}
func checkPermission() {
err := readFile("/path/to/protected/file")
// 無法直接判斷 err 是否是權(quán)限錯(cuò)誤,因?yàn)樵嫉?os.ErrPermission 信息丟失了
// if err == os.ErrPermission { ... } // 這通常行不通
}
這種方式的問題在于,返回的錯(cuò)誤是一個(gè)全新的 string
類型的錯(cuò)誤(由 fmt.Errorf
創(chuàng)建),原始錯(cuò)誤的類型信息(例如 *os.PathError
)和值(例如 os.ErrNotExist
)丟失了。調(diào)用者無法方便地檢查錯(cuò)誤的根本原因,例如判斷它是不是一個(gè)特定的錯(cuò)誤類型或哨兵錯(cuò)誤值(sentinel error)。
Go 1.13 的解決方案:%w
, Unwrap
, Is
, As
Go 1.13 通過以下方式解決了這個(gè)問題:
fmt.Errorf 的 %w 動(dòng)詞
fmt.Errorf
函數(shù)增加了一個(gè)新的格式化動(dòng)詞 %w
。當(dāng)使用 %w
來格式化一個(gè)錯(cuò)誤時(shí),fmt.Errorf
會(huì)創(chuàng)建一個(gè)新的錯(cuò)誤,這個(gè)新錯(cuò)誤不僅包含了格式化后的字符串信息,還 包裝 (wrap) 了原始的錯(cuò)誤。這個(gè)包裝后的錯(cuò)誤會(huì)實(shí)現(xiàn)一個(gè) Unwrap() error
方法,該方法返回被包裝的原始錯(cuò)誤。
package main
import (
"errors"
"fmt"
"os"
"io/fs" // fs.ErrNotExist 在 Go 1.16 引入,之前是 os.ErrNotExist
)
// queryDatabase 模擬數(shù)據(jù)庫(kù)查詢錯(cuò)誤
var ErrDBConnection = errors.New("database connection failed")
func queryDatabase(query string) error {
// 模擬連接失敗
return ErrDBConnection
}
// handleRequest 處理請(qǐng)求,調(diào)用數(shù)據(jù)庫(kù)查詢
func handleRequest(req string) error {
err := queryDatabase(req)
if err != nil {
// 使用 %w 包裝原始錯(cuò)誤 ErrDBConnection
return fmt.Errorf("failed to handle request '%s': %w", req, err)
}
return nil
}
// readFileWithErrorWrapping 示例
func readFileWithErrorWrapping(path string) error {
_, err := os.Open(path)
if err != nil {
// 使用 %w 包裝 os.Open 返回的錯(cuò)誤
return fmt.Errorf("error opening file %s: %w", path, err)
}
return nil
}
func main() {
// 場(chǎng)景1:檢查特定的哨兵錯(cuò)誤
err := handleRequest("SELECT * FROM users")
if err != nil {
fmt.Printf("Original error: %v\n", err) // 輸出包含包裝信息
// 使用 errors.Is 檢查錯(cuò)誤鏈中是否包含 ErrDBConnection
if errors.Is(err, ErrDBConnection) {
fmt.Println("Error check passed: The root cause is ErrDBConnection.")
} else {
fmt.Println("Error check failed: The root cause is NOT ErrDBConnection.")
}
}
fmt.Println("---")
// 場(chǎng)景2:檢查特定的錯(cuò)誤類型并獲取其值
errFile := readFileWithErrorWrapping("non_existent_file.txt")
if errFile != nil {
fmt.Printf("Original file error: %v\n", errFile)
// 使用 errors.As 檢查錯(cuò)誤鏈中是否有 *fs.PathError 類型
// 并將該類型的錯(cuò)誤值賦給 pathErr
var pathErr *fs.PathError
if errors.As(errFile, &pathErr) {
fmt.Printf("Error check passed: It's a PathError.\n")
fmt.Printf(" Operation: %s\n", pathErr.Op)
fmt.Printf(" Path: %s\n", pathErr.Path)
fmt.Printf(" Underlying error: %v\n", pathErr.Err) // 底層具體錯(cuò)誤
} else {
fmt.Println("Error check failed: It's NOT a PathError.")
}
// 也可以用 errors.Is 檢查底層的哨兵錯(cuò)誤
if errors.Is(errFile, fs.ErrNotExist) {
fmt.Println("Further check: The underlying error IS fs.ErrNotExist.")
}
}
}
errors.Unwrap(err error) error
這個(gè)函數(shù)接收一個(gè)錯(cuò)誤 err
。如果 err
實(shí)現(xiàn)了 Unwrap() error
方法,errors.Unwrap
會(huì)調(diào)用它并返回其結(jié)果(即被包裝的那個(gè)錯(cuò)誤)。如果 err
沒有包裝其他錯(cuò)誤,則返回 nil
。這允許你手動(dòng)地逐層解開錯(cuò)誤鏈。
errors.Is(err error, target error) bool
這是檢查錯(cuò)誤鏈的首選方式。它會(huì)遞歸地解開 err
的錯(cuò)誤鏈(通過調(diào)用 Unwrap
),檢查鏈中的任何一個(gè)錯(cuò)誤是否 等于target
哨兵錯(cuò)誤值(使用 ==
比較)。如果找到匹配項(xiàng),返回 true
。這對(duì)于檢查是否發(fā)生了某個(gè)已知的、預(yù)定義的錯(cuò)誤(如 io.EOF
, sql.ErrNoRows
, 或自定義的哨兵錯(cuò)誤)非常有用。
errors.As(err error, target interface{}) bool
這也是檢查錯(cuò)誤鏈的首選方式。它會(huì)遞歸地解開 err
的錯(cuò)誤鏈,檢查鏈中的任何一個(gè)錯(cuò)誤是否可以賦值給target
指向的類型。如果找到匹配項(xiàng),它會(huì)將該錯(cuò)誤值賦給 target
(target
必須是一個(gè)指向錯(cuò)誤類型接口或具體錯(cuò)誤類型的指針),并返回 true
。這對(duì)于檢查錯(cuò)誤是否屬于某個(gè)特定類型,并希望獲取該類型錯(cuò)誤的具體字段信息(如 *os.PathError
的 Op
和 Path
字段)非常有用。
最佳實(shí)踐
- 當(dāng)你想給一個(gè)錯(cuò)誤添加上下文,并且希望調(diào)用者能夠檢查或響應(yīng)原始錯(cuò)誤時(shí),使用
fmt.Errorf
的%w
動(dòng)詞進(jìn)行包裝。 - 當(dāng)你只想記錄錯(cuò)誤信息,不關(guān)心調(diào)用者是否需要檢查原始錯(cuò)誤時(shí),繼續(xù)使用
%v
或err.Error()
。 - 優(yōu)先使用
errors.Is
來檢查錯(cuò)誤鏈中是否包含特定的哨兵錯(cuò)誤值。 - 優(yōu)先使用
errors.As
來檢查錯(cuò)誤鏈中是否包含特定類型的錯(cuò)誤,并獲取該錯(cuò)誤的值以訪問其字段。 - 避免直接調(diào)用
Unwrap
方法,除非你有特殊需要逐層處理錯(cuò)誤鏈。errors.Is
和errors.As
通常是更健壯和方便的選擇。
錯(cuò)誤包裝機(jī)制極大地增強(qiáng)了 Go 的錯(cuò)誤處理能力,使得構(gòu)建更健壯、更易于調(diào)試和維護(hù)的程序成為可能。
sync 包:性能優(yōu)化與 sync.Pool
改進(jìn)
Go 1.13 對(duì) sync
包中的一些常用同步原語進(jìn)行了性能優(yōu)化,并改進(jìn)了 sync.Pool
的行為。
鎖和 Once 的性能提升
sync.Mutex
(互斥鎖)、sync.RWMutex
(讀寫鎖)和 sync.Once
(保證函數(shù)只執(zhí)行一次)是非?;A(chǔ)且常用的同步工具。
sync.Mutex
: 用于保護(hù)臨界區(qū),確保同一時(shí)間只有一個(gè) goroutine 可以訪問共享資源。
var mu sync.Mutex
var counter int
func increment() {
mu.Lock() // 獲取鎖
defer mu.Unlock() // 保證釋放鎖
counter++
}
sync.RWMutex
: 允許多個(gè)讀取者同時(shí)訪問資源,但寫入者必須獨(dú)占訪問。適用于讀多寫少的場(chǎng)景。
var rwMu sync.RWMutex
var config map[string]string
func getConfig(key string) string {
rwMu.RLock() // 獲取讀鎖
defer rwMu.RUnlock() // 釋放讀鎖
return config[key]
}
func setConfig(key, value string) {
rwMu.Lock() // 獲取寫鎖
defer rwMu.Unlock() // 釋放寫鎖
config[key] = value
}
sync.Once
: 用于確保某個(gè)初始化操作或其他需要只執(zhí)行一次的動(dòng)作,在并發(fā)環(huán)境下確實(shí)只執(zhí)行一次。
var once sync.Once
var serviceInstance *Service
func GetService() *Service {
once.Do(func() {
// 初始化操作,只會(huì)在首次調(diào)用 Do 時(shí)執(zhí)行
serviceInstance = &Service{}
serviceInstance.init()
})
return serviceInstance
}
在 Go 1.13 中,這些原語的 快速路徑 (fast path) (即沒有發(fā)生鎖競(jìng)爭(zhēng)或 Once.Do
已經(jīng)被執(zhí)行過的情況)被 內(nèi)聯(lián) (inlined) 到了調(diào)用者的代碼中。這意味著在最常見、性能最關(guān)鍵的非競(jìng)爭(zhēng)場(chǎng)景下,調(diào)用這些方法的開銷顯著降低。根據(jù)官方說明,在 amd64 架構(gòu)下:
Mutex.Lock
,Mutex.Unlock
,RWMutex.Lock
,RWMutex.RUnlock
的非競(jìng)爭(zhēng)情況性能提升高達(dá) 10%。Once.Do
在非首次執(zhí)行時(shí)(即once
已經(jīng)被觸發(fā)后)的速度提升了大約 2 倍。
sync.Pool 的改進(jìn)
sync.Pool
是一個(gè)用于存儲(chǔ)和復(fù)用臨時(shí)對(duì)象的技術(shù),主要目的是減少內(nèi)存分配次數(shù)和 GC 壓力,尤其適用于那些需要頻繁創(chuàng)建和銷毀、生命周期短暫的對(duì)象(如網(wǎng)絡(luò)連接的緩沖區(qū)、編解碼器的狀態(tài)對(duì)象等)。
var bufferPool = sync.Pool{
New: func() interface{} {
// New 函數(shù)用于在 Pool 為空時(shí)創(chuàng)建新對(duì)象
fmt.Println("Allocating new buffer")
return make([]byte, 4096) // 例如創(chuàng)建一個(gè) 4KB 的緩沖區(qū)
},
}
func handleConnection(conn net.Conn) {
// 從 Pool 獲取一個(gè) buffer
buf := bufferPool.Get().([]byte)
// 使用 buffer ...
n, err := conn.Read(buf)
// ...
// 將 buffer 放回 Pool 以便復(fù)用
// 注意:放回前最好清理一下 buffer 內(nèi)容(如果需要)
// e.g., buf = buf[:0] or zero out parts of it
bufferPool.Put(buf)
}
Go 1.13 對(duì) sync.Pool
做了兩項(xiàng)重要改進(jìn):
- 減少對(duì) GC STW (Stop-The-World) 暫停時(shí)間的影響 :在之前的版本中,如果
sync.Pool
中緩存了大量對(duì)象,清理這些對(duì)象(尤其是在 GC 期間)可能會(huì)對(duì) STW 暫停時(shí)間產(chǎn)生比較明顯的影響。Go 1.13 優(yōu)化了sync.Pool
的內(nèi)部實(shí)現(xiàn),使得即使池中對(duì)象很多,對(duì) GC 暫停時(shí)間的影響也顯著減小。 - 跨 GC 保留部分對(duì)象 :這是
sync.Pool
行為的一個(gè)重大變化。在 Go 1.13 之前, 每次 GC 運(yùn)行時(shí),sync.Pool
中的所有緩存對(duì)象都會(huì)被無條件清除 。這意味著每次 GC 之后,如果程序繼續(xù)請(qǐng)求對(duì)象,Pool
會(huì)變空,導(dǎo)致大量調(diào)用New
函數(shù)來重新填充緩存,這可能在 GC 后造成短暫的性能抖動(dòng)(分配和 GC 壓力增加)。
從 Go 1.13 開始,sync.Pool
可以在 GC 之后保留一部分之前緩存的對(duì)象 。它使用了一個(gè)兩階段的緩存機(jī)制,主緩存池仍然會(huì)在 GC 時(shí)被清理,但會(huì)有一個(gè)備用(受害者)緩存池保留上一次 GC 清理掉的對(duì)象,供本次 GC 后使用。這樣,GC 之后 Pool
不再是完全空的,可以更快地提供緩存對(duì)象,減少了對(duì) New
的調(diào)用頻率,從而平滑了 GC 后的性能表現(xiàn),降低了負(fù)載峰值。
使用 sync.Pool
的注意事項(xiàng)(結(jié)合 1.13 改進(jìn))
sync.Pool
仍然適用于臨時(shí)對(duì)象的復(fù)用,以減少分配和 GC 壓力。- 由于對(duì)象現(xiàn)在可能跨 GC 保留,從
Pool
中Get
到的對(duì)象可能包含上次使用時(shí)殘留的數(shù)據(jù)。因此,在使用前對(duì)其進(jìn)行必要的 重置或清理 變得更加重要(例如,對(duì)于[]byte
,使用buf = buf[:0]
;對(duì)于結(jié)構(gòu)體,清零關(guān)鍵字段)。 Pool
保留對(duì)象的能力并不意味著你可以用它來管理需要精確生命周期控制的資源(如文件句柄、網(wǎng)絡(luò)連接),這些資源通常需要顯式的Close
方法。- 雖然跨 GC 保留對(duì)象減少了冷啟動(dòng)開銷,但也意味著
Pool
可能會(huì)持有內(nèi)存更長(zhǎng)時(shí)間。不過,Go 1.13 運(yùn)行時(shí)本身也改進(jìn)了內(nèi)存歸還給操作系統(tǒng)的策略,這在一定程度上平衡了這一點(diǎn)。
總的來說,Go 1.13 中 sync
包的改進(jìn)提升了常用同步原語的性能,并使 sync.Pool
在高并發(fā)和頻繁 GC 的場(chǎng)景下表現(xiàn)更加穩(wěn)定和高效。