1. 各ステップの「学習内容」を読み、概念を理解します
  2. skeleton/stepX/main.go にあるTODOコメントを修正し実装します
  3. 修正したコードを実行します
  4. 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宣言を必要とする設計には重要な意味があります。

この設計により、危険な操作が暗黙的に行われることを防ぎ、コードの監査性を高めています。

unsafeパッケージの危険性

unsafeパッケージを使用する際の主なリスクは次の通りです。

  1. 型安全性の喪失 - 誤った型変換によるメモリ破壊
  2. プラットフォーム依存 - メモリレイアウトがアーキテクチャによって異なる場合がある
  3. Goランタイムとの非互換 - GC(ガベージコレクタ)やスタック移動により予期しない動作
  4. 将来の互換性なし - Goのバージョンアップで動作しなくなる可能性

重要な概念

実装タスク

このステップでは、パッケージpkgAの非公開フィールドを持つ構造体Aに対して、unsafe.Pointerを使ってアクセスします。

skeleton/step1/main.goを修正して、次の要件を満たしてください。

  1. 構造体Aの非公開フィールドnと同じメモリレイアウトを持つ構造体Bを定義
  2. unsafe.Pointerを使って*A*Bに変換
  3. 構造体B経由で値を設定し、Aのメソッド経由で確認

理解度チェック

学習内容

このステップでは、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関数の重要な特性:

  1. コンパイル時定数 - 結果はコンパイル時に決定され、定数として扱える
  2. 型のサイズを返す - 実際の値の内容ではなく、型自体のサイズ
  3. パディングを含む - 構造体の場合、アライメント用のパディングも含む
  4. 参照型は固定サイズ - スライスや文字列は内部構造のサイズが返される(要素数に依存しない)
// 基本型のサイズ
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

この手法のポイントは次の通りです。

  1. 差分が0の場合、-0 * 0 = 0で配列サイズは0(問題なし)
  2. 差分が非0の場合、-delta * deltaは必ず負になりコンパイルエラー
  3. 実行時ではなくコンパイル時に問題を検出できる

実際に起きた問題

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

重要な概念

実装タスク

このステップでは、Step 1のコードにサイズチェックを追加して、より安全なコードにします。

skeleton/step2/main.goを修正して、次の要件を満たしてください。

  1. Step 1と同じく、構造体B型を定義(構造体A型の非公開フィールドと同じレイアウト)
  2. 構造体A型と構造体B型のサイズ差分をチェックするコンパイル時アサーションを追加
  3. サイズが一致した場合のみ、unsafe.Pointer型による変換を実行
  4. 構造体B型のフィールドを変更して、コンパイルエラーになることを確認

理解度チェック

学習内容

このステップでは、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関数を使用する際の主な利点は次の通りです。

  1. uintptr型変換の規則を満たす - unsafe.Pointer型のドキュメントで定められた規則(ポインタ演算時のuintptr型変換は同一式内で行う)を自動的に満たす
  2. コードの簡潔性 - uintptr型変換の往復が不要で、一つの関数呼び出しでポインタ演算が完結
  3. 意図が明確 - ポインタに対してオフセットを加算する操作であることが関数名から明示的

重要な概念

実装タスク

このステップでは、Goのスライスが内部的に「配列へのポインタ」「長さ」「容量」の3つのフィールドを持つ構造体として実装されていることを、unsafeパッケージを使って確認します。

skeleton/step2/main.goを修正して、次の要件を満たしてください。

  1. スライスの内部構造を表す構造体SliceHeader型を定義
  2. int型のスライスを作成し、unsafe.Pointer型を使ってSliceHeader型として解釈
  3. unsafe.Add関数を使って内部配列の各要素に直接アクセス
  4. 通常のスライス操作で得られる値(len関数、cap関数、各要素)と一致することを確認

理解度チェック

学んだ概念

Step 1: unsafeパッケージとunsafe.Pointer

Step 2: 構造体サイズのコンパイル時検証

Step 3: uintptr型とポインタ演算、そしてunsafe.Add関数

次のステップ

  1. 開発しているGoのプロジェクトでunsafeパッケージを使用しているものがないか調べてみる
  2. reflectパッケージとunsafeパッケージの組み合わせを理解する
  3. 実際のプロダクションコードでunsafeパッケージが使われている例を調査する