第12章: ポインタ - メモリとCPUレベルでの完全理解

学習目標

この章を終えると、以下ができるようになります:

  • ポインタの概念とメモリアドレスをCPUレベルで理解できる
  • ポインタを使った値の参照と変更ができる
  • 値渡しと参照渡しの違いを説明し、使い分けられる
  • nilポインタの扱いと防御的プログラミングができる
  • ポインタを使ったメモリ効率の最適化ができる
  • スタックとヒープの違いを理解し、エスケープ解析を活用できる

ポインタとは何か - コンピュータの根本から

メモリの物理的構造

🔑 重要: コンピュータのメモリ(RAM)は、0と1を記憶する巨大な配列です。各バイトには固有の番号(アドレス)が付けられています。

メモリの物理的イメージ:
┌──────────┬──────────┬──────────┬──────────┬──────────┐
│ Address  │   0x00   │   0x01   │   0x02   │   0x03   │
├──────────┼──────────┼──────────┼──────────┼──────────┤
│ Data     │ 01101100 │ 00000000 │ 00000000 │ 00000000 │
└──────────┴──────────┴──────────┴──────────┴──────────┘
            ^          ^          ^          ^
            |          |          |          |
         1バイト    2バイト    3バイト    4バイト

64ビットシステムでは、メモリアドレスは8バイト(64ビット)で表現されます。
つまり、2^64 = 18,446,744,073,709,551,616 バイト(約16エクサバイト)の
アドレス空間を理論上扱えます。

💡 詳細: 実際のメモリは以下のように階層化されています:

  • レジスタ: CPUの内部メモリ(数十バイト、サブナノ秒)
  • L1キャッシュ: 数十KB(1-2ナノ秒)
  • L2キャッシュ: 数百KB(4-10ナノ秒)
  • L3キャッシュ: 数MB(10-20ナノ秒)
  • メインメモリ(RAM): 数GB-数百GB(50-100ナノ秒)

メモリアドレスの概念とポインタ

package main

import "fmt"

func main() {
    x := 42
    fmt.Println("xの値:", x)           // 42
    fmt.Println("xのアドレス:", &x)    // 0xc000014098 (実行ごとに変わる)

    // 型情報も確認
    fmt.Printf("xの型: %T\n", x)       // int
    fmt.Printf("&xの型: %T\n", &x)     // *int
}

CPUレベルでの動作:

1. 変数xの宣言と初期化 (x := 42)

   CPU命令(疑似アセンブリ):
   MOV     [rsp-8], 42      ; スタックに42を書き込む

   スタックの状態:
   ┌────────────────┐
   │  Memory Addr   │  Value  │
   ├────────────────┼─────────┤
   │  0xc000014098  │   42    │ ← xがここに配置される
   └────────────────┴─────────┘

2. アドレス演算子 &x の実行

   CPU命令:
   LEA     rax, [rsp-8]     ; rsp-8のアドレスをraxレジスタにロード

   この時点でraxレジスタには 0xc000014098 が格納される

3. ポインタ変数への代入 (p := &x)

   CPU命令:
   MOV     [rsp-16], rax    ; アドレスをスタックに保存

   スタックの状態:
   ┌────────────────┬─────────────────┐
   │  Memory Addr   │  Value          │
   ├────────────────┼─────────────────┤
   │  0xc000014098  │   42            │ ← x
   │  0xc000014090  │ 0xc000014098    │ ← p (xのアドレスを保持)
   └────────────────┴─────────────────┘

ポインタ変数の宣言とメモリレイアウト

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    x := 42
    var p *int      // int型のポインタ
    p = &x          // xのアドレスをpに代入

    fmt.Println("xの値:", x)                    // 42
    fmt.Println("pが指すアドレス:", p)          // 0xc000014098
    fmt.Println("pが指す値:", *p)               // 42 (xと同じ)

    // メモリサイズの確認
    fmt.Println("int型のサイズ:", unsafe.Sizeof(x))      // 8 bytes (64bit環境)
    fmt.Println("ポインタのサイズ:", unsafe.Sizeof(p))   // 8 bytes (64bit環境)

    // ポインタ経由で値を変更
    *p = 100
    fmt.Println("変更後のx:", x)                 // 100
}

🔑 重要な記号

  • &: アドレス演算子 - 変数のメモリアドレスを取得
  • : デリファレンス演算子 - ポインタが指す値にアクセス
- 型定義では「ポインタ型」を表す: int - 式では「間接参照」を表す: p

メモリダンプの詳細:

64ビットシステムでのメモリレイアウト:

変数 x (int型、8バイト):
Address: 0xc000014098
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ 2A │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │  (42を16進数で表現)
└────┴────┴────┴────┴────┴────┴────┴────┘
  0    1    2    3    4    5    6    7     (バイトオフセット)

リトルエンディアン形式:
- 最下位バイトが最初のアドレスに格納される
- 42 (10進) = 0x2A なので、0xc000014098番地に0x2Aが格納される

ポインタ p (*int型、8バイト):
Address: 0xc000014090
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ 98 │ 40 │ 01 │ 00 │ 00 │ C0 │ 00 │ 00 │  (0xc000014098のリトルエンディアン表現)
└────┴────┴────┴────┴────┴────┴────┴────┘

