🕌

なんとなく使っている配列(スライス)とポインタとは?

に公開

Goスライス定義パターンと性能比較

背景

コードレビューをしていると、Goで配列(正確にはスライス)を定義する際に書き方がバラバラなケースをよく見かけます。
今回のドキュメントでは、代表的な3パターンの違いと、実際の挙動・性能差を確認するベンチマーク結果をまとめます。

サンプルコード一式GitHubリポジトリ を参照してください。


基本構造と3パターン

パターンA: []User(値スライス / ValueSlice)

  • Goのデフォルトで最も一般的な形
  • スライスは参照型なので、関数に渡しても参照先は共有されるが、スライス自体のコピーは軽量
  • nilスライスと空スライスを区別する必要がない場合に推奨
type Hoge struct {
    Users []User
}

パターンB: *[]User(スライスポインタ / SlicePointer)

  • スライス全体へのポインタを持つ
  • nilスライス / 空スライス / 要素あり を厳密に区別したいときに有効
  • 性能メリットは薄く、可読性を落とす可能性あり
type Fuga struct {
    Users *[]User
}

パターンC: []*User(要素がポインタ / ElementPointerSlice)

  • 各要素をポインタで保持
  • 大きな構造体のコピーを避けたいときや共有更新が必要な場合に有効
  • 副作用管理が難しくなるため、設計時に注意が必要
type Piyo struct {
    Users []*User
}

比較表

パターン メリット デメリット 主な用途
A: []User(値スライス) 可読性高い / デフォルトの選択肢 大きな構造体コピーはコスト増 一般的なコレクション保持
B: *[]User(スライスポインタ) nil/空/要素ありの明確区別 性能面の恩恵少 / 可読性低下 厳密な状態管理が必要な場合
C: []*User(要素がポインタ) 大きな構造体コピー回避 / 共有更新可能 副作用管理が複雑 / nil管理が必要 大規模データ共有 / 更新処理

実行例

main.go の出力例

% go run .
=== 挙動デモ ===

[A] 値スライス
nilスライス (omitempty適用)    => {}
空スライス ([])               => {}
要素あり                      => {"users":[{"ID":1,"Name":"Alice","Age":20,"Email":"a@example.com","City":"Sendai"}]}
a.Users[0].Name="Alice", a2.Users[0].Name="Changed in a2"

[B] スライスポインタ
nilポインタ(キー省略)        => {}
空スライス ([])               => {"users":[]}
要素あり                      => {"users":[{"ID":2,"Name":"Bob","Age":30,"Email":"b@example.com","City":"Kanazawa"}]}
B共有性確認: Bob-Updated

[C] 要素がポインタ
c.Users[0].Name="Carol-Shared", other.Users[0].Name="Carol-Shared"
Cの2要素目はnil: true

ベンチマーク

ベンチマーク項目

bench_test.go より

  • Iterate(値スライス / 要素がポインタ)
  • Copy(値スライス / 要素がポインタ)
  • Update(値スライス / 要素がポインタ)
  • JSONマーシャル(JSON Marshal)(値スライス / 要素がポインタ)
  • DTO変換(DTO Transform)(値スライス / 要素がポインタ)
  • Filter(値スライス / 要素がポインタ)
  • Sort(値スライス / 要素がポインタ)
  • GroupByCity(値スライス / 要素がポインタ)
  • JSON Lines(行区切りJSON)(値スライス / 要素がポインタ)

ベンチマーク総評

  1. 走査(Iterate)
    要素がポインタの方が約20%高速(37.4µs → 29.6µs)
    要素がポインタなのでループ時のコピーコストが低い
    構造体が小さい場合は差が縮まる

  2. コピー(Copy)
    要素がポインタの方が圧倒的に速く、アロケーションも大幅減(6.4MB → 0.8MB)
    値スライスは全構造体を複製、要素がポインタの場合は参照だけ移すため軽量

  3. 更新(Update)
    双方ほぼ同等(ns/opレベル)
    値スライスは独立性あり、副作用なし
    要素がポインタの場合は共有更新となり副作用に注意

  4. JSONマーシャル(Marshal)
    両者ほぼ同等
    エンコード処理が支配的で差は小さい

  5. DTO変換(DTO Transform)
    要素がポインタの方が若干速い(743µs → 699µs)
    メモリ確保量は同じ(変換先スライスの新規確保)

  6. フィルタ(Filter)
    要素がポインタの方が約3倍高速&メモリ消費も大幅減(1.16MB → 0.15MB)
    値スライスは一致要素をコピーするためコスト増

  7. ソート(Sort)
    双方ほぼ同等(要素がポインタの方がわずかに軽量)

  8. グルーピング(GroupByCity)
    要素がポインタの方が約2.3倍高速&メモリ大幅減(11.6MB → 1.5MB)
    値スライスは要素コピー、要素がポインタの場合は参照追加だけ

  9. JSON Lines(行区切りJSON)
    要素がポインタの方がわずかに速く、メモリも減少
    I/Oバウンド寄りで差は限定的


判断フロー(おすすめ)

  1. 特別な理由がなければ A: 値スライス([]User
  2. 要素の共有更新や大きな構造体コピー回避が必要 → C: 要素がポインタ([]*User
  3. nil/空/要素ありを厳密に区別する必要がある → B: スライスポインタ(*[]User

付録A:なぜスライスポインタのベンチを入れていないのか

スライスポインタ(*[]User)は内部的にスライス構造体(ヘッダ部分)をポインタで参照するだけで、要素アクセスやコピー時の挙動は値スライス([]User)とほぼ同一です。
そのため、ベンチマーク項目を増やしても数値的な傾向は値スライスと同じになるため、比較対象からは除外しました。

付録B:副作用管理とnil管理のサンプル(要素がポインタ)

対象: 「要素がポインタ([]*User)」で起こりやすい副作用とnil混入の落とし穴、および安全策。
実行: リポジトリに examples/side_effects_and_nil/main.go を配置して、以下を実行します。

go run ./examples/side_effects_and_nil

何が分かるか

  1. 共有参照の副作用:フィルタ後の更新が元スライスへ波及(浅いコピーの罠)。
  2. nil要素の危険:アクセス/ソート/JSON出力でのクラッシュやnull混入。
  3. 安全策:防衛的ディープコピー、nil除去(compact)、JSON直前での値スライス化、レイヤ境界での独立化。

主要抜粋(要点のみ)

// 浅いコピーだと共有参照のまま(副作用が伝播)
shallow := append([]*User(nil), ptrs...)
shallow[1].City = "Tokyo" // -> ptrs[1].City も "Tokyo" になる

// nil混入に配慮したソート(nilは末尾へ)
sort.Slice(ptrs, func(i, j int) bool {
    ui, uj := ptrs[i], ptrs[j]
    switch {
    case ui == nil && uj == nil:
        return false
    case ui == nil:
        return false
    case uj == nil:
        return true
    default:
        return ui.Name < uj.Name
    }
})

// 防衛的ディープコピーで独立させる
func filterPtrDeepCopy(ps []*User, pred func(*User) bool) []*User {
    out := make([]*User, 0, len(ps))
    for _, p := range ps {
        if p == nil || !pred(p) { continue }
        cp := *p
        out = append(out, &cp)
    }
    return out
}

完全版ソース: examples/side_effects_and_nil/main.go を参照してください。

Discussion