uintptr
型を使ったポインタ演算ができるunsafe
パッケージの危険性を理解するskeleton/stepX/main.go
にあるTODOコメントを修正し実装しますsolution/stepX/main.go
と比較して理解を深めます各ステップのコードを修正したら、以下のコマンドで動作を確認してください。
go run skeleton/stepX/main.go
このステップでは、unsafe.Pointer
を使って通常はアクセスできない非公開フィールドへのアクセス方法を学びます。Goの型安全性を回避し、任意の型のポインタを別の型のポインタに変換する手法を理解します。
unsafe
パッケージの役割unsafe
パッケージは、Goの型安全性を回避するための低レベル操作を提供します。主な用途としては次のようなものが挙げられます。
多くの場合使用を避けるべきですが、パフォーマンスが重要なケースや、cgo
を用いてC言語との相互運用時に必要となることがあります。
unsafe.Pointer
型unsafe.Pointer
は特殊なポインタ型で、次のように任意のポインタ型を相互変換が可能になります。
// 任意の型のポインタ -> unsafe.Pointer
var x int64 = 100
p := unsafe.Pointer(&x)
// unsafe.Pointer -> 任意の型のポインタ
y := (*float64)(p)
fmt.Println(*y)
import
宣言が必要な理由unsafe
パッケージが言語組み込みではなく、明示的なimport
宣言を必要とする設計には重要な意味があります。
import
宣言からunsafe
パッケージを使用しているコードを機械的に特定できるunsafe
使用を特別に可視化し、セキュリティリスクとして警告この設計により、危険な操作が暗黙的に行われることを防ぎ、コードの監査性を高めています。
unsafe
パッケージの危険性unsafe
パッケージを使用する際の主なリスクは次の通りです。
重要な概念
unsafe.Pointer
は任意の型のポインタと相互変換可能このステップでは、パッケージpkgA
の非公開フィールドを持つ構造体A
に対して、unsafe.Pointer
を使ってアクセスします。
skeleton/step1/main.go
を修正して、次の要件を満たしてください。
A
の非公開フィールドn
と同じメモリレイアウトを持つ構造体B
を定義unsafe.Pointer
を使って*A
を*B
に変換B
経由で値を設定し、A
のメソッド経由で確認unsafe.Pointer
を介さずに直接型変換できないのか説明できますか?unsafe
パッケージが言語組み込みではなく、明示的なimport
宣言を必要とする設計の利点を2つ以上挙げられますか?unsafe.Pointer
の使用を避けるべき理由を3つ以上挙げられますか?このステップでは、Step 1で学んだ構造体の型変換に対して、安全性を高める防御的プログラミングの手法を学びます。構造体のサイズが一致することをコンパイル時に検証し、メモリレイアウトの不一致によるバグを防ぐ方法を理解します。
unsafe.Pointer
型を使って異なる構造体型に変換する際、両者のメモリレイアウトが同じであることが前提となります。しかし、以下の場合にレイアウトが変わる可能性があります。
unsafe.Sizeof
関数とはunsafe.Sizeof
関数は、引数に渡された値や型のメモリ上のサイズをバイト単位で返します。
var x int32
fmt.Println(unsafe.Sizeof(x)) // 4
fmt.Println(unsafe.Sizeof(int64(0))) // 8
type Person struct {
Name string // 16バイト(64ビット環境)
Age int // 8バイト(64ビット環境)
}
fmt.Println(unsafe.Sizeof(Person{})) // 24
unsafe.Sizeof
関数の重要な特性:
// 基本型のサイズ
fmt.Println(unsafe.Sizeof(true)) // 1 (bool型)
fmt.Println(unsafe.Sizeof(uint8(0))) // 1
fmt.Println(unsafe.Sizeof(int32(0))) // 4
fmt.Println(unsafe.Sizeof(int64(0))) // 8
// ポインタのサイズ(環境依存)
var p *int
fmt.Println(unsafe.Sizeof(p)) // 8(64ビット環境)または4(32ビット環境)
unsafe.Sizeof
関数がコンパイル時定数であることを利用して、構造体のサイズの差分を検証します。
type B struct {
N int
}
// 構造体Aと構造体Bのサイズの差分を計算
const delta = int64(unsafe.Sizeof(B{})) - int64(unsafe.Sizeof(pkgA.A{}))
// deltaが0でなければ、配列サイズが負になりコンパイルエラー
var _ [-delta * delta]int
この手法のポイントは次の通りです。
-0 * 0 = 0
で配列サイズは0(問題なし)-delta * delta
は必ず負になりコンパイルエラーGo issue #74462では、golang.org/x/tools
パッケージがこのサイズチェック手法を使用していたため、Go 1.25で古いバージョンのライブラリがビルドできなくなる問題が発生しました。これはunsafe
パッケージを使用するリスクの実例です。
var a pkgA.A
// サイズチェックが成功した場合のみ、この変換が安全
b := (*B)(unsafe.Pointer(&a))
b.N = 100
fmt.Println(a.N()) // 100
重要な概念
unsafe.Sizeof
関数の結果はコンパイル時に決定されるこのステップでは、Step 1のコードにサイズチェックを追加して、より安全なコードにします。
skeleton/step2/main.go
を修正して、次の要件を満たしてください。
B
型を定義(構造体A
型の非公開フィールドと同じレイアウト)A
型と構造体B
型のサイズ差分をチェックするコンパイル時アサーションを追加unsafe.Pointer
型による変換を実行B
型のフィールドを変更して、コンパイルエラーになることを確認unsafe.Sizeof
関数がコンパイル時定数になる理由を理解していますか?-delta * delta
という式を使う理由を説明できますか?このステップでは、uintptr
型を使ったポインタ演算の危険性と、Go 1.17で導入されたunsafe.Add
関数を使った安全な方法を学びます。ポインタを数値として扱うことのリスクと、go vet
による検出について理解します。
uintptr
型とはuintptr
型は、ポインタ値を保持できる大きさの符号なし整数型です。
var x int = 42
p := &x
addr := uintptr(unsafe.Pointer(p)) // ポインタを数値に変換
fmt.Printf("アドレス: 0x%x\n", addr)
uintptr
型を使ったポインタ演算の危険性uintptr
型に変換した場合、その値はGC(ガベージコレクタ)に追跡されなくなります。
// 危険なコード
arr := []int{10, 20, 30}
base := uintptr(unsafe.Pointer(&arr[0]))
arr = nil
// この間にGCが発生すると、arrが移動する可能性がある
var _ = (*int)(unsafe.Pointer(base + unsafe.Sizeof(arr[0])))
go vet
コマンドによる検出go vet
コマンドは、unsafeptr解析器を使って、uintptr
型からunsafe.Pointer
型への変換を検出します。 なお、go vet
コマンドが検出しないケースも存在します。
$ go vet main.go
./main.go:11:17: possible misuse of unsafe.Pointer
unsafe.Add
関数Go 1.17で追加されたunsafe.Add
関数は、より安全にポインタ演算を行えます。
// 推奨される方法
arr := []int{10, 20, 30}
base := unsafe.Pointer(&arr[0])
arr = nil
second := (*int)(unsafe.Add(base, unsafe.Sizeof(arr[0])))
fmt.Println(*second) // 20
unsafe.Add
関数の利点unsafe.Add
関数を使用する際の主な利点は次の通りです。
uintptr
型変換の規則を満たす - unsafe.Pointer
型のドキュメントで定められた規則(ポインタ演算時のuintptr
型変換は同一式内で行う)を自動的に満たすuintptr
型変換の往復が不要で、一つの関数呼び出しでポインタ演算が完結重要な概念
uintptr
型は単なる整数であり、GCはポインタとして追跡しないunsafe.Add
関数は内部でポインタの妥当性を保持するこのステップでは、Goのスライスが内部的に「配列へのポインタ」「長さ」「容量」の3つのフィールドを持つ構造体として実装されていることを、unsafe
パッケージを使って確認します。
skeleton/step2/main.go
を修正して、次の要件を満たしてください。
SliceHeader
型を定義int
型のスライスを作成し、unsafe.Pointer
型を使ってSliceHeader
型として解釈unsafe.Add
関数を使って内部配列の各要素に直接アクセスlen
関数、cap
関数、各要素)と一致することを確認uintptr
型に変換するとGCに追跡されなくなる理由を説明できますか?go vet
コマンドがunsafe.Pointer
型の誤用を検出する仕組みを理解していますか?unsafe.Add
関数を使うべき場面を2つ以上挙げられますか?unsafe
パッケージとunsafe.Pointer
型unsafe.Pointer
型は任意の型のポインタと相互変換可能import
宣言により、危険な操作が可視化される設計unsafe.Sizeof
関数はコンパイル時定数として評価されるuintptr
型とポインタ演算、そしてunsafe.Add
関数uintptr
型に変換するとGCに追跡されなくなる危険性unsafe.Add
関数により安全なポインタ演算が可能unsafe
パッケージを使用しているものがないか調べてみるreflect
パッケージとunsafe
パッケージの組み合わせを理解するunsafe
パッケージが使われている例を調査する