unsafe
公式ドキュメント:unsafe package - unsafe - Go Packages
unsafe 標準ライブラリは、公式が提供する低レベルプログラミング用のライブラリです。このパッケージは Go のタイプシステムをスキップしてメモリを直接読み書きできる操作を提供します。このパッケージは移植性がない可能性があり、公式は Go 1 互換性基準の保護対象外であると明言しています。それでも、unsafe は多くのプロジェクトで広く使用されており、公式の標準ライブラリにも含まれています。
TIP
移植性がない理由は、一部の操作の結果が OS 実装に依存しており、異なるシステムでは異なる結果になる可能性があるためです。
ArbitraryType
type ArbitraryType intArbitrary は「任意の」と訳せます。ここでは任意のタイプを表し、any とは等しくありません。実際、このタイプは unsafe パッケージに属しておらず、ドキュメント目的のためにのみ記載されています。
IntegerType
type IntegerType intIntegerType は任意の整数タイプを表します。実際、このタイプは unsafe パッケージに属しておらず、ドキュメント目的のためにのみ記載されています。
上記の 2 つのタイプはあまり気にする必要はありません。これらは単なる代表であり、unsafe パッケージの関数を使用する際、エディタはタイプが一致しないという警告を表示します。実際のタイプは传入された具体的なタイプです。
Sizeof
func Sizeof(x ArbitraryType) uintptr変数 x のサイズをバイト単位で返します。参照コンテンツのサイズは含まれません。例:
func main() {
var ints byte = 1
fmt.Println(unsafe.Sizeof(ints))
var floats float32 = 1.0
fmt.Println(unsafe.Sizeof(floats))
var complexs complex128 = 1 + 2i
fmt.Println(unsafe.Sizeof(complexs))
var slice []int = make([]int, 100)
fmt.Println(unsafe.Sizeof(slice))
var mp map[string]int = make(map[string]int, 0)
fmt.Println(unsafe.Sizeof(mp))
type person struct {
name string
age int
}
fmt.Println(unsafe.Sizeof(person{}))
type man struct {
name string
}
fmt.Println(unsafe.Sizeof(man{}))
}1
4
16
24
8
24
16Offsetof
func Offsetof(x ArbitraryType) uintptrこの関数は構造体内のフィールドオフセットを表します。したがって、x は構造体フィールドである必要があります。つまり、戻り値は構造体アドレス開始位置からフィールドアドレス開始位置までのバイト数です。例:
func main() {
type person struct {
name string
age int
}
p := person{
name: "aa",
age: 11,
}
fmt.Println(unsafe.Sizeof(p))
fmt.Println(unsafe.Offsetof(p.name))
fmt.Println(unsafe.Sizeof(p.name))
fmt.Println(unsafe.Offsetof(p.age))
fmt.Println(unsafe.Sizeof(p.age))
}24
0
16
16
8Alignof
メモリアライメントについてよくわからない場合は、こちらをご覧ください:Go 言語メモリアライメント详解 - 掘金 (juejin.cn)
func Alignof(x ArbitraryType) uintptrアライメントサイズは通常、バイト単位のコンピュータワード長と Sizeof の最小値です。例えば amd64 マシンでは、ワード長は 64 ビット、つまり 8 バイトです。例:
func main() {
type person struct {
name string
age int32
}
p := person{
name: "aa",
age: 11,
}
fmt.Println(unsafe.Alignof(p), unsafe.Sizeof(p))
fmt.Println(unsafe.Alignof(p.name), unsafe.Sizeof(p.name))
fmt.Println(unsafe.Alignof(p.age), unsafe.Sizeof(p.age))
}8 24
8 16
4 4Pointer
type Pointer *ArbitraryTypePointer は任意のタイプを指すことができる「ポインタ」です。そのタイプは *ArbitraryType です。このタイプは uintptr と組み合わせて使用することで、初めて unsafe パッケージの真の威力を発揮できます。公式ドキュメントの説明によると、unsafe.Pointer タイプは 4 つの特殊操作を実行できます。
- 任意のタイプのポインタを
unsafe.Pointerに変換できます unsafe.Pointerを任意のタイプのポインタに変換できますuintptrをunsafe.Pointerに変換できますunsafe.Pointerをuintptrに変換できます
これら 4 つの特殊操作が unsafe パッケージ全体の基盤を構成しています。これらの操作により、タイプシステムを無視してメモリを直接読み書きするコードを作成できます。使用する際は特に注意することをお勧めします。
TIP
unsafe.Pointer は逆参照できません。同様に、アドレスを取得することもできません。
(1) *T1 を unsafe.Pointer に変換し、さらに *T2 に変換
タイプ *T1、*T2 があるとします。T2 が T1 以下であり、両者のメモリレイアウトが等価である場合、T2 タイプのデータを T1 に変換できます。例:
func main() {
fmt.Println(Float64bits(12.3))
fmt.Println(Float64frombits(Float64bits(12.3)))
}
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
func Float64frombits(b uint64) float64 {
return *(*float64)(unsafe.Pointer(&b))
}4623113902481840538
12.3これら 2 つの関数は実際には math パッケージの 2 つの関数です。プロセス中のタイプ変化は以下の通りです。
float64 -> *float64 -> unsafe.Pointer -> *uint64 -> uint64 -> *uint64 -> unsafe.Pointer -> *float64 -> float64(2) unsafe.Pointer を uintptr に変換
unsafe.Pointer を uintptr に変換すると、前者が指すアドレスが後者の値になります。uintptr はアドレスを保存しますが、前者は構文的にはポインタであり参照ですが、後者は単なる整数値です。例:
func main() {
num := 1
fmt.Println(unsafe.Pointer(&num))
fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}0xc00001c088
0xc00001c088もう 1 つの大きな違いはガベージコレクションの処理です。unsafe.Pointer は参照であるため、必要に応じて回収されません。一方、後者は単なる値であるため、このような特別な扱いはありません。もう 1 つ注意すべき点は、ポインタが指す要素アドレスが移動する際、GC はポインタ参照の旧アドレスを更新しますが、uintptr が保存する値は更新しないことです。例えば、以下のコードは問題を引き起こす可能性があります。
func main() {
num := 16
address := uintptr(unsafe.Pointer(&num))
np := (*int64)(unsafe.Pointer(address))
fmt.Println(*np)
}場合によっては、GC が変数を移動した後、address が指すアドレスは無効になり、その値を使用してポインタを作成すると panic が発生します。
panic: runtime error: invalid memory address or nil pointer dereferenceしたがって、Pointer を uintptr に変換した値を保存することはお勧めしません。
(3) uintptr を unsafe.Pointer に変換
以下の方法で uintptr からポインタを取得できます。ポインタが有効であれば、例 2 のような無効アドレスの問題は発生しません。Pointer とタイプポインタ自体はポインタ演算をサポートしていませんが、uintptr は単なる整数値であるため、数学演算が可能です。uintptr に数学演算を実行した後、Pointer に変換することでポインタ演算を完了できます。
p = unsafe.Pointer(uintptr(p) + offset)これにより、1 つのポインタのみを使用して、配列や構造体などの内部要素にアクセスできます。内部要素が外部に公開されているかどうかに関係なく、例:
func main() {
type person struct {
name string
age int32
}
p := &person{"jack", 18}
pp := unsafe.Pointer(p)
fmt.Println(*(*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.name))))
fmt.Println(*(*int32)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.age))))
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
ps := unsafe.Pointer(&s[0])
fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 8)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 16)))
}jack
18
2Add
func Add(ptr Pointer, len IntegerType) PointerAdd はオフセット量 len で更新された Pointer を返します。Pointer(uintptr(ptr) + uintptr(len)) と等価です。
Pointer(uintptr(ptr) + uintptr(len))例:
func main() {
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
ps := unsafe.Pointer(&s[0])
fmt.Println(*(*int)(unsafe.Add(ps, 8)))
fmt.Println(*(*int)(unsafe.Add(ps, 16)))
}2
3SliceData
func SliceData(slice []ArbitraryType) *ArbitraryTypeこの関数はスライスを受け取り、その底层配列の開始アドレスを返します。SliceData を使用しない場合、最初の要素のポインタを取得することで底层配列のアドレスを取得できます。以下のように:
func main() {
nums := []int{1, 2, 3, 4}
for p, i := unsafe.Pointer(&nums[0]), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(nums[0])), i+1 {
num := *(*int)(p)
fmt.Println(num)
}
}もちろん、reflect.SliceHeader タイプを使用して取得することもできますが、1.20 バージョン以降は非推奨となっています。SliceData はそれを置き換えるためのものです。SliceData を使用した例は以下の通りです。
func main() {
nums := []int{1, 2, 3, 4}
for p, i := unsafe.Pointer(unsafe.SliceData(nums)), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(int(0))), i+1 {
num := *(*int)(p)
fmt.Println(num)
}
}Slice
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryTypeSlice 関数はポインタと長さのオフセットを受け取り、そのメモリのスライス表現形式を返します。プロセス中にメモリコピーは発生せず、スライスを変更するとそのアドレス上のデータに直接影響します。逆も同様です。通常、SliceData と組み合わせて使用します。
func main() {
nums := []int{1, 2, 3, 4}
numsRef1 := unsafe.Slice(unsafe.SliceData(nums), len(nums))
numsRef1[0] = 2
fmt.Println(nums)
}[2 2 3 4]numsRef1 スライスのデータを変更すると、nums のデータも変化します。
StringData
func StringData(str string) *byteSliceData 関数と同様ですが、文字列をバイトスライスに変換するニーズが頻繁であるため、個別に用意されています。使用例は以下の通りです。
func main() {
str := "hello,world!"
for ptr, i := unsafe.Pointer(unsafe.StringData(str)), 0; i < len(str); ptr, i = unsafe.Add(ptr, unsafe.Sizeof(byte(0))), i+1 {
char := *(*byte)(ptr)
fmt.Println(string(char))
}
}文字列リテラルはプロセスの読み取り専用セグメントに保存されているため、ここで文字列の底层データを変更しようとすると、プログラムは fatal エラーでクラッシュします。ただし、ヒープスタックに保存された文字列変数の場合、実行時に底层データを変更することは完全に可能です。
String
func String(ptr *byte, len IntegerType) stringSlice 関数と同様で、バイトタイプポインタとその長さのオフセットを受け取り、その文字列表現形式を返します。プロセス中にメモリコピーは発生しません。以下はバイトスライスを文字列に変換する例です。
func main() {
bytes := []byte("hello world")
str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
fmt.Println(str)
}StringData と String は文字列とバイトスライスの変換プロセス中にメモリコピーを発生させず、パフォーマンスは直接タイプ変換よりも優れていますが、読み取り専用の場合にのみ適しています。データを変更する予定がある場合は、使用しない方が良いでしょう。
