Skip to content

unsafe

公式ドキュメント:unsafe package - unsafe - Go Packages

unsafe 標準ライブラリは、公式が提供する低レベルプログラミング用のライブラリです。このパッケージは Go のタイプシステムをスキップしてメモリを直接読み書きできる操作を提供します。このパッケージは移植性がない可能性があり、公式は Go 1 互換性基準の保護対象外であると明言しています。それでも、unsafe は多くのプロジェクトで広く使用されており、公式の標準ライブラリにも含まれています。

TIP

移植性がない理由は、一部の操作の結果が OS 実装に依存しており、異なるシステムでは異なる結果になる可能性があるためです。

ArbitraryType

go
type ArbitraryType int

Arbitrary は「任意の」と訳せます。ここでは任意のタイプを表し、any とは等しくありません。実際、このタイプは unsafe パッケージに属しておらず、ドキュメント目的のためにのみ記載されています。

IntegerType

go
type IntegerType int

IntegerType は任意の整数タイプを表します。実際、このタイプは unsafe パッケージに属しておらず、ドキュメント目的のためにのみ記載されています。

上記の 2 つのタイプはあまり気にする必要はありません。これらは単なる代表であり、unsafe パッケージの関数を使用する際、エディタはタイプが一致しないという警告を表示します。実際のタイプは传入された具体的なタイプです。

Sizeof

go
func Sizeof(x ArbitraryType) uintptr

変数 x のサイズをバイト単位で返します。参照コンテンツのサイズは含まれません。例:

go
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
16

Offsetof

go
func Offsetof(x ArbitraryType) uintptr

この関数は構造体内のフィールドオフセットを表します。したがって、x は構造体フィールドである必要があります。つまり、戻り値は構造体アドレス開始位置からフィールドアドレス開始位置までのバイト数です。例:

go
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
8

Alignof

メモリアライメントについてよくわからない場合は、こちらをご覧ください:Go 言語メモリアライメント详解 - 掘金 (juejin.cn)

go
func Alignof(x ArbitraryType) uintptr

アライメントサイズは通常、バイト単位のコンピュータワード長と Sizeof の最小値です。例えば amd64 マシンでは、ワード長は 64 ビット、つまり 8 バイトです。例:

go
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))
}
go
8 24
8 16
4 4

Pointer

go
type Pointer *ArbitraryType

Pointer は任意のタイプを指すことができる「ポインタ」です。そのタイプは *ArbitraryType です。このタイプは uintptr と組み合わせて使用することで、初めて unsafe パッケージの真の威力を発揮できます。公式ドキュメントの説明によると、unsafe.Pointer タイプは 4 つの特殊操作を実行できます。

  • 任意のタイプのポインタを unsafe.Pointer に変換できます
  • unsafe.Pointer を任意のタイプのポインタに変換できます
  • uintptrunsafe.Pointer に変換できます
  • unsafe.Pointeruintptr に変換できます

これら 4 つの特殊操作が unsafe パッケージ全体の基盤を構成しています。これらの操作により、タイプシステムを無視してメモリを直接読み書きするコードを作成できます。使用する際は特に注意することをお勧めします。

TIP

unsafe.Pointer は逆参照できません。同様に、アドレスを取得することもできません。

(1) *T1unsafe.Pointer に変換し、さらに *T2 に変換

タイプ *T1*T2 があるとします。T2T1 以下であり、両者のメモリレイアウトが等価である場合、T2 タイプのデータを T1 に変換できます。例:

go
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.Pointeruintptr に変換

unsafe.Pointeruintptr に変換すると、前者が指すアドレスが後者の値になります。uintptr はアドレスを保存しますが、前者は構文的にはポインタであり参照ですが、後者は単なる整数値です。例:

go
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 が保存する値は更新しないことです。例えば、以下のコードは問題を引き起こす可能性があります。

go
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

したがって、Pointeruintptr に変換した値を保存することはお勧めしません。

(3) uintptrunsafe.Pointer に変換

以下の方法で uintptr からポインタを取得できます。ポインタが有効であれば、例 2 のような無効アドレスの問題は発生しません。Pointer とタイプポインタ自体はポインタ演算をサポートしていませんが、uintptr は単なる整数値であるため、数学演算が可能です。uintptr に数学演算を実行した後、Pointer に変換することでポインタ演算を完了できます。

go
p = unsafe.Pointer(uintptr(p) + offset)

これにより、1 つのポインタのみを使用して、配列や構造体などの内部要素にアクセスできます。内部要素が外部に公開されているかどうかに関係なく、例:

go
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
2

Add

go
func Add(ptr Pointer, len IntegerType) Pointer

Add はオフセット量 len で更新された Pointer を返します。Pointer(uintptr(ptr) + uintptr(len)) と等価です。

go
Pointer(uintptr(ptr) + uintptr(len))

例:

go
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
3

SliceData

go
func SliceData(slice []ArbitraryType) *ArbitraryType

この関数はスライスを受け取り、その底层配列の開始アドレスを返します。SliceData を使用しない場合、最初の要素のポインタを取得することで底层配列のアドレスを取得できます。以下のように:

go
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 を使用した例は以下の通りです。

go
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

go
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

Slice 関数はポインタと長さのオフセットを受け取り、そのメモリのスライス表現形式を返します。プロセス中にメモリコピーは発生せず、スライスを変更するとそのアドレス上のデータに直接影響します。逆も同様です。通常、SliceData と組み合わせて使用します。

go
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

go
func StringData(str string) *byte

SliceData 関数と同様ですが、文字列をバイトスライスに変換するニーズが頻繁であるため、個別に用意されています。使用例は以下の通りです。

go
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

go
func String(ptr *byte, len IntegerType) string

Slice 関数と同様で、バイトタイプポインタとその長さのオフセットを受け取り、その文字列表現形式を返します。プロセス中にメモリコピーは発生しません。以下はバイトスライスを文字列に変換する例です。

go
func main() {
  bytes := []byte("hello world")
  str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
  fmt.Println(str)
}

StringDataString は文字列とバイトスライスの変換プロセス中にメモリコピーを発生させず、パフォーマンスは直接タイプ変換よりも優れていますが、読み取り専用の場合にのみ適しています。データを変更する予定がある場合は、使用しない方が良いでしょう。

Golang学习网由www.golangdev.cn整理维护