Zenn
💬

Go の range で発生しがちなポインタの問題

2025/01/07に公開

はじめに

Go 言語の range 構文を使用したスライス操作は非常に便利であるが、特にポインタを扱う場合、意図しない挙動が発生することがある。
この問題は、プログラミング初心者や中級者にとって見落としやすく、バグにつながるケースが多いと感じたためメモ ✍️。

本記事では、以下の内容を解説する。

  • range の内部動作を説明
  • 問題が発生する理由
  • 問題を回避する正しいコードの実装方法

問題の概要

次のコードを例として挙げる。

type Product struct {
    ID    string
    Price float64
}

type Inventory struct {
    items map[string]*Product
}

func (inv *Inventory) addProducts(products []Product) {
    for _, product := range products {
        inv.items[product.ID] = &product // マップにポインタを保存
    }
}

このコードでは、スライス内の各 Product をループし、そのポインタをマップに保存しようとしている。しかし、期待通りの動作にはならない。

意図した動作

以下のスライスを入力として渡した場合:

[]Product{
    {ID: "101", Price: 100.0},
    {ID: "102", Price: 200.0},
    {ID: "103", Price: 300.0},
}

マップには次のように各要素のポインタが保存されることを期待する。

  • key: "101" → ポインタが{ID: "101", Price: 100.0}を指す
  • key: "102" → ポインタが{ID: "102", Price: 200.0}を指す
  • key: "103" → ポインタが{ID: "103", Price: 300.0}を指す

実際の動作

しかし、実際には以下のような動作となる。

  • key: "101" → ポインタが{ID: "103", Price: 300.0}を指す
  • key: "102" → ポインタが{ID: "103", Price: 300.0}を指す
  • key: "103" → ポインタが{ID: "103", Price: 300.0}を指す

全てのエントリが最後のスライス要素を指す結果となる。

問題の原因

range の挙動

Go の range 構文では、スライスをループ処理する際に以下の動作を行う。

  1. 各要素を保持するための一時変数を内部的に使用する。
  2. この一時変数(例: product)のメモリアドレスは固定されており、ループ全体を通じて使い回される。
  3. ループごとにスライス要素がこの変数に上書きされるが、変数自体のアドレスは変わらない。

その結果、マップに保存されるポインタは全て同じアドレスを指し、最後に処理された値がすべてのエントリに反映される。

アドレスの確認

次のコードを実行して、アドレスを確認する。

func (inv *Inventory) addProducts(products []Product) {
    for _, product := range products {
        fmt.Printf("%p\n", &product) // アドレスを出力
        inv.items[product.ID] = &product
    }
}

出力例:

0xc000096020
0xc000096020
0xc000096020

アドレスがすべて同じであることが確認できる。

解決策

この問題を解決するには、ループごとに新しい変数を作成し、その変数のポインタを保存する必要がある。

修正コード

以下は正しい実装例である。

func (inv *Inventory) addProducts(products []Product) {
    for i := range products {
        product := products[i] // 新しい変数を作成
        inv.items[product.ID] = &product
    }
}

このコードでは、スライスから要素を取り出すたびに新しい product 変数を作成し、そのポインタを保存している。

修正後のアドレス確認

修正後のコードで再びアドレスを確認する。

func (inv *Inventory) addProducts(products []Product) {
    for i := range products {
        product := products[i]
        fmt.Printf("%p\n", &product)
        inv.items[product.ID] = &product
    }
}

出力例:

0xc000096020
0xc000096030
0xc000096040

ループごとに異なるアドレスが割り当てられていることが確認できる。

他の類似ケース:Goroutine での range

同様の問題は、Goroutine で range を使用する場合にも発生する。

問題例

for _, v := range []int{1, 2, 3} {
    go func() {
        fmt.Println(v) // 全てのGoroutineで最後の値が出力される
    }()
}

修正例

for _, v := range []int{1, 2, 3} {
    v := v // 新しい変数を作成
    go func() {
        fmt.Println(v)
    }()
}

まとめ

  • range 構文では、ループ変数のメモリアドレスが固定されているため、ポインタを扱う際に注意が必要。
  • 問題を回避するためには、新しい変数を作成してポインタを保存する方法が有効。
  • こうすることで、Go における典型的なバグを回避し、安定したコードを実装できる。

参考

GitHubで編集を提案

Discussion

ログインするとコメントできます