たんぶろぐ

すぐ忘れる

Go の備忘録 (range とポインタ編)

Go の range は変数を宣言するので、その値をポインタで渡したかったら対象スライスをインデックス参照して渡してねという話です。

今見ると、それはそう、となるのですがアウトプットするくせがないので、こういった小さいところから書いていこうと思います。

本編

こういうコードがありますよね。Goroutine である processor にチャネルを介して値を渡し、処理してもらおうという寸法です。構造体は大きい (予定) のでポインタで渡すことにします。

package main

import (
    "fmt"
    "sync"
)

type A struct {
    n int
}

func main() {
    as := []A{
        {n: 0},
        {n: 1},
        {n: 2},
        {n: 3},
        {n: 4},
    }

    q := make(chan *A)
    wg := &sync.WaitGroup{}
    wg.Add(1)

    go processor(q, wg)

    for _, a := range as {
        q <- &a
    }

    wg.Wait()
}

func processor(q <-chan *A, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 5; i++ {
        a := <-q
        fmt.Println(a.n)
    }
}

さあ、実行してみましょう。感覚で読むと 0, 1, 2, 3, 4 と出力されるような気がします。

0
2
2
4
4

あれっ、あれっ。なにかおかしい。とりあえず同じ構造体を何度か見ているようなのでポインタを合わせて出力させてみます。

Sent: A{n: 0} 0x41402c
Got : A{n: 0} 0x41402c
Sent: A{n: 1} 0x41402c
Sent: A{n: 2} 0x41402c
Got : A{n: 2} 0x41402c
Got : A{n: 2} 0x41402c
Sent: A{n: 3} 0x41402c
Sent: A{n: 4} 0x41402c
Got : A{n: 4} 0x41402c
Got : A{n: 4} 0x41402c

んん?なんで同じポインタ?

僕は答えを知っているのでもう書いてしまいますが、犯人はここなんですね。分かってる方は「あたりめーだろ!」とツッコミを入れてる頃だと思いますが許してください。

for _, a := range as {
    q <- &a
}

range は a という変数を定義して、それを as の各要素を入れるのに使い回すため a は常に同一オブジェクトなわけです。そのため、同じポインタがチャネルに入れられており、range のループの進捗によって参照する値が書き換えられていただけなのです。上の出力を見るとその様子がよくわかりますね。

つまり意図した挙動は次のように書きます。range の Value を定義せずに、Index のみを定義してスライスをインデックス参照するようにします。

for i, _ := range as {
    q <- &as[i]
}

わかってしまったらもう間違えようがないのですが、こういうところは気をつけていきたいですね。

以上、ありがとうございました。