深入理解Go語言unsafe包
1.引言
Go語言核心設(shè)計(jì)哲學(xué)
Go語言以簡潔、高效、并發(fā)特性著稱,強(qiáng)調(diào)類型安全和內(nèi)存安全,通過自動垃圾回收和嚴(yán)格類型系統(tǒng)降低內(nèi)存錯(cuò)誤風(fēng)險(xiǎn),為開發(fā)者提供可靠編程環(huán)境。
unsafe包引入原因
在與底層硬件交互、極致性能優(yōu)化等特殊場景下,Go的嚴(yán)格類型系統(tǒng)可能受限。unsafe包應(yīng)運(yùn)而生,允許繞過類型安全檢查,直接操作內(nèi)存,但使用風(fēng)險(xiǎn)較高。
Go語言特性與unsafe包背景
為什么要有unsafe指針?
unsafe.Pointer 存在的根本原因是為了突破 Go 語言嚴(yán)格的類型安全限制和內(nèi)存管理限制。直接與底層內(nèi)存、硬件或外部系統(tǒng)(如 C 庫)進(jìn)行高性能或特殊交互的場景中,提供必要的工具
unsafe指針與普通指針的區(qū)別
2.unsafe包的由來與核心概念
Go語言類型安全與內(nèi)存管理機(jī)制
Go的類型特性:
Go通過限制指針使用、禁止直接指針?biāo)阈g(shù)和不同類型指針轉(zhuǎn)換,確保內(nèi)存訪問合法性,防止懸空指針、緩沖區(qū)溢出等問題,保障程序內(nèi)存安全。 特點(diǎn):編譯時(shí)運(yùn)行
內(nèi)存管理機(jī)制:
Go引入垃圾回收機(jī)制,自動管理內(nèi)存分配和回收,避免C/C++中常見的內(nèi)存管理復(fù)雜性和安全漏洞,簡化系統(tǒng)編程。
unsafe包的誕生背景:
Go的類型安全雖有優(yōu)勢,但在特定場景下帶來性能或功能挑戰(zhàn)。為解決這些問題,unsafe包提供“逃生艙”機(jī)制,允許開發(fā)者繞過類型和內(nèi)存安全限制。
unsafe包的核心類型與函數(shù)
unsafe.Pointer
源碼實(shí)現(xiàn)
type ArbitraryType int type Pointer *ArbitraryType
源碼注釋:
unsafe.Pointer是特別定義的一種指針類型(譯注:類似C語言中的void*類型的指針),它可以包含任意類型變量的地址.
它代表一個(gè)指向任意類型的指針 ,可以指向任何數(shù)據(jù)類型,并且不攜帶任何類型信息 。
unsafe.Sizeof
unsafe.Sizeof函數(shù)返回操作數(shù)在內(nèi)存中的字節(jié)大小,參數(shù)可以是任意類型的表達(dá)式,但是它并不會對表達(dá)式進(jìn)行求值。一個(gè)Sizeof函數(shù)調(diào)用是一個(gè)對應(yīng)uintptr類型的常量表達(dá)式,因此返回的結(jié)果可以用作數(shù)組類型的長度大小,或者用作計(jì)算其他的常量。
Sizeof函數(shù)返回的大小只包括數(shù)據(jù)結(jié)構(gòu)中固定的部分,例如字符串對應(yīng)結(jié)構(gòu)體中的指針和字符串長度部分,但是并不包含指針指向的字符串的內(nèi)容。
unsafe.Alignof(expression)
unsafe.Alignof 函數(shù)返回對應(yīng)參數(shù)的類型需要對齊的倍數(shù)。和 Sizeof 類似, Alignof 也是返回一個(gè)常量表達(dá)式,對應(yīng)一個(gè)常量。
內(nèi)存對齊
什么是內(nèi)存對齊呢?
內(nèi)存對齊就是指數(shù)據(jù)在內(nèi)存中的起始地址必須是某個(gè)特定數(shù)字(對齊值)的倍數(shù)。這個(gè)“特定數(shù)字”通常是 2 的冪次方,比如 1、2、4、8、16 字節(jié)。
為什么需要內(nèi)存對齊呢?
CPU 訪問效率: CPU 并不是一個(gè)字節(jié)一個(gè)字節(jié)地從內(nèi)存中讀取數(shù)據(jù)。它通常會以為單位(比如 4 字節(jié)、8 字節(jié)、16 字節(jié))進(jìn)行批量讀取。如果一個(gè)數(shù)據(jù)類型(例如一個(gè) 8 字節(jié)的
int64
)的起始地址不是其字長的倍數(shù),那么 CPU 可能需要:- 進(jìn)行多次內(nèi)存訪問(比如一次讀取前半部分,另一次讀取后半部分)。
- 或者進(jìn)行額外的位移操作來提取所需的數(shù)據(jù)。 這些都會增加 CPU 的負(fù)擔(dān),降低程序運(yùn)行速度。如果數(shù)據(jù)是對齊的,CPU 就能在一個(gè)內(nèi)存周期內(nèi)高效地讀取整個(gè)數(shù)據(jù)。
緩存優(yōu)化: CPU 有高速緩存(Cache),它一次性會加載一塊內(nèi)存數(shù)據(jù)到緩存中(稱為緩存行)。如果數(shù)據(jù)對齊,并且能完整地放入一個(gè)或幾個(gè)緩存行中,就能提高緩存命中率,進(jìn)一步提升性能。
有內(nèi)存對齊,就肯定要有內(nèi)存對齊規(guī)則
每個(gè)數(shù)據(jù)類型都有一個(gè)默認(rèn)的對齊值。
通常,一個(gè)基本數(shù)據(jù)類型的對齊值等于它在內(nèi)存中占用的字節(jié)數(shù)。
bool
、byte
:1 字節(jié)對齊int16
:2 字節(jié)對齊int32
、float32
:4 字節(jié)對齊int64
、float64
、指針、string
(頭部)、slice
(頭部)、interface
(頭部):8 字節(jié)對齊(在 64 位系統(tǒng)上)
在 Go 語言中,可以通過
unsafe.Alignof()
函數(shù)來查看任何變量的對齊值。結(jié)構(gòu)體(Struct)的對齊值。
- 整個(gè)結(jié)構(gòu)體的對齊值是其所有字段中最大那個(gè)字段的對齊值。
填充(Padding)字節(jié)。
- 為了滿足對齊要求,編譯器會在結(jié)構(gòu)體字段之間以及結(jié)構(gòu)體末尾插入額外的填充(Padding)字節(jié)。這些填充字節(jié)不存儲任何實(shí)際數(shù)據(jù),只是為了確保下一個(gè)字段(或下一個(gè)結(jié)構(gòu)體實(shí)例)能夠從正確的對齊地址開始。
- 你可以通過
unsafe.Sizeof()
來查看結(jié)構(gòu)體的實(shí)際大小,這個(gè)大小包含了填充字節(jié)。
結(jié)構(gòu)體總大小必須是對齊值的倍數(shù)。
- 即使結(jié)構(gòu)體的所有字段都正確對齊了,如果結(jié)構(gòu)體的總大小不是其自身對齊值的倍數(shù),編譯器也會在結(jié)構(gòu)體末尾添加填充字節(jié),以確保當(dāng)這個(gè)結(jié)構(gòu)體作為數(shù)組元素或嵌套在其他結(jié)構(gòu)體中時(shí),下一個(gè)元素也能正確對齊。
unsafe. Offsetof
函數(shù)返回結(jié)構(gòu)體中某個(gè)字段相對于結(jié)構(gòu)體起始地址的字節(jié)偏移量。這個(gè)偏移量是考慮了字段大小和內(nèi)存對齊后,該字段實(shí)際開始的字節(jié)位置。
目的: 這個(gè)函數(shù)揭示了編譯器在內(nèi)存中如何排列結(jié)構(gòu)體字段,包括為了對齊而插入的任何填充。
示例:
對于一個(gè)結(jié)構(gòu)體:
var x struct { a bool b int16 c []int }
下面顯示了對x和它的三個(gè)字段調(diào)用unsafe包相關(guān)函數(shù)的計(jì)算結(jié)果:
顯示了一個(gè)結(jié)構(gòu)體變量 x 以及其在32位和64位機(jī)器上的典型的內(nèi)存?;疑珔^(qū)域是空洞。
對于不同的系統(tǒng)計(jì)算是不一樣的:
32位系統(tǒng):
Sizeof(x) = 16 Alignof(x) = 4 Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0 Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2 Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4
64位系統(tǒng):
Sizeof(x) = 32 Alignof(x) = 8 Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0 Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2 Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
unsafe.Add(ptr Pointer, len IntegerType) Pointer
此函數(shù)將一個(gè)偏移量 len 添加到 ptr 指向的地址,并返回一個(gè)新的 unsafe.Pointer,代表新的內(nèi)存地址。這部分地覆蓋了之前通過 uintptr 進(jìn)行指針?biāo)阈g(shù)的常見用法,并提供了更清晰的語義 。
unsafe.Slice(ptr *ArbitraryType, len IntegerType)ArbitraryType:
從一個(gè)安全指針 ptr 和指定長度 len 創(chuàng)建一個(gè)切片。ArbitraryType 是結(jié)果切片的元素類型。這允許在不復(fù)制數(shù)據(jù)的情況下將底層數(shù)組解釋為切片 。
unsafe.String(ptr *byte, len IntegerType) string:
從一個(gè) byte 指針 ptr 和指定長度 len 創(chuàng)建一個(gè)字符串。由于Go字符串是不可變的,通過此函數(shù)創(chuàng)建的字符串,其底層字節(jié)在返回后不應(yīng)被修改 。 =
unsafe.StringData(str string) *byte:
返回字符串 str 底層字節(jié)的指針。對于空字符串,返回值是不確定的,可能為 nil。同樣,返回的字節(jié)不應(yīng)被修改 。
unsafe.SliceData(sliceArbitraryType) *ArbitraryType:
返回切片 slice 底層數(shù)組的指針。這有助于在不進(jìn)行額外內(nèi)存分配的情況下,獲取切片底層數(shù)據(jù)的直接引用 。
示例:
package main import ( "fmt" "unsafe" ) type Employee struct { ID int32 Name string Age int16 Active bool } func main() { emp := Employee{ID: 101, Name: "Alice", Age: 30, Active: true} basePtr := unsafe.Pointer(&emp) fmt.Println(basePtr) ageOffset := unsafe.Offsetof(emp.Age) agePtr := unsafe.Add(basePtr, ageOffset) // add函數(shù)是將原始的地址加上一個(gè)偏移量,返回一個(gè)新的地址 fmt.Println(agePtr) data := [5]byte{10, 20, 30, 40, 50} fmt.Printf("原始 Go 數(shù)組: %v (地址: %p)\n", data, &data[0]) // 使用 unsafe.Slice 將原始數(shù)組的底層內(nèi)存轉(zhuǎn)換為 []byte 切片 // 第一個(gè)參數(shù)是原始內(nèi)存的起始指針 // 第二個(gè)參數(shù)是切片的長度 // 這是 Go 1.17+ 用于安全創(chuàng)建切片的方式 slice := unsafe.Slice(&data[0], len(data)) fmt.Printf("通過 unsafe.Slice 創(chuàng)建的切片: %v (地址: %p)\n", slice, &slice[0]) // 驗(yàn)證地址是否一致 (零拷貝) fmt.Printf("原始數(shù)組起始地址 == 切片起始地址? %t\n", unsafe.Pointer(&data[0]) == unsafe.Pointer(&slice[0])) slice[0] = 100 fmt.Printf("修改切片后原始數(shù)組: %v\n", data) // Output: [100 20 30 40 50] }
運(yùn)行結(jié)果:
0xc0000943a0
0xc0000943b8
原始 Go 數(shù)組: [10 20 30 40 50] (地址: 0xc00008c0a8)
通過 unsafe.Slice 創(chuàng)建的切片: [10 20 30 40 50] (地址: 0xc00008c0a8)
原始數(shù)組起始地址 == 切片起始地址? true
修改切片后原始數(shù)組: [100 20 30 40 50]
3.unsafe包的應(yīng)用場景與代碼示例
不同類型間的零拷貝轉(zhuǎn)換:
Go通常不允許不同類型間直接零拷貝轉(zhuǎn)換,unsafe包打破限制,實(shí)現(xiàn)底層內(nèi)存布局兼容的類型轉(zhuǎn)換,避免內(nèi)存分配和復(fù)制,提高性
package main import ( "fmt" "reflect" "unsafe" ) // Float64bits 返回 f 的 IEEE 754 浮點(diǎn)數(shù)的二進(jìn)制表示 func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) // 將 float64 的地址轉(zhuǎn)換為 *uint64 類型,然后解引用 } // Float64frombits 返回 IEEE 754 浮點(diǎn)數(shù)的二進(jìn)制表示 b 對應(yīng)的 float64 值 func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) // 將 uint64 的地址轉(zhuǎn)換為 *float64 類型,然后解引用 } func main() { f := 3.1415926535 bits := Float64bits(f) fmt.Printf("Original float64: %f\n", f) fmt.Println(reflect.TypeOf(bits).Name()) newFloat := Float64frombits(bits) fmt.Println(reflect.TypeOf(newFloat).Name()) fmt.Printf("Converted back: %f\n", newFloat) // 演示byte 和 string 的零拷貝轉(zhuǎn)換 byteSlice := []byte{'H', 'e', 'l', 'l', 'o', ' ', 'G', 'o'} fmt.Printf("原始 byteSlice 地址: %p\n", &byteSlice[0]) // 將byte 轉(zhuǎn)換為 string,避免復(fù)制。 // 注意:轉(zhuǎn)換后的 string 不應(yīng)再修改原始 byteSlice 的內(nèi)容。 s := unsafe.String(unsafe.SliceData(byteSlice), len(byteSlice)) fmt.Printf("轉(zhuǎn)換為 string (s) 的底層數(shù)據(jù)地址: %p\n", unsafe.StringData(s)) fmt.Printf("Byte slice to string (zero-copy): %s\n", s) // 將 string 轉(zhuǎn)換為byte,避免復(fù)制。 // 注意:轉(zhuǎn)換后的byte 不應(yīng)修改,因?yàn)樵?string 是不可變的。 b := unsafe.Slice(unsafe.StringData(s), len(s)) fmt.Printf("轉(zhuǎn)換為 []byte (b) 的底層數(shù)據(jù)地址: %p\n", unsafe.SliceData(b)) fmt.Printf("String to byte slice : %v\n", b) }
運(yùn)行結(jié)果:
Original float64: 3.141593
uint64
float64
Converted back: 3.141593
原始 byteSlice 地址: 0xc00000a128
轉(zhuǎn)換為 string (s) 的底層數(shù)據(jù)地址: 0xc00000a128
Byte slice to string (zero-copy): Hello Go
轉(zhuǎn)換為 []byte (b) 的底層數(shù)據(jù)地址: 0xc00000a128
String to byte slice : [72 101 108 108 111 32 71 111]
結(jié)構(gòu)體內(nèi)部字段的直接訪問與修改:
Go語言的結(jié)構(gòu)體字段默認(rèn)是可訪問的,但對于未導(dǎo)出的(小寫字母開頭)字段,外部包無法直接訪問。unsafe 包可以繞過這種訪問限制,允許直接通過內(nèi)存地址計(jì)算來訪問和修改結(jié)構(gòu)體的任何字段,包括未導(dǎo)出的字段 。
package main import ( "fmt" "unsafe" ) type MyStruct struct { id int // 未導(dǎo)出字段 Name string // 導(dǎo)出字段 } func main() { s := MyStruct{ id: 123, Name: "Original Name", } fmt.Printf("Original struct: %+v\n", s) // 1. 通過 unsafe.Offsetof 獲取未導(dǎo)出字段 id 的偏移量 idOffset := unsafe.Offsetof(s.id) fmt.Printf("Offset of 'id' field: %d bytes\n", idOffset) // 2. 獲取結(jié)構(gòu)體 s 的內(nèi)存地址,并轉(zhuǎn)換為 uintptr sPtr := uintptr(unsafe.Pointer(&s)) // 3. 計(jì)算 id 字段的內(nèi)存地址 idAddr := sPtr + idOffset // 4. 將 id 字段的內(nèi)存地址轉(zhuǎn)換為 *int 類型指針,并修改其值 idPtr := (*int)(unsafe.Pointer(idAddr)) *idPtr = 456 fmt.Printf("Modified struct: %+v\n", s) // 驗(yàn)證修改是否成功 fmt.Printf("Accessing modified id: %d\n", s.id) }
運(yùn)行結(jié)果; Original struct: {id:123 Name:Original Name} Offset of 'id' field: 0 bytes Modified struct: {id:456 Name:Original Name} Accessing modified id: 456
具體性能提升:
這里從類型轉(zhuǎn)化和字段修改,兩個(gè)方面具體,通過測試體現(xiàn)出使用unsafe的速度提升:
可以看出由于unsafe直接可以操作底層內(nèi)存,對于性能的提升是很大的。
類型轉(zhuǎn)換:
package main import ( "fmt" "strings" "testing" // 導(dǎo)入 testing 包,用于基準(zhǔn)測試函數(shù) "unsafe" ) // stringFromBytesSafe 是安全、常規(guī)的 []byte 到 string 轉(zhuǎn)換(有復(fù)制) func stringFromBytesSafe(b []byte) string { return string(b) } // stringFromBytesUnsafe 是不安全、零拷貝的 []byte 到 string 轉(zhuǎn)換 func stringFromBytesUnsafe(b []byte) string { // 確保傳入的 []byte 在 string 的生命周期內(nèi)不會被修改! return unsafe.String(unsafe.SliceData(b), len(b)) } func main() { // 創(chuàng)建一個(gè)大字節(jié)切片,模擬需要轉(zhuǎn)換的數(shù)據(jù) data := []byte(strings.Repeat("A", 1024*1024)) // 1MB 的字節(jié)數(shù)據(jù) fmt.Println("--- []byte 到 string 轉(zhuǎn)換性能比較 ---") // 模擬基準(zhǔn)測試,實(shí)際項(xiàng)目中應(yīng)使用 go test -bench=. fmt.Println("運(yùn)行安全轉(zhuǎn)換 (string(b))...") safeResult := testing.Benchmark(func(b *testing.B) { for i := 0; i < b.N; i++ { _ = stringFromBytesSafe(data) } }) fmt.Printf("安全轉(zhuǎn)換平均耗時(shí): %s/op\n", safeResult.T) fmt.Printf("安全轉(zhuǎn)換平均內(nèi)存分配: %d B/op (每次操作的內(nèi)存分配量)\n", safeResult.AllocedBytesPerOp()) fmt.Printf("安全轉(zhuǎn)換平均內(nèi)存分配次數(shù): %d allocs/op\n", safeResult.AllocsPerOp()) fmt.Println("\n運(yùn)行不安全零拷貝轉(zhuǎn)換 (unsafe.String())...") unsafeResult := testing.Benchmark(func(b *testing.B) { for i := 0; i < b.N; i++ { _ = stringFromBytesUnsafe(data) } }) fmt.Printf("不安全轉(zhuǎn)換平均耗時(shí): %s/op\n", unsafeResult.T) fmt.Printf("不安全轉(zhuǎn)換平均內(nèi)存分配: %d B/op\n", unsafeResult.AllocedBytesPerOp()) fmt.Printf("不安全轉(zhuǎn)換平均內(nèi)存分配次數(shù): %d allocs/op\n", unsafeResult.AllocsPerOp()) }
--- []byte 到 string 轉(zhuǎn)換性能比較 ---
運(yùn)行安全轉(zhuǎn)換 (string(b))...
安全轉(zhuǎn)換平均耗時(shí): 1.0996818s/op
安全轉(zhuǎn)換平均內(nèi)存分配: 1048583 B/op (每次操作的內(nèi)存分配量)
安全轉(zhuǎn)換平均內(nèi)存分配次數(shù): 1 allocs/op運(yùn)行不安全零拷貝轉(zhuǎn)換 (unsafe.String())...
不安全轉(zhuǎn)換平均耗時(shí): 320.9903ms/op
不安全轉(zhuǎn)換平均內(nèi)存分配: 0 B/op
不安全轉(zhuǎn)換平均內(nèi)存分配次數(shù): 0 allocs/op
修改結(jié)構(gòu)體字段:
package main import ( "fmt" "reflect" "testing" // 導(dǎo)入 testing 包,用于基準(zhǔn)測試 "unsafe" ) type MyData struct { id int name string value float64 } // 通過 unsafe 直接修改私有字段 'id' func unsafeSetID(data *MyData, newID int) { basePtr := unsafe.Pointer(data) idOffset := unsafe.Offsetof(data.id) idPtr := unsafe.Add(basePtr, idOffset) *(*int)(idPtr) = newID } type MyDataPublic struct { ID int // 公共字段 name string value float64 } // 通過 reflect 修改公共字段 'ID' func reflectSetID(data *MyDataPublic, newID int) { v := reflect.ValueOf(data).Elem() idField := v.FieldByName("ID") idField.SetInt(int64(newID)) } func main() { privateData := &MyData{id: 1, name: "private", value: 1.23} publicData := &MyDataPublic{ID: 1, name: "public", value: 1.23} // 基準(zhǔn)測試:通過 unsafe 修改私有字段 'id' fmt.Println("unsafe 修改私有字段 'id':") unsafeResult := testing.Benchmark(func(b *testing.B) { for i := 0; i < b.N; i++ { unsafeSetID(privateData, i) } }) fmt.Printf(" 平均耗時(shí): %s/op\n", unsafeResult.T) fmt.Printf(" 內(nèi)存分配: %d B/op (bytes allocated per operation)\n", unsafeResult.AllocedBytesPerOp()) fmt.Printf(" 分配次數(shù): %d allocs/op (allocations per operation)\n", unsafeResult.AllocsPerOp()) // 基準(zhǔn)測試:通過 reflect 修改公共字段 'ID' fmt.Println("\nreflect 修改公共字段 'ID':") reflectResult := testing.Benchmark(func(b *testing.B) { v := reflect.ValueOf(publicData).Elem() idField := v.FieldByName("ID") b.ResetTimer() // 重置計(jì)時(shí)器,從這里開始測量 for i := 0; i < b.N; i++ { idField.SetInt(int64(i)) } }) fmt.Printf(" 平均耗時(shí): %s/op\n", reflectResult.T) fmt.Printf(" 內(nèi)存分配: %d B/op\n", reflectResult.AllocedBytesPerOp()) fmt.Printf(" 分配次數(shù): %d allocs/op\n", reflectResult.AllocsPerOp()) }
運(yùn)行結(jié)果:
unsafe 修改私有字段 'id':
平均耗時(shí): 213.3485ms/op
內(nèi)存分配: 0 B/op (bytes allocated per operation)
分配次數(shù): 0 allocs/op (allocations per operation)reflect 修改公共字段 'ID':
平均耗時(shí): 1.1784909s/op
內(nèi)存分配: 0 B/op
分配次數(shù): 0 allocs/op
具體使用案例:
unsafe在GO標(biāo)準(zhǔn)庫使用:
reflect 包
runtime包
bytes 包和 strings 包
go內(nèi)置的還有map、slice、chan 等
unsafe在第三方庫使用:
jsoniter/go (json-iterator/go):(高性能JSON庫)
valyala/fasthttp:(高性能HTTP框架)
高性能核心:
規(guī)避不必要的內(nèi)存分配和數(shù)據(jù)復(fù)制。
繞過運(yùn)行時(shí)類型系統(tǒng)和反射的開銷,直接與內(nèi)存打交道。
4.使用unsafe包的風(fēng)險(xiǎn)
1. 破壞類型安全
如果你轉(zhuǎn)換的類型與實(shí)際內(nèi)存中的數(shù)據(jù)不匹配,那么在解引用或操作時(shí),就會讀取到無意義的數(shù)據(jù),或者更糟糕,導(dǎo)致程序崩潰(panic)。
2.懸空指針 (Dangling Pointers) 和垃圾回收問題
首先我們要先理解Go GC工作方式(這里作簡要描述):
Go GC 的工作方式
Go 語言的垃圾回收器是精確的 (precise)。這意味著 GC 能夠準(zhǔn)確地識別內(nèi)存中的哪些值是指針,以及這些指針指向了哪里。為了做到這一點(diǎn),GC 嚴(yán)重依賴于 Go 語言在編譯時(shí)和運(yùn)行時(shí)維護(hù)的類型信息。
當(dāng) GC 掃描內(nèi)存時(shí),它會:
- 知道每個(gè)對象的類型:
根據(jù)類型信息追蹤指針:
- 如果 GC 發(fā)現(xiàn)某個(gè)對象沒有任何活躍的指針指向它(即從根對象,如全局變量、活躍 Goroutine 的棧等,都無法到達(dá)它),那么 GC 就會認(rèn)為這個(gè)對象是“垃圾”,可以在后續(xù)階段將其內(nèi)存回收。
為什么會出現(xiàn)這個(gè)問題?
unsafe.Pointer的設(shè)計(jì)目的就是為了擺脫類型信息,它只是一個(gè)純粹的內(nèi)存地址。
1.GC無法跟蹤unsafe.Pointer所指向的對象:
當(dāng) Go GC 看到一個(gè) unsafe.Pointer
時(shí),它不知道這個(gè)指針指向的內(nèi)存區(qū)域包含什么類型的數(shù)據(jù)。它無法判斷這個(gè)內(nèi)存區(qū)域里是否有其他 Go 對象指針,也無法判斷這個(gè) unsafe.Pointer
是否是某個(gè) Go 對象的唯一“活著”的引用。
因此,Go GC 明確選擇不追蹤 unsafe.Pointer
本身所指向的內(nèi)存。它將其僅僅視為一個(gè)普通的數(shù)字 (uintptr
)
2.懸空指針的產(chǎn)生:
由于 GC 不追蹤 unsafe.Pointer
所指向的對象,這可能導(dǎo)致一個(gè)嚴(yán)重的后果:當(dāng)你通過 unsafe.Pointer
獲得了某個(gè) Go 對象的內(nèi)存地址,但這個(gè)對象卻沒有其他 “可追蹤的 Go 指針” 指向它時(shí),GC 可能會錯(cuò)誤地認(rèn)為這個(gè)對象是垃圾并將其回收。
這時(shí)unsafe.Pointer 就變成了一個(gè)“懸空指針”。
如何避免
1.你所操作的內(nèi)存區(qū)域不會在其生命周期內(nèi)被 GC 回收,除非你已經(jīng)明確知道并處理了回收后的行為。
2.通常情況下,你所操作的 Go 對象至少有一個(gè)普通 Go 指針在活躍地引用它,從而阻止 GC 回收它。
5.unsafe包的替代方案與常規(guī)方法
1.性能優(yōu)化方面:
優(yōu)化數(shù)據(jù)結(jié)構(gòu)布局 (內(nèi)存對齊):
算法和數(shù)據(jù)結(jié)構(gòu)的優(yōu)化:
2.繞過類型系統(tǒng)
Go 1.18+ 的泛型:
訪問私有字段:reflect
包
6.總結(jié)
價(jià)值:
unsafe包為Go語言提供底層內(nèi)存操作能力,在特定場景下實(shí)現(xiàn)極致性能和靈活性,是Go生態(tài)系統(tǒng)的重要補(bǔ)充。
限制:
使用unsafe面臨非可移植性、安全問題、未定義行為和調(diào)試?yán)щy等風(fēng)險(xiǎn),
到此這篇關(guān)于深入理解Go語言unsafe包的文章就介紹到這了,更多相關(guān)Go語言unsafe包內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于go-zero單體服務(wù)使用泛型簡化注冊Handler路由的問題
這篇文章主要介紹了go-zero單體服務(wù)使用泛型簡化注冊Handler路由,涉及到Golang環(huán)境安裝及配置Go Module的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Centos下搭建golang環(huán)境及vim高亮Go關(guān)鍵字設(shè)置的方法
這篇文章先給大家詳細(xì)介紹了在Centos下搭建golang環(huán)境的步驟,大家按照下面的方法就可以自己搭建golang環(huán)境,搭建完成后又給大家介紹了vim高亮Go關(guān)鍵字設(shè)置的方法,文中通過示例代碼介紹的很詳細(xì),有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-11-11Golang安裝和使用protocol-buffer流程介紹
這篇文章主要介紹了Golang安裝和使用protocol-buffer過程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09Golang中匿名函數(shù)的實(shí)現(xiàn)
本文主要介紹了Golang中匿名函數(shù)的實(shí)現(xiàn),包括直接調(diào)用、賦值給變量及定義全局匿名函數(shù)三種方式,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-06-06