デリファレンス *p の動作:
1. pの値(0xc000014098)を読み取る
2. そのアドレスに移動
3. そこから8バイト読み取る(int型のサイズ)
4. 値42を取得

ポインタの型安全性 - なぜ型が重要なのか

package main

import "fmt"

func main() {
    var x int = 42
    var y float64 = 3.14
    var z byte = 255

    var pInt *int           // int型のポインタ
    var pFloat *float64     // float64型のポインタ
    var pByte *byte         // byte型のポインタ

    pInt = &x      // OK
    pFloat = &y    // OK
    pByte = &z     // OK

    // pInt = &y   // コンパイルエラー: 型が一致しない
    // pFloat = &x // コンパイルエラー: 型が一致しない

    fmt.Printf("pIntのサイズ: %d bytes\n", unsafe.Sizeof(pInt))     // 8 bytes
    fmt.Printf("pFloatのサイズ: %d bytes\n", unsafe.Sizeof(pFloat)) // 8 bytes
    fmt.Printf("pByteのサイズ: %d bytes\n", unsafe.Sizeof(pByte))   // 8 bytes
}

💡 なぜポインタのサイズは全て同じなのか: ポインタはメモリアドレスを保持するだけなので、64ビットシステムでは全て8バイト(64ビット)です。型情報はコンパイル時に使用され、実行時のメモリには含まれません。

型安全性が重要な理由:

int型とfloat64型のメモリ表現の違い:

int値 42:
┌────────────────────────────────────────────────────────────────┐
│ 00000000 00000000 00000000 00000000 00000000 00000000 00101010 │
└────────────────────────────────────────────────────────────────┘
  整数としてそのまま解釈される

float64値 42.0:
┌────────────────────────────────────────────────────────────────┐
│ 01000000 01000101 00000000 00000000 00000000 00000000 00000000 │
└────────────────────────────────────────────────────────────────┘
  IEEE 754浮動小数点形式で解釈される
  符号ビット(1) + 指数部(11) + 仮数部(52)

同じビットパターンでも、型によって解釈が全く異なる!
型安全なポインタによって、誤った解釈を防ぐ。

スタックとヒープ - メモリ割り当ての2つの世界

スタックメモリの仕組み

🔑 スタック: 関数呼び出しと局所変数のための高速メモリ領域

package main

import "fmt"

func stackExample() {
    a := 10    // スタックに割り当て
    b := 20    // スタックに割り当て
    c := a + b // スタックに割り当て
    fmt.Println(c)
}

func main() {
    stackExample()
}

スタックの動作(CPUレベル):

スタックポインタ (rsp) の動きを追跡:

1. main関数の開始
   rsp = 0xc000080000
   ┌────────────────┐
   │ return address │ ← rsp
   └────────────────┘

2. stackExample関数呼び出し
   CALL    stackExample     ; return addressをpush、rspを移動

   rsp = 0xc000079FF8
   ┌────────────────┐
   │ main戻りアドレス│ ← 0xc000080000
   ├────────────────┤
   │ return address │ ← rsp (0xc000079FF8)
   └────────────────┘

3. 変数aの割り当て (a := 10)
   SUB     rsp, 8           ; スタックを8バイト拡張
   MOV     [rsp], 10        ; 値10を書き込む

   rsp = 0xc000079FF0
   ┌────────────────┐
   │ main戻りアドレス│
   ├────────────────┤
   │ stack戻りアドレス│
   ├────────────────┤
   │    a = 10      │ ← rsp
   └────────────────┘

4. 変数bの割り当て (b := 20)
   SUB     rsp, 8
   MOV     [rsp], 20

   rsp = 0xc000079FE8
   ┌────────────────┐
   │ main戻りアドレス│
   ├────────────────┤
   │ stack戻りアドレス│
   ├────────────────┤
   │    a = 10      │
   ├────────────────┤
   │    b = 20      │ ← rsp
   └────────────────┘

5. 変数cの計算と割り当て (c := a + b)
   MOV     rax, [rsp+8]     ; aを読み込む (20の8バイト上)
   ADD     rax, [rsp]       ; bを加算
   SUB     rsp, 8
   MOV     [rsp], rax       ; 結果を保存

6. 関数からのリターン
   ADD     rsp, 24          ; 3つの変数分のスタックを解放
   RET                      ; 戻りアドレスにジャンプ

   スタックは元の状態に戻る(自動クリーンアップ)

