Go 1.24 相比 Go 1.23 有哪些值得注意的改動?
官方發(fā)布說明:https://go.dev/doc/go1.24
Go 1.24 值得關(guān)注的改動:
- 泛型類型別名 : Go 1.24 完全支持泛型類型別名(generic type aliases),允許類型別名像定義類型一樣進行參數(shù)化。
- 工具鏈升級 : go.mod 文件新增 tool 指令用于追蹤可執(zhí)行依賴;新增 GOAUTH 環(huán)境變量用于私有模塊認證;go build 默認將版本控制信息嵌入二進制文件。
- 運行時性能提升 : 通過基于 Swiss Tables 的新 map 實現(xiàn)、更高效的小對象內(nèi)存分配和新的內(nèi)部互斥鎖實現(xiàn),平均 CPU 開銷降低 2-3%。
- 限制目錄的文件系統(tǒng)訪問 : 新增 os.Root 類型,提供在特定目錄內(nèi)執(zhí)行文件系統(tǒng)操作的能力,防止訪問目錄外的路徑。
- 新的基準測試函數(shù) : 新增 testing.B.Loop 方法,用于替代傳統(tǒng)的 b.N 循環(huán),執(zhí)行基準測試迭代更快速且不易出錯。
- 改進的 Finalizer : 新增 runtime.AddCleanup 函數(shù),提供比 runtime.SetFinalizer 更靈活、高效且不易出錯的對象清理機制。
- 新增 weak 包 : 提供弱指針(weak pointers),用于構(gòu)建內(nèi)存高效的數(shù)據(jù)結(jié)構(gòu),如弱引用映射、規(guī)范化映射和緩存。
下面是一些值得展開的討論:
泛型類型別名支持
Go 1.24 現(xiàn)在完全支持泛型類型別名(generic type aliases)。這意味著類型別名可以像定義的類型(defined types)一樣,擁有自己的類型參數(shù)列表。在此之前,類型別名無法直接參數(shù)化。
這項改動使得代碼組織更加靈活。例如,你可以為一個已有的泛型類型創(chuàng)建一個別名,而無需重復(fù)其類型參數(shù)約束:
package main
import "fmt"
// 一個泛型類型
type Vector[T any] []T
// Go 1.24 起,可以為泛型類型創(chuàng)建別名
// VectorAlias 和 Vector[T] 是同一類型
type VectorAlias[T any] = Vector[T]
func main() {
var v VectorAlias[int] = []int{1, 2, 3}
v = append(v, 4)
fmt.Println(v) // 輸出: [1 2 3 4]
var originalV Vector[int] = v // 可以直接賦值,因為它們是同一類型
fmt.Println(originalV) // 輸出: [1 2 3 4]
}
類型別名也可以有自己的約束,只要它們與原始類型兼容:
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// 定義一個帶約束的泛型接口
type Number interface {
constraints.Integer | constraints.Float
}
// 定義一個泛型結(jié)構(gòu)體
type Point[T Number] struct {
X, Y T
}
// 為 Point 創(chuàng)建一個泛型類型別名,使用相同的約束
type PointAlias[T Number] = Point[T]
// 也可以創(chuàng)建更具體約束的別名 (如果原始類型允許)
// 例如,如果我們只想為整數(shù)創(chuàng)建別名
type IntPointAlias[T constraints.Integer] = Point[T]
func main() {
var p1 PointAlias[float64] = Point[float64]{X: 1.5, Y: 2.5}
fmt.Println("PointAlias[float64]:", p1) // PointAlias[float64]: {1.5 2.5}
var p2 IntPointAlias[int] = Point[int]{X: 10, Y: 20}
fmt.Println("IntPointAlias[int]:", p2) // IntPointAlias[int]: {10 20}
// 下面的代碼會編譯錯誤,因為 string 不滿足 Number 約束
// var p3 PointAlias[string] = Point[string]{X: "a", Y: "b"}
}
這個特性可以通過設(shè)置 GOEXPERIMENT=noaliastypeparams 來禁用,但這個選項計劃在 Go 1.25 中移除。
Go 命令和工具鏈的增強
Go 1.24 對 Go 命令和工具鏈進行了一些重要的改進,旨在提升開發(fā)體驗和構(gòu)建過程的可靠性。
1. 使用 tool 指令管理工具依賴
以前,開發(fā)者通常在項目根目錄下創(chuàng)建一個 tools.go 文件,并使用空導(dǎo)入(blank imports)來記錄項目所需的構(gòu)建工具(如代碼生成器、linter 等),以便 go mod tidy 不會將其移除。
Go 1.24 引入了 tool 指令,可以直接在 go.mod 文件中聲明這些工具依賴。
// go.mod
module example.com/mymodule
go 1.24
toolchain go1.24.0 // Go 1.21 引入,指定期望的工具鏈版本
require (
// ... 其他依賴 ...
)
// 新增的 tool 指令塊
tool (
golang.org/x/tools/cmd/stringer v0.19.0
honnef.co/go/tools/cmd/staticcheck latest // 也可以使用 latest
)
你可以使用 go get -tool <package> 命令來添加或更新工具依賴,例如:
go get -tool honnef.co/go/tools/cmd/staticcheck
go mod tidy 現(xiàn)在也會考慮 tool 依賴。
2. 增強的 go tool 命令
go tool 命令現(xiàn)在不僅可以運行 Go 發(fā)行版自帶的工具(如 go tool pprof, go tool vet),還可以直接運行在 go.mod 中通過 tool 指令聲明的工具。
go tool staticcheck ./...
go tool stringer -type=MyType
3. 新的 tool 元模式
可以使用 tool 作為元模式(meta-pattern)來指代 go.mod 中聲明的所有工具。
# 更新所有工具依賴到最新版本
go get tool
# 將所有工具安裝到 $GOBIN 目錄
go install tool
4. 可執(zhí)行文件緩存
go run 和 go tool(用于運行 tool 指令聲明的工具時)構(gòu)建的可執(zhí)行文件現(xiàn)在會被緩存到 Go 的構(gòu)建緩存中。這使得重復(fù)運行這些命令更快,但會增加構(gòu)建緩存的大小。
5. JSON 構(gòu)建輸出
go build 和 go install 命令新增了 -json 標志,可以將構(gòu)建過程的輸出和錯誤信息以結(jié)構(gòu)化的 JSON 格式輸出到標準輸出。這對于自動化構(gòu)建和集成分析非常有用。
go test -json 現(xiàn)在也會在輸出測試結(jié)果的同時,以 JSON 格式穿插報告構(gòu)建過程的輸出和錯誤。如果這給現(xiàn)有的測試集成系統(tǒng)帶來問題,可以通過設(shè)置 GODEBUG=gotestjsonbuildtext=1 恢復(fù)到舊的行為(構(gòu)建輸出為文本)。
6. GOAUTH 環(huán)境變量
新增 GOAUTH 環(huán)境變量,為獲取私有模塊提供了更靈活的認證方式。你可以配置不同的 URL 前綴使用不同的認證憑據(jù)。詳情請查閱 go help goauth。
7. 構(gòu)建時嵌入版本控制信息
go build 現(xiàn)在默認會根據(jù)版本控制系統(tǒng)(VCS)的信息(如 Git 的標簽和提交哈希)將主模塊的版本信息嵌入到編譯后的二進制文件中。如果工作目錄存在未提交的更改,版本信息會附加 +dirty 后綴。
你可以通過 runtime/debug.ReadBuildInfo() 讀取這些信息。如果不想嵌入這些信息,可以使用 -buildvcs=false 標志。
8. 工具鏈追蹤
可以通過設(shè)置 GODEBUG=toolchaintrace=1 來追蹤 go 命令在選擇和執(zhí)行工具鏈(編譯器、鏈接器等)時的詳細過程,這有助于調(diào)試工具鏈相關(guān)的問題。
限制目錄的文件系統(tǒng)訪問 (os.Root)
Go 1.24 在 os 包中引入了一個重要的新特性:os.Root 類型,用于將文件系統(tǒng)操作限制在指定的目錄樹內(nèi)。
os.OpenRoot(dir string) 函數(shù)會打開一個目錄 dir 并返回一個 os.Root 對象。之后,所有通過這個 os.Root 對象進行的文件系統(tǒng)操作(如 Open, Create, Mkdir, Stat, ReadDir 等)都將被限制在 dir 目錄及其子目錄下。任何試圖訪問 dir 目錄之外路徑的操作,包括通過 .. 或解析符號鏈接(symbolic links)到目錄外的情況,都會失敗并返回錯誤。
這對于需要處理不可信路徑輸入或需要在沙盒環(huán)境中操作文件的應(yīng)用程序(例如 Web 服務(wù)器、插件系統(tǒng))來說,是一個非常有用的安全增強功能。
下面是一個簡單的例子:
package main
import (
"fmt"
"log"
"os"
"path/filepath"
)
func main() {
// 創(chuàng)建一個臨時根目錄
rootDir, err := os.MkdirTemp("", "osroot-demo-root")
if err != nil {
log.Fatalf("創(chuàng)建根目錄失敗: %v", err)
}
defer os.RemoveAll(rootDir) // 清理
// 在根目錄下創(chuàng)建一些內(nèi)容
safeFilePath := filepath.Join(rootDir, "safe_file.txt")
err = os.WriteFile(safeFilePath, []byte("安全內(nèi)容"), 0644)
if err != nil {
log.Fatalf("寫入安全文件失敗: %v", err)
}
subDir := filepath.Join(rootDir, "subdir")
err = os.Mkdir(subDir, 0755)
if err != nil {
log.Fatalf("創(chuàng)建子目錄失敗: %v", err)
}
fmt.Printf("測試根目錄: %s\n", rootDir)
fmt.Printf("安全文件路徑: %s\n", safeFilePath)
// 打開根目錄,獲取 os.Root
root, err := os.OpenRoot(rootDir)
if err != nil {
log.Fatalf("os.OpenRoot 失敗: %v", err)
}
// 注意:目前的實現(xiàn) os.Root 也需要 Close,未來版本可能改變
// defer root.Close()
// 1. 嘗試在 root 內(nèi)打開文件 (成功)
f, err := root.Open("safe_file.txt") // 使用相對于 rootDir 的路徑
if err != nil {
log.Printf("在 root 內(nèi)打開 safe_file.txt 失敗: %v", err)
} else {
fmt.Println("成功在 root 內(nèi)打開 safe_file.txt")
f.Close()
}
// 2. 嘗試在 root 內(nèi)創(chuàng)建目錄 (成功)
err = root.Mkdir("another_dir", 0755)
if err != nil {
log.Printf("在 root 內(nèi)創(chuàng)建 another_dir 失敗: %v", err)
} else {
fmt.Println("成功在 root 內(nèi)創(chuàng)建 another_dir")
}
// 3. 嘗試使用 ".." 訪問 root 之外 (失敗)
_, err = root.Open("../outside_file.txt")
if err != nil {
fmt.Printf("正確地失敗了: 嘗試使用 .. 訪問外部 (%v)\n", err) // 預(yù)計錯誤
} else {
log.Fatalf("錯誤:竟然成功訪問了外部目錄!")
}
// 4. 嘗試使用絕對路徑訪問 root 之內(nèi) (失敗,os.Root 的方法只接受相對路徑)
_, err = root.Open(safeFilePath)
if err != nil {
fmt.Printf("正確地失敗了: 嘗試使用絕對路徑 %s (%v)\n", safeFilePath, err) // 預(yù)計錯誤
} else {
log.Fatalf("錯誤:竟然成功使用絕對路徑訪問!")
}
}
測試根目錄: /tmp/osroot-demo-root1840017364
安全文件路徑: /tmp/osroot-demo-root1840017364/safe_file.txt
成功在 root 內(nèi)打開 safe_file.txt
成功在 root 內(nèi)創(chuàng)建 another_dir
正確地失敗了: 嘗試使用 .. 訪問外部 (openat ../outside_file.txt: path escapes from parent)
正確地失敗了: 嘗試使用絕對路徑 /tmp/osroot-demo-root1840017364/safe_file.txt (openat /tmp/osroot-demo-root1840017364/safe_file.txt: path escapes from parent)
新的基準測試函數(shù) (testing.B.Loop)
Go 1.24 在 testing 包中引入了一個新的方法 (*testing.B).Loop,用于編寫基準測試(benchmarks)。它旨在替代傳統(tǒng)的 for i := 0; i < b.N; i++ 循環(huán),提供更精確、更不易出錯的基準測試方式。
傳統(tǒng)的 b.N 循環(huán)存在兩個主要問題:
- 如果測試函數(shù)包含昂貴的設(shè)置(setup)或清理(cleanup)代碼,這些代碼可能會在 b.N 的每次迭代中都執(zhí)行(或者至少部分執(zhí)行),從而干擾測試結(jié)果。
- 編譯器有時會過度優(yōu)化循環(huán)體,甚至完全消除它,特別是當(dāng)循環(huán)結(jié)果未被使用時,導(dǎo)致測試結(jié)果失真。
b.Loop() 方法解決了這些問題:
- 設(shè)置/清理只執(zhí)行一次 :包含 b.Loop() 的基準測試函數(shù)本身,對于每次 -count 運行(默認 -count=1),只會完整執(zhí)行一次。b.Loop() 內(nèi)部會根據(jù)需要自動調(diào)整迭代次數(shù)來達到穩(wěn)定的測量結(jié)果,但外層的設(shè)置和清理代碼只會執(zhí)行一次。
- 防止過度優(yōu)化 :b.Loop() 的實現(xiàn)機制有助于保持函數(shù)調(diào)用的參數(shù)和結(jié)果“存活”(live),防止編譯器將核心測試邏輯完全優(yōu)化掉。
使用 b.Loop() 的基本模式如下:
package main_test
import (
"strconv"
"testing"
)
// 待測試的函數(shù)
func formatInt(i int) string {
return strconv.Itoa(i)
}
// 使用 b.Loop() 的基準測試
func BenchmarkFormatIntLoop(b *testing.B) {
// 1. 在循環(huán)外執(zhí)行設(shè)置代碼
num := 12345
var result string // 聲明一個變量來接收結(jié)果,防止優(yōu)化
b.ReportAllocs() // 可選:報告內(nèi)存分配
b.ResetTimer() // 重置計時器,忽略設(shè)置時間
// 2. 使用 b.Loop() 替代 for i := 0; i < b.N; i++
for b.Loop() {
// 3. 將要測試的核心操作放在循環(huán)體內(nèi)
result = formatInt(num)
}
// 4. (可選)使用結(jié)果,進一步防止優(yōu)化
_ = result
}
// 傳統(tǒng)方式對比
func BenchmarkFormatIntOld(b *testing.B) {
num := 12345
var result string
b.ReportAllocs()
b.ResetTimer() // ResetTimer 在循環(huán)外
for i := 0; i < b.N; i++ { // 傳統(tǒng)的 b.N 循環(huán)
result = formatInt(num)
}
_ = result
}
在 BenchmarkFormatIntLoop 中,num 的初始化和 result 的聲明只在每次 -count 運行時執(zhí)行一次。b.Loop() 會負責(zé)執(zhí)行核心操作 formatInt(num) 足夠的次數(shù)以獲取可靠的性能數(shù)據(jù)。
改進的 Finalizer (runtime.AddCleanup)
Go 長期以來提供了 runtime.SetFinalizer 函數(shù),允許開發(fā)者為一個對象設(shè)置一個“終結(jié)器”(finalizer)函數(shù)。當(dāng)垃圾回收器(GC)確定該對象不再可達時,終結(jié)器函數(shù)會被調(diào)用,通常用于釋放對象關(guān)聯(lián)的非內(nèi)存資源(如文件句柄、數(shù)據(jù)庫連接等)。
然而,runtime.SetFinalizer 有一些眾所周知的缺點:
- 一個對象只能設(shè)置一個終結(jié)器。
- 不能為指向?qū)ο髢?nèi)部(例如結(jié)構(gòu)體字段的地址)的指針設(shè)置終結(jié)器。
- 如果對象參與了循環(huán)引用(cycle),即使對象實際上已經(jīng)不再使用,終結(jié)器也可能永遠不會執(zhí)行,導(dǎo)致資源泄漏。
- 終結(jié)器會延遲對象本身及其引用的其他對象的內(nèi)存回收。
Go 1.24 引入了 runtime.AddCleanup 函數(shù),提供了一個更靈活、更高效、更不易出錯的替代方案。
runtime.AddCleanup 的主要優(yōu)點:
- 多個清理函數(shù) :可以為一個對象關(guān)聯(lián)多個清理函數(shù)。它們會在對象不可達后(不保證順序)被調(diào)用。
- 支持內(nèi)部指針 :可以為指向?qū)ο髢?nèi)部的指針(interior pointers)添加清理函數(shù)。
- 循環(huán)引用更安全 :通常情況下,即使對象存在于循環(huán)引用中,只要該循環(huán)整體不再可達,關(guān)聯(lián)的清理函數(shù)也能被執(zhí)行。
- 不延遲內(nèi)存回收 :清理函數(shù)的執(zhí)行通常不會延遲對象本身或其引用對象的內(nèi)存釋放。
Go 團隊建議新代碼優(yōu)先使用 runtime.AddCleanup 而不是 runtime.SetFinalizer。
使用示例:
package main
import (
"fmt"
"runtime"
"time"
)
type FileHandle struct {
fd int
name string
}
// 定義清理函數(shù)所需的參數(shù)類型
type cleanupData struct {
fd int
name string
}
func openFile(name string, fd int) *FileHandle {
handle := &FileHandle{fd: fd, name: name}
fmt.Printf("打開文件 '%s' (fd=%d)\n", name, fd)
// 準備清理數(shù)據(jù)
data := cleanupData{fd: handle.fd, name: handle.name}
// 注冊第一個清理函數(shù)
runtime.AddCleanup(handle, func(d cleanupData) {
fmt.Printf("清理函數(shù): 關(guān)閉文件 '%s' (fd=%d)\n", d.name, d.fd)
// 實際關(guān)閉文件操作,例如 close(d.fd)
}, data)
// 注冊第二個清理函數(shù)
runtime.AddCleanup(handle, func(d cleanupData) {
fmt.Printf("清理函數(shù)2: 文件 '%s' 已處理完畢\n", d.name)
}, data)
return handle
}
func main() {
func() {
f1 := openFile("config.txt", 1)
f2 := openFile("data.log", 2)
_ = f1 // 使用 f1, f2
_ = f2
fmt.Println("內(nèi)部作用域即將結(jié)束...")
}()
fmt.Println("強制執(zhí)行 GC...")
runtime.GC() // 觸發(fā) GC
// 給清理函數(shù)執(zhí)行時間
time.Sleep(100 * time.Millisecond)
runtime.GC() // 可能需要再次 GC
time.Sleep(100 * time.Millisecond)
fmt.Println("程序結(jié)束")
}
打開文件 'config.txt' (fd=1)
打開文件 'data.log' (fd=2)
內(nèi)部作用域即將結(jié)束...
強制執(zhí)行 GC...
清理函數(shù): 關(guān)閉文件 'data.log' (fd=2)
清理函數(shù)2: 文件 'data.log' 已處理完畢
清理函數(shù): 關(guān)閉文件 'config.txt' (fd=1)
清理函數(shù)2: 文件 'config.txt' 已處理完畢
程序結(jié)束
注意 :清理函數(shù)的執(zhí)行時機依賴于 GC。它們會在對象不可達后的某個時間點執(zhí)行,但不保證立即執(zhí)行,也不保證在程序退出前一定執(zhí)行。因此,對于必須在程序退出前完成的關(guān)鍵清理操作(如刷新緩沖區(qū)),仍需依賴 defer 或其他顯式機制。
新增 weak 包提供弱指針
Go 1.24 引入了一個新的標準庫包 weak,提供了對弱指針(weak pointers)的支持。
弱指針是一種特殊的指針,它指向一個對象,但 不會 阻止該對象被垃圾回收器(GC)回收。如果對象只被弱指針引用,那么在下一次 GC 循環(huán)中,該對象就可能被回收。
weak 包主要提供了 weak.Pointer[T] 類型:
- weak.Make[T](p *T) weak.Pointer[T]: 從一個普通的強指針 p 創(chuàng)建一個弱指針。
- wp.Strong() *T: 嘗試從弱指針 wp 獲取一個指向原始對象的強指針。如果對象還未被 GC 回收,則返回該強指針;如果對象已經(jīng)被回收,則返回 nil。通過 Strong() 獲取到的強指針會阻止對象被回收,直到該強指針不再被使用。
弱指針是一個相對低級的原語,主要用于構(gòu)建內(nèi)存敏感或需要特殊生命周期管理的數(shù)據(jù)結(jié)構(gòu),例如:
- 弱引用映射(Weak Maps) : Key 或 Value 是弱引用的映射。當(dāng) Key 或 Value 被 GC 回收后,相應(yīng)的條目可以從映射中自動移除,避免內(nèi)存泄漏。常用于將元數(shù)據(jù)關(guān)聯(lián)到對象上,而又不影響對象的生命周期。
- 規(guī)范化映射(Canonicalization Maps) : 確保某個值(例如,一個大的不可變對象)在內(nèi)存中只有一個實例。弱指針可以用于檢查現(xiàn)有實例是否已被回收。
- 緩存(Caches) : 實現(xiàn)當(dāng)緩存項不再被外部強引用時可以自動從緩存中移除的策略,從而更有效地利用內(nèi)存。
weak 包通常需要與 runtime.AddCleanup(當(dāng)對象被回收時執(zhí)行清理邏輯,例如從映射中移除弱指針)或 maphash.Comparable(使指針可以用作 map 的 key)結(jié)合使用。
由于弱指針的復(fù)雜性和潛在的微妙行為,直接使用它需要非常謹慎。大多數(shù)應(yīng)用程序開發(fā)者可能不需要直接使用 weak 包,但它為庫開發(fā)者提供了構(gòu)建更高級、內(nèi)存更高效的抽象提供了基礎(chǔ)。
下面是一個非常簡化的使用弱指針作為緩存值的例子( 注意:這是一個高度簡化的示例,并非生產(chǎn)級的弱緩存實現(xiàn) ):
package main
import (
"fmt"
"runtime"
"sync"
"time"
"weak" // 導(dǎo)入 weak 包
)
type CachedData struct {
ID int
Data string
}
var cache = struct {
sync.Mutex
// 使用 string 作為 key,弱指針指向 *CachedData 作為 value
m map[string]weak.Pointer[CachedData]
}{
m: make(map[string]weak.Pointer[CachedData]),
}
func getData(id string) *CachedData {
cache.Lock()
wp, ok := cache.m[id]
cache.Unlock() // 盡快解鎖
if ok {
// 嘗試從弱指針獲取強指針
strongPtr := wp.Strong()
if strongPtr != nil {
fmt.Printf("緩存命中: %s\n", id)
return strongPtr // 對象仍然存活,返回強指針
}
// 對象已被 GC,但可能 finalizer/cleanup 還沒清理 map
fmt.Printf("緩存失效 (GC'd): %s\n", id)
// 可以在這里主動清理 map 條目
// cache.Lock()
// delete(cache.m, id)
// cache.Unlock()
}
fmt.Printf("緩存未命中或失效,重新加載: %s\n", id)
// 模擬從數(shù)據(jù)庫或其他來源加載數(shù)據(jù)
newData := &CachedData{ID: len(cache.m), Data: fmt.Sprintf("Data for %s", id)}
// 創(chuàng)建弱指針并存入緩存
wp = weak.Make(newData)
cache.Lock()
cache.m[id] = wp
cache.Unlock()
// 重要:添加清理函數(shù),當(dāng) newData 被 GC 時,從緩存中移除弱指針
// 否則弱指針對象本身會留在 map 中造成泄漏
runtime.AddCleanup(newData, func(id string) {
fmt.Printf("清理函數(shù): 移除緩存條目 %s (關(guān)聯(lián)對象已 GC)\n", id)
cache.Lock()
// 檢查當(dāng)前的弱指針是否還是當(dāng)初設(shè)置的那個,以及它是否確實已死
if currentWp, exists := cache.m[id]; exists && currentWp.Strong() == nil {
delete(cache.m, id)
}
cache.Unlock()
}, id)
return newData
}
func main() {
d1 := getData("item1") // 加載并緩存 item1
fmt.Printf("獲取到 d1: %+v\n", *d1)
d2 := getData("item2") // 加載并緩存 item2
fmt.Printf("獲取到 d2: %+v\n", *d2)
// 再次獲取 item1,應(yīng)該命中緩存
d1_again := getData("item1")
fmt.Printf("再次獲取到 d1: %+v\n", *d1_again)
// 移除對 d1 和 d1_again 的強引用
d1 = nil
d1_again = nil
fmt.Println("移除了對 item1 數(shù)據(jù)的強引用")
// 強制 GC
fmt.Println("執(zhí)行 GC...")
runtime.GC()
time.Sleep(100 * time.Millisecond) // 等待清理函數(shù)執(zhí)行
runtime.GC() // 可能需要多次 GC
time.Sleep(100 * time.Millisecond)
// 嘗試再次獲取 item1,預(yù)期緩存失效,重新加載
d1_final := getData("item1")
fmt.Printf("最終獲取到 d1: %+v\n", *d1_final)
// 獲取 item2,應(yīng)該仍然在緩存中
d2_again := getData("item2")
fmt.Printf("再次獲取到 d2: %+v\n", *d2_again)
_ = d2_again // 使用d2_again
}
這個例子展示了弱指針的基本用法:通過 weak.Make 創(chuàng)建,通過 Strong 獲取強引用,并結(jié)合 runtime.AddCleanup 在對象被回收后清理相關(guān)聯(lián)的弱指針記錄。
筆者使用 go 1.24.0 ,但是上述例子報錯:
$ go version
go version go1.24.0 linux/amd64
$ go run main.go
# command-line-arguments
./main.go:31:25: wp.Strong undefined (type weak.Pointer[CachedData] has no field or method Strong)
./main.go:60:66: currentWp.Strong undefined (type weak.Pointer[CachedData] has no field or method Strong)
這與官方文檔以及源碼相悖:
// src/internal/weak/pointer.go
// Strong creates a strong pointer from the weak pointer.
// Returns nil if the original value for the weak pointer was reclaimed by
// the garbage collector.
// If a weak pointer points to an object with a finalizer, then Strong will
// return nil as soon as the object's finalizer is queued for execution.
func (p Pointer[T]) Strong() *T {
return (*T)(runtime_makeStrongFromWeak(p.u))
}