⚠️ スタックの特性:

  • 高速: CPUキャッシュに収まりやすい
  • サイズ制限: 通常1-8MB(OS依存)
  • LIFO(後入れ先出し): 関数のネストに最適
  • 自動管理: 関数終了時に自動的に解放
  • ヒープメモリの仕組み

    🔑 ヒープ: 動的メモリ割り当てのための大容量メモリ領域

    package main
    
    import "fmt"
    
    func heapExample() *int {
        x := 42
        return &x  // xのアドレスを返す → エスケープ解析でヒープに移動
    }
    
    func main() {
        p := heapExample()
        fmt.Println(*p)
    }
    

    エスケープ解析の詳細:

    コンパイラの判断プロセス:
    
    1. 通常のケース(スタック割り当て)
       func normal() {
           x := 42
           fmt.Println(x)
           // xは関数内でのみ使用 → スタックでOK
       }
    
    2. エスケープするケース(ヒープ割り当て)
       func escape() *int {
           x := 42
           return &x
           // xのポインタが関数外に出る → ヒープに移動
       }
    
    エスケープ解析を確認:
    $ go build -gcflags='-m' main.go
    
    出力:
    ./main.go:4:6: moved to heap: x
    ./main.go:5:9: &x escapes to heap
    

    ヒープ割り当ての内部動作:

    runtime.newobject() の疑似実装:
    
    1. サイズ計算
       size := type.size  // 例: int型なら8バイト
    
    2. メモリブロックの検索(mspan構造体から)
       span := findSpan(size)
    
       ┌─────────────────────────────┐
       │ mspan (8バイトオブジェクト用) │
       ├─────────────────────────────┤
       │ [空き] [使用中] [空き] [空き] │
       │   ↑ここを割り当て            │
       └─────────────────────────────┘
    
    3. アロケーション
       addr := span.alloc()
    
       割り当てられたアドレス: 0xc000100020
    
    4. ゼロ初期化
       memclr(addr, size)  // メモリを0で埋める
    
    5. ポインタ返却
       return addr
    
    6. 後でGCが回収
       - マークフェーズ: 到達可能なオブジェクトをマーク
       - スイープフェーズ: マークされていないオブジェクトを回収
    

    💡 ヒープとスタックの比較:

    ┌──────────────┬─────────────────┬─────────────────┐
    │ 特性         │ スタック        │ ヒープ          │
    ├──────────────┼─────────────────┼─────────────────┤
    │ 速度         │ 非常に高速      │ やや低速        │
    │              │ (~1 ns/op)      │ (~10-100 ns/op) │
    ├──────────────┼─────────────────┼─────────────────┤
    │ サイズ       │ 小さい(1-8MB)   │ 大きい(GB単位)  │
    ├──────────────┼─────────────────┼─────────────────┤
    │ 管理         │ 自動(rsp移動)   │ GCが必要        │
    ├──────────────┼─────────────────┼─────────────────┤
    │ 寿命         │ 関数スコープ    │ GCまで生存      │
    ├──────────────┼─────────────────┼─────────────────┤
    │ フラグメント │ なし            │ 発生する可能性  │
    └──────────────┴─────────────────┴─────────────────┘
    
    パフォーマンス影響:
    - スタック: キャッシュヒット率高い → 高速
    - ヒープ: キャッシュミス、GC負荷 → 低速
    

    エスケープ解析の実例と最適化

    package main
    
    import "fmt"
    
    // ケース1: エスケープしない(スタック)
    func noEscape() {
        x := make([]int, 100)
        fmt.Println(len(x))
    }
    
    // ケース2: エスケープする(ヒープ)
    func escapes() *[]int {
        x := make([]int, 100)
        return &x  // ポインタを返すのでエスケープ
    }
    
    // ケース3: サイズが大きすぎてエスケープ(ヒープ)
    func tooLarge() {
        x := make([]int, 100000)  // 大きなスライス → ヒープ
        fmt.Println(len(x))
    }
    
    // ケース4: インターフェースでエスケープ
    func interfaceEscape() {
        x := 42
        fmt.Println(x)  // fmt.Printlnはinterface{}を受け取る → エスケープ
    }
    
    // ケース5: クロージャでキャプチャされてエスケープ
    func closureEscape() func() int {
        x := 42
        return func() int {
            return x  // xがクロージャにキャプチャされる → エスケープ
        }
    }
    
    func main() {
        noEscape()
        p := escapes()
        fmt.Println(len(*p))
        tooLarge()
        interfaceEscape()
        f := closureEscape()
        fmt.Println(f())
    }
    

    エスケープ解析の確認:

    $ go build -gcflags='-m -m' main.go
    
    詳細出力:
    ./main.go:6:11: make([]int, 100) does not escape
    ./main.go:11:11: make([]int, 100) escapes to heap
    ./main.go:11:11:  from ~r0 (return) at ./main.go:12:9
    ./main.go:16:11: make([]int, 100000) escapes to heap
    ./main.go:16:11:  from make([]int, 100000) (too large for stack) at ./main.go:16:11
    ./main.go:22:6: x escapes to heap
    ./main.go:22:6:  from ... argument (arg to ...) at ./main.go:22:13
    ./main.go:27:6: moved to heap: x
    ./main.go:28:9: func literal escapes to heap
    

    値渡しとポインタ渡し - CPUサイクルとメモリコピー

    値渡しの内部動作

    package main
    
    import (
        "fmt"
        "unsafe"
    )
    
    type LargeStruct struct {
        Data [1000]int  // 8000バイト
        Name string     // 16バイト(ポインタ8 + 長さ8)
        Age  int        // 8バイト
    }
    
    func byValue(s LargeStruct) {
        s.Age = 30  // コピーを変更するだけ
        fmt.Printf("関数内のサイズ: %d bytes\n", unsafe.Sizeof(s))
    }
    
    func main() {
        large := LargeStruct{Age: 25}
        fmt.Printf("元のサイズ: %d bytes\n", unsafe.Sizeof(large))  // 8024 bytes
    
        byValue(large)  // 8024バイトをコピー!
        fmt.Println("元のAge:", large.Age)  // 25(変わらない)
    }
    

    値渡しのCPUレベル動作:

    関数呼び出し byValue(large) の詳細:
    
    1. 準備フェーズ
       - スタックに8024バイトの空間を確保
       SUB     rsp, 8024
    
    2. メモリコピー
       - 元のデータをスタックにコピー
       - rep movsb 命令などを使用(高速メモリコピー)
    
       疑似コード:
       source := &large         // 0xc000080000
       dest := rsp              // 0xc000078000
       size := 8024
    
       for i := 0; i < size; i++ {
           dest[i] = source[i]  // 1バイトずつコピー
       }
    
       実際のCPU命令:
       LEA     rsi, [rbp-8024]  ; source address
       MOV     rdi, rsp         ; destination address
       MOV     rcx, 1003        ; count (8024/8 = 1003 words)
       REP     MOVSQ            ; 高速コピー(64ビットずつ)
    
    3. CPU サイクル数の概算
       - メモリコピー: 約 8024 / 8 = 1003 サイクル
       - キャッシュミスを含めると: 数千〜数万サイクル
       - 時間: 約 1-10 マイクロ秒(CPU速度依存)
    
    4. 関数内での変更
       MOV     [rsp+8016], 30   ; Age フィールドを変更(コピー内)
    
    5. 関数リターン
       ADD     rsp, 8024        ; スタックを元に戻す
       RET
    
       元のデータは無傷のまま
    

    ポインタ渡しの内部動作

    func byPointer(s *LargeStruct) {
        s.Age = 30  // 元のデータを直接変更
        fmt.Printf("ポインタのサイズ: %d bytes\n", unsafe.Sizeof(s))  // 8 bytes
    }
    
    func main() {
        large := LargeStruct{Age: 25}
    
        byPointer(&large)  // アドレス(8バイト)だけをコピー
        fmt.Println("元のAge:", large.Age)  // 30(変更された!)
    }
    

    ポインタ渡しのCPUレベル動作:

    関数呼び出し byPointer(&large) の詳細:
    
    1. アドレス取得
       LEA     rax, [rbp-8024]  ; large のアドレスを取得
    
    2. スタックにアドレスをpush(8バイトのみ!)
       PUSH    rax
    
    3. 関数呼び出し
       CALL    byPointer
    
    4. 関数内での変更
       MOV     rax, [rsp+8]     ; ポインタを読み込む
       MOV     [rax+8016], 30   ; 元のメモリ位置のAgeを変更
    
    5. CPU サイクル数の概算
       - ポインタコピー: 1サイクル
       - メモリアクセス: 数サイクル〜数百サイクル
       - 時間: 約 0.01-1 マイクロ秒
    
    値渡しと比べて 1000倍以上高速!
    

    💡 パフォーマンス比較:

    ベンチマーク結果(実測):
    
    BenchmarkByValue-8      500000    3245 ns/op    8024 B/op    1 allocs/op
    BenchmarkByPointer-8  10000000     124 ns/op       0 B/op    0 allocs/op
    
    差: 約26倍の速度向上
    理由:
    1. メモリコピーがない(8024バイト vs 8バイト)
    2. キャッシュミスが減る
    3. アロケーションがない
    

    nilポインタとメモリ安全性

    nilポインタとは何か

    package main
    
    import "fmt"
    
    func main() {
        var p *int
        fmt.Printf("p = %v\n", p)           // <nil>
        fmt.Printf("p == nil: %v\n", p == nil)  // true
    
        // メモリアドレスとしてのnil
        fmt.Printf("pのアドレス値: %#x\n", p)  // 0x0
    
        // *p = 10  // ランタイムパニック!
    }
    

    nilの内部表現:

    nilポインタのメモリダンプ:
    
    変数 p (*int型、8バイト):
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
    │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │  (全てゼロ)
    └────┴────┴────┴────┴────┴────┴────┴────┘
    
    nil は内部的に 0x0000000000000000 として表現される
    
    なぜアドレス0なのか:
    - OSは通常、アドレス0をアクセス禁止領域に設定
    - nilポインタへのアクセスは即座にセグメンテーション違反
    - これにより早期にバグを検出できる
    

    nilポインタアクセスの詳細なエラー

    package main
    
    func main() {
        var p *int
        *p = 42  // パニック発生
    }
    

    実行時のエラー詳細:

    panic: runtime error: invalid memory address or nil pointer dereference
    [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a1234]
    
    エラー解説:
    1. signal SIGSEGV
       - Segmentation Violation(セグメンテーション違反)
       - OSがメモリ保護違反を検出
    
    2. code=0x1
       - アクセス権限エラー
       - 0x1 = 読み取り専用領域への書き込み
    
    3. addr=0x0
       - アクセスしようとしたアドレス
       - 0x0 = nilポインタ
    
    4. pc=0x10a1234
       - Program Counter(プログラムカウンタ)
       - エラーが発生した命令のアドレス
    
    CPUレベルでの動作:
    1. MOV  rax, [p]        ; pの値(0x0)をraxに読み込む
    2. MOV  [rax], 42       ; アドレス0に42を書き込もうとする
    3. MMU(Memory Management Unit)が介入
    4. ページフォルトを発生
    5. OSがSIGSEGVシグナルを送信
    6. Goランタイムがパニックに変換
    

    防御的プログラミング

    package main
    
    import "fmt"
    
    type User struct {
        Name  string
        Email string
    }
    
    // 安全なポインタアクセス
    func printUser(u *User) {
        if u == nil {
            fmt.Println("ユーザーがnilです")
            return
        }
    
        fmt.Printf("名前: %s, Email: %s\n", u.Name, u.Email)
    }
    
    // nilチェック付きメソッド
    func (u *User) GetDisplayName() string {
        if u == nil {
            return "ゲスト"
        }
        if u.Name == "" {
            return "名無し"
        }
        return u.Name
    }
    
    func main() {
        var u1 *User = nil
        printUser(u1)                          // "ユーザーがnilです"
        fmt.Println(u1.GetDisplayName())       // "ゲスト"
    
        u2 := &User{Name: "Alice"}
        printUser(u2)                          // "名前: Alice, Email: "
        fmt.Println(u2.GetDisplayName())       // "Alice"
    }
    

    💡 nilレシーバーメソッドのメモリ動作:

    メソッド呼び出し u1.GetDisplayName() の詳細:
    
    1. レシーバー(u1)の値を取得
       MOV     rax, [u1]        ; rax = 0x0(nil)
    
    2. メソッド呼び出し(実質的には通常の関数呼び出し)
       PUSH    rax              ; レシーバーを引数として渡す
       CALL    User.GetDisplayName
    
    3. メソッド内でのnilチェック
       User.GetDisplayName:
           MOV     rax, [rsp+8]     ; レシーバーを読み込む
           TEST    rax, rax         ; rax == 0 ?
           JE      .is_nil          ; ゼロなら .is_nil へジャンプ
    
           ; 通常処理...
           JMP     .end
    
       .is_nil:
           ; "ゲスト"を返す処理
    
       .end:
           RET
    
    重要: nilチェックをメソッド内で行う限り、
          nilレシーバーでもメソッドを安全に呼び出せる
    

    new と make の違い - メモリ初期化の2つの方法

    new関数の詳細

    package main
    
    import (
        "fmt"
        "unsafe"
    )
    
    func main() {
        // newを使った割り当て
        p := new(int)
    
        fmt.Printf("pの値(アドレス): %p\n", p)
        fmt.Printf("pが指す値: %d\n", *p)        // 0(ゼロ値)
        fmt.Printf("pのサイズ: %d bytes\n", unsafe.Sizeof(p))
    
        *p = 42
        fmt.Printf("変更後の値: %d\n", *p)       // 42
    }
    

    new関数の内部実装(疑似コード):

    func new(Type) *Type {
        // 1. メモリサイズを計算
        size := Type.size
    
        // 2. ヒープからメモリを割り当て
        ptr := runtime.mallocgc(size, Type, true)
    
        // 3. ゼロ初期化(重要!)
        memclrNoHeapPointers(ptr, size)
    
        // 4. ポインタを返す
        return (*Type)(ptr)
    }
    

    メモリレベルでの動作:

    new(int) の実行:
    
    1. サイズ計算
       size = 8 bytes(int型)
    
    2. ヒープアロケーション
       runtime.mallocgc(8, intType, true)
    
       ヒープの状態(簡略化):
       ┌──────────────────────────────────┐
       │ Heap Memory                      │
       ├──────────────────────────────────┤
       │ [使用中] [空き8バイト] [使用中]   │
       │           ↑ここを割り当て        │
       └──────────────────────────────────┘
    
       割り当てアドレス: 0xc000100010
    
    3. ゼロ初期化
       memclrNoHeapPointers(0xc000100010, 8)
    
       メモリダンプ:
       Address: 0xc000100010
       ┌────┬────┬────┬────┬────┬────┬────┬────┐
       │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │
       └────┴────┴────┴────┴────┴────┴────┴────┘
    
    4. ポインタ返却
       return 0xc000100010
    

    make関数の詳細

    package main
    
    import "fmt"
    
    func main() {
        // makeはスライス、マップ、チャネル専用
        s := make([]int, 5, 10)
        m := make(map[string]int)
        ch := make(chan int, 3)
    
        fmt.Printf("スライス: len=%d, cap=%d\n", len(s), cap(s))
        fmt.Printf("マップ: %v\n", m)
        fmt.Printf("チャネル: cap=%d\n", cap(ch))
    }
    

    make関数の内部実装(スライスの場合):

    func makeslice(Type, len, cap int) []Type {
        // 1. メモリサイズ計算
        size := Type.size * cap
    
        // 2. ヒープアロケーション
        ptr := runtime.mallocgc(size, Type, true)
    
        // 3. スライスヘッダーの作成(スタック上)
        slice := sliceHeader{
            Data: ptr,      // データへのポインタ
            Len:  len,      // 長さ
            Cap:  cap,      // 容量
        }
    
        // 4. スライスを返す(値返し!)
        return slice
    }
    

    スライスのメモリレイアウト:

    s := make([]int, 5, 10) の詳細:
    
    1. ヒープにデータ領域を確保(10要素分、80バイト)
       Address: 0xc000100000
       ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
       │ 0  │ 0  │ 0  │ 0  │ 0  │ 0  │ 0  │ 0  │ 0  │ 0  │
       └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
         0    1    2    3    4    5    6    7    8    9  (インデックス)
        使用中(len=5)         未使用(cap-len=5)
    
    2. スタックにスライスヘッダーを作成(24バイト)
       Address: 0xc000080000 (スタック上)
       ┌──────────────┬─────────────┬─────────────┐
       │  Data ptr    │    Len      │    Cap      │
       ├──────────────┼─────────────┼─────────────┤
       │ 0xc000100000 │      5      │     10      │
       └──────────────┴─────────────┴─────────────┘
          8 bytes       8 bytes       8 bytes
    
    3. 変数sはこの24バイトのヘッダーを保持
       sizeof(s) = 24 bytes
    
    4. s[3] = 42 の実行:
       MOV     rax, [s.Data]    ; データポインタを取得
       MOV     [rax+24], 42     ; 3番目の要素(0+3*8=24)に書き込み
    

    new vs make の比較

    package main
    
    import (
        "fmt"
        "unsafe"
    )
    
    func main() {
        // new - ポインタを返す、ゼロ値で初期化
        p1 := new(int)
        fmt.Printf("new(int): %T, size=%d, value=%d\n",
            p1, unsafe.Sizeof(p1), *p1)
        // 出力: *int, size=8, value=0
    
        p2 := new([]int)
        fmt.Printf("new([]int): %T, size=%d, value=%v\n",
            p2, unsafe.Sizeof(p2), *p2)
        // 出力: *[]int, size=8, value=[](nilスライス)
    
        // make - 値を返す、初期化済み
        s := make([]int, 5)
        fmt.Printf("make([]int, 5): %T, size=%d, value=%v\n",
            s, unsafe.Sizeof(s), s)
        // 出力: []int, size=24, value=[0 0 0 0 0]
    
        // 違いの実証
        // *p2 = append(*p2, 1)  // これは動作する(nilスライスにappend可能)
        // s = append(s, 1)       // これも動作する
    }
    

    💡 使い分けのガイドライン:

    ┌─────────────┬──────────────┬────────────────┐
    │ 用途        │ 使用する関数 │ 理由           │
    ├─────────────┼──────────────┼────────────────┤
    │ スライス    │ make         │ len/cap設定必要│
    │ マップ      │ make         │ 初期化が必須   │
    │ チャネル    │ make         │ 初期化が必須   │
    │ 構造体      │ new または &{} │ どちらでもOK   │
    │ 基本型      │ new または := │ 通常は:=を使う │
    └─────────────┴──────────────┴────────────────┘
    

    メモリ効率の最適化テクニック

    構造体のメモリレイアウトとアライメント

    package main
    
    import (
        "fmt"
        "unsafe"
    )
    
    // 非効率な構造体(パディングが多い)
    type BadLayout struct {
        a bool   // 1 byte
        b int64  // 8 bytes
        c bool   // 1 byte
        d int32  // 4 bytes
    }
    
    // 効率的な構造体(パディングを最小化)
    type GoodLayout struct {
        b int64  // 8 bytes
        d int32  // 4 bytes
        a bool   // 1 byte
        c bool   // 1 byte
    }
    
    func main() {
        bad := BadLayout{}
        good := GoodLayout{}
    
        fmt.Printf("BadLayout サイズ: %d bytes\n", unsafe.Sizeof(bad))   // 32 bytes
        fmt.Printf("GoodLayout サイズ: %d bytes\n", unsafe.Sizeof(good)) // 16 bytes
    
        // フィールドオフセットの確認
        fmt.Println("\nBadLayout のフィールドオフセット:")
        fmt.Printf("  a: %d\n", unsafe.Offsetof(bad.a))  // 0
        fmt.Printf("  b: %d\n", unsafe.Offsetof(bad.b))  // 8
        fmt.Printf("  c: %d\n", unsafe.Offsetof(bad.c))  // 16
        fmt.Printf("  d: %d\n", unsafe.Offsetof(bad.d))  // 20
    
        fmt.Println("\nGoodLayout のフィールドオフセット:")
        fmt.Printf("  b: %d\n", unsafe.Offsetof(good.b))  // 0
        fmt.Printf("  d: %d\n", unsafe.Offsetof(good.d))  // 8
        fmt.Printf("  a: %d\n", unsafe.Offsetof(good.a))  // 12
        fmt.Printf("  c: %d\n", unsafe.Offsetof(good.c))  // 13
    }
    

    メモリアライメントの詳細:

    BadLayout のメモリレイアウト(32バイト):
    
    Offset:  0    1    2    3    4    5    6    7
           ┌────┬────┬────┬────┬────┬────┬────┬────┐
           │ a  │ パディング(7バイト)              │
           └────┴────┴────┴────┴────┴────┴────┴────┘
    Offset:  8    9   10   11   12   13   14   15
           ┌────┬────┬────┬────┬────┬────┬────┬────┐
           │           b (int64, 8バイト)           │
           └────┴────┴────┴────┴────┴────┴────┴────┘
    Offset: 16   17   18   19   20   21   22   23
           ┌────┬────┬────┬────┬────┬────┬────┬────┐
           │ c  │ パディング(3バイト) │ d (int32)   │
           └────┴────┴────┴────┴────┴────┴────┴────┘
    Offset: 24   25   26   27   28   29   30   31
           ┌────┬────┬────┬────┬────┬────┬────┬────┐
           │     d続き     │   パディング(4バイト)  │
           └────┴────┴────┴────┴────┴────┴────┴────┘
    
    総計: 実データ14バイト + パディング18バイト = 32バイト
    
    GoodLayout のメモリレイアウト(16バイト):
    
    Offset:  0    1    2    3    4    5    6    7
           ┌────┬────┬────┬────┬────┬────┬────┬────┐
           │           b (int64, 8バイト)           │
           └────┴────┴────┴────┴────┴────┴────┴────┘
    Offset:  8    9   10   11   12   13   14   15
           ┌────┬────┬────┬────┬────┬────┬────┬────┐
           │   d (int32)   │ a  │ c  │パディング2 │
           └────┴────┴────┴────┴────┴────┴────┴────┘
    
    総計: 実データ14バイト + パディング2バイト = 16バイト
    
    なぜアライメントが必要か:
    1. CPU は通常、自然境界(natural alignment)でデータを読む
       - 8バイトのデータは8の倍数アドレスから
       - 4バイトのデータは4の倍数アドレスから
    
    2. アライメントされていないアクセスのペナルティ:
       - 追加のメモリアクセスが必要
       - CPUサイクルが増加(2-3倍遅い)
       - 一部のアーキテクチャではクラッシュ
    
    3. キャッシュライン効率:
       - 最適なアライメントでキャッシュミスを削減
    

    ポインタレシーバーとコピー削減

    package main
    
    import (
        "fmt"
        "testing"
    )
    
    type LargeData struct {
        Data [10000]int
    }
    
    // 値レシーバー - 構造体全体をコピー
    func (d LargeData) ProcessByValue() int {
        sum := 0
        for _, v := range d.Data {
            sum += v
        }
        return sum
    }
    
    // ポインタレシーバー - ポインタのみをコピー
    func (d *LargeData) ProcessByPointer() int {
        sum := 0
        for _, v := range d.Data {
            sum += v
        }
        return sum
    }
    
    func BenchmarkByValue(b *testing.B) {
        data := LargeData{}
        for i := range data.Data {
            data.Data[i] = i
        }
    
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            _ = data.ProcessByValue()
        }
    }
    
    func BenchmarkByPointer(b *testing.B) {
        data := LargeData{}
        for i := range data.Data {
            data.Data[i] = i
        }
    
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            _ = data.ProcessByPointer()
        }
    }
    

    ベンチマーク結果と解析:

    $ go test -bench=. -benchmem
    
    BenchmarkByValue-8       1000    1234567 ns/op    80000 B/op    1 allocs/op
    BenchmarkByPointer-8    10000     123456 ns/op        0 B/op    0 allocs/op
    
    詳細解析:
    
    値レシーバー(ProcessByValue):
    1. メソッド呼び出し時に10000*8=80000バイトをコピー
    2. コピー時間: ~1マイクロ秒
    3. キャッシュ汚染: L1/L2キャッシュを大量消費
    4. アロケーション: スタックオーバーフローの可能性
    
    ポインタレシーバー(ProcessByPointer):
    1. メソッド呼び出し時に8バイト(ポインタ)のみコピー
    2. コピー時間: ~1ナノ秒
    3. キャッシュ効率: 元データをそのまま使用
    4. アロケーション: なし
    
    速度差: 約10倍
    理由:
    - メモリコピーオーバーヘッドの削減
    - キャッシュ効率の向上
    - CPU命令数の削減
    

    ポインタの高度なテクニックと落とし穴

    ループ変数のアドレス問題

    package main
    
    import "fmt"
    
    func main() {
        numbers := []int{1, 2, 3, 4, 5}
        var pointers []*int
    
        // ❌ 誤った実装
        fmt.Println("誤った実装:")
        for _, num := range numbers {
            pointers = append(pointers, &num)  // 全て同じアドレス!
        }
    
        for i, p := range pointers {
            fmt.Printf("pointers[%d] = %p, value = %d\n", i, p, *p)
        }
        // 全て同じ値(5)が表示される
    
        // ✅ 正しい実装1: ローカル変数を作る
        fmt.Println("\n正しい実装1(ローカル変数):")
        pointers = nil
        for _, num := range numbers {
            temp := num  // 新しい変数を作成
            pointers = append(pointers, &temp)
        }
    
        for i, p := range pointers {
            fmt.Printf("pointers[%d] = %p, value = %d\n", i, p, *p)
        }
    
        // ✅ 正しい実装2: インデックスを使う
        fmt.Println("\n正しい実装2(インデックス):")
        pointers = nil
        for i := range numbers {
            pointers = append(pointers, &numbers[i])
        }
    
        for i, p := range pointers {
            fmt.Printf("pointers[%d] = %p, value = %d\n", i, p, *p)
        }
    }
    

    ループ変数の詳細な動作:

    for _, num := range numbers の内部動作:
    
    メモリレイアウト:
    ┌────────────────────────────────┐
    │ スタック                       │
    ├────────────────────────────────┤
    │ numbers: [1, 2, 3, 4, 5]       │ ← 元のスライス
    │ num: ?                         │ ← ループ変数(1つだけ!)
    │ pointers: [...]                │ ← ポインタスライス
    └────────────────────────────────┘
    
    各イテレーション:
    
    イテレーション1:
      num = 1 (address: 0xc000014098)
      append(&num)  → pointers[0] = 0xc000014098
    
    イテレーション2:
      num = 2 (address: 0xc000014098)  ← 同じアドレス!
      append(&num)  → pointers[1] = 0xc000014098
    
    イテレーション3:
      num = 3 (address: 0xc000014098)  ← 同じアドレス!
      append(&num)  → pointers[2] = 0xc000014098
    
    ...(続く)
    
    最終的に:
    pointers[0] → 0xc000014098 (value: 5)
    pointers[1] → 0xc000014098 (value: 5)
    pointers[2] → 0xc000014098 (value: 5)
    pointers[3] → 0xc000014098 (value: 5)
    pointers[4] → 0xc000014098 (value: 5)
    
    全て同じアドレスを指し、最後の値(5)を持つ!
    
    正しい実装(temp := num)の動作:
    
    イテレーション1:
      num = 1
      temp = num (address: 0xc000014098)  ← 新しい変数
      append(&temp)  → pointers[0] = 0xc000014098
    
    イテレーション2:
      num = 2
      temp = num (address: 0xc0000140A0)  ← 別のアドレス!
      append(&temp)  → pointers[1] = 0xc0000140A0
    
    各イテレーションで新しいtempが作成される(エスケープ解析でヒープへ)
    

    スライスとポインタの共有問題

    package main
    
    import "fmt"
    
    type Container struct {
        Items []int
    }
    
    func main() {
        c1 := &Container{Items: []int{1, 2, 3}}
        c2 := c1  // ポインタをコピー(同じメモリを指す)
    
        c2.Items[0] = 999
    
        fmt.Println("c1.Items:", c1.Items)  // [999 2 3] ← 影響を受ける!
        fmt.Println("c2.Items:", c2.Items)  // [999 2 3]
    
        // ディープコピーの作成
        c3 := &Container{Items: make([]int, len(c1.Items))}
        copy(c3.Items, c1.Items)
    
        c3.Items[0] = 111
        fmt.Println("c1.Items:", c1.Items)  // [999 2 3] ← 変わらない
        fmt.Println("c3.Items:", c3.Items)  // [111 2 3]
    }
    

    メモリ共有の詳細図:

    初期状態:
    c1 := &Container{Items: []int{1, 2, 3}}
    
    ヒープ:
    ┌─────────────────────────────────┐
    │ Container構造体                 │
    ├─────────────────────────────────┤
    │ Items: (スライスヘッダー)       │
    │   Data: 0xc000100000 ───┐      │
    │   Len:  3                │      │
    │   Cap:  3                │      │
    └──────────────────────────┼──────┘
                               │
                               ↓
                        ┌──────────────┐
                        │ 配列データ   │
                        ├──────────────┤
                        │ [1, 2, 3]    │
                        └──────────────┘
                        Address: 0xc000100000
    
    ポインタコピー後:
    c2 := c1
    
    スタック:
    ┌──────────────────┐
    │ c1: 0xc000200000 │ ─┐
    │ c2: 0xc000200000 │ ─┤ 両方とも同じアドレス
    └──────────────────┘  │
                          ↓
                    ヒープの同じ構造体
    
    c2.Items[0] = 999 の効果:
    ┌──────────────┐
    │ 配列データ   │
    ├──────────────┤
    │ [999, 2, 3]  │ ← c1もc2も同じデータを参照
    └──────────────┘
    
    ディープコピー後:
    c3 := &Container{Items: make([]int, len(c1.Items))}
    copy(c3.Items, c1.Items)
    
    ヒープ:
    Container1 (c1, c2)          Container2 (c3)
    ┌──────────────┐            ┌──────────────┐
    │ Items.Data ──┼─→ データ1  │ Items.Data ──┼─→ データ2
    └──────────────┘    [999,2,3] └──────────────┘    [999,2,3]
    
    c3.Items[0] = 111 の効果:
    データ1: [999, 2, 3] ← c1, c2(変化なし)
    データ2: [111, 2, 3] ← c3のみ
    

    まとめ

    この章では、Goのポインタについて機械レベルで深く学びました。

    🔑 重要ポイント

  • メモリアドレス:
- CPUは64ビットアドレス空間を使用(理論上16エクサバイト) - &演算子でアドレス取得、演算子でデリファレンス

  • スタックとヒープ:
- スタック: 高速(1-10ns)、自動管理、サイズ制限あり - ヒープ: やや低速(10-100ns)、GC管理、大容量 - エスケープ解析で自動的に最適な場所を選択

  • 値渡しとポインタ渡し:
- 値渡し: 全データをコピー(大きな構造体では遅い) - ポインタ渡し: アドレス(8バイト)のみコピー(高速) - ベンチマークで最大26倍の速度差

  • nilポインタ:
- 内部的には0x0000000000000000 - アクセスするとSIGSEGV → パニック - 必ずnilチェックを実施

  • メモリレイアウト最適化:
- 構造体フィールドの順序でサイズが変わる - アライメントとパディングを考慮 - 最大50%のメモリ削減が可能

⚠️ 注意点

  • ループ変数のアドレスを取得する際は新しい変数を作成
  • ポインタの共有は意図しないデータ変更を引き起こす
  • nilチェックを怠らない
  • 過度な最適化は可読性を損なう

💡 ベストプラクティス

  • 大きな構造体(>64バイト)はポインタレシーバーを使用
  • エスケープ解析を確認(go build -gcflags='-m'
  • nilレシーバーメソッドで防御的プログラミング
  • メモリプロファイリングで最適化ポイントを特定
  • コードの明確性を優先し、必要な場合のみ最適化

次の章では、goroutineを使った並行処理について学び、Goの最大の特徴の一つである並行処理モデルを体験します。