Help us understand the problem. What is going on with this article?

Goのrangeに関するよくある間違い

More than 1 year has passed since last update.

はじめに

「Go なにもわからない」というツイートを先日したところ、多くの反響を受けました。

実は、この問題は社内SlackのGoチャネルに同僚が投稿したものがもとになっています。答えは、もっとも投票も多いb bですが、どうしてそうなるのかが面白かったためツイートしたものです。

社内のGoの強い人はさすがで

    for _, i := range c {
        i := i
        b = append(b, i.f())
    }

これで直ると即答していました。

Yuguiさんもさすがで、5分しか悩んでません。

本記事は、5分以上悩んでもしっくりとこない人のための解説記事となります。

なにが問題なのか?

このコードを書いた人の意図はa bを出力することで間違いないでしょう。 実は、このコードの出力がb bとなってしまう原因は複数の箇所の組み合わせによるものです。

正しくa bが出力される直し方も一つではなく、この問題にまつわる議論が混乱します。 一箇所を直すと意図通り動くようにはなるのですが、すべての動きを理解しておかないと、将来、別の問題を起こす可能性があります。

最初の問題はrangeが返す値を受ける変数iにあります。

    for _, i := range c {
        b = append(b, i.f())
    }

普通はvとかを使うところで、ちょっと意地悪にiを使っていますが1、iはindexではなくcの値が入ります。

別の言い方をすると、iの型は値型であるaです。参照型*aやスライス[]aではありません。

    for k, i := range c {
        fmt.Printf("%T\n", i)
        b = append(b, i.f())
    }

$ go run foo.go
[]main.a
[]main.a

値型であることからiにはcの要素のコピーが代入されます。

The iteration values are assigned to the respective iteration variables as in an assignment statement.
Go言語仕様/For statementより

参照型ではないため、forの中でcを書き換えてもiに影響はありません。

    for k, i := range c {
        c[k].N = "c"
        fmt.Printf("%v\n", i)
    }
    fmt.Printf("%v\n", c)

$ go run foo.go
{a}
{b}
[{c} {c}]
b
b

iが値型だということを踏まえた上で、メソッドf()をみると。

func (s *a) f() func() {
    return func() {
        fmt.Printf("%s\n", s.N)
    }
}

メソッドf()のレシーバーsの型が*aと参照型であることに気が付いたでしょうか。sが参照型ということは、fの呼び出しでiのコピーは作られずにiへの参照が渡され、クロージャによってキャプチャされるsはiへの参照だということになります。

問題の核心に近づいてきました。呼び出し元をもう一度みてみましょう。

    for _, i := range c {
        b = append(b, i.f())
    }

このiはforをスコープとして宣言されています。しかし、その意味するところはiがループの実行ごとに用意されるという意味ではありません。iはforが実行されている限り同じものが使われます。

The iteration variables may be declared by the "range" clause using a form of short variable declaration (:=). In this case their types are set to the types of the respective iteration values and their scope is the block of the "for" statement; they are re-used in each iteration. If the iteration variables are declared outside the "for" statement, after execution their values will be those of the last iteration.
Go言語仕様/For statementより

つまり、f()はiへの参照を受け取り、それをクロージャの中へとキャプチャしていました。iへの参照はループの2回の実行において変化せず、後のクロージャの実行において最終的にiに入っていた値であるbが2回出力されたということがわかります。

次のようにfの呼び出しを除いて、直接、ループの中でiキャプチャしてみても結果がb bから変化しないことからも、iが使い回されていることが確認できます。

    for _, i := range c {
        b = append(b, func() { fmt.Printf("%s\n", i.N) })
    }

どう修正するのか

原因は分かりました。では、a bが出力されるようにするにはどうしたらいいのでしょうか。実は、直し方には大きく2つの方向性があります。

そこで続きの問題を出題してみたところ

このコードに対してa cという出力を期待する人が多いようです。気持ちとしてはcの要素へのポインターを渡したのだから(渡してませんが)、クロージャーが実行されるタイミングでの値を表示してほしいということでしょう。

一方で、f()を呼び出したタイミングでのcの要素をキャプチャしてほしいというa bを期待するという状況もあるでしょう。

a bを出力する

上記の質問においても、a bを出力するよう修正を行うことを考えます。

これはfの呼び出し時の値を出力してほしいということですから、値ではなく参照をキャプチャする実装になっているfを修正することが考えられます。

sの値*sをクロージャーの外でスタックに積み、それをキャプチャするように実装するとa bが出力されます。

func (s *a) f() func() {
    t := *s
    return func() {
        fmt.Printf("%s\n", t.N)
    }
}

また、レシーバーを値型に変えても期待通り動きます。

func (s a) f() func() {
    return func() {
        fmt.Printf("%s\n", s.N)
    }
}

スタックに積むという意味では、関数の引数でも問題ありません。

メソッドはオブジェクト自体の動作であるべきですので、この変更が受け入れられるかどうかはfはsを利用しているだけでありsの一部ではないというときでしょう。

func f(s a) func() {
    return func() {
        fmt.Printf("%s\n", s.N)
    }
}

以上の修正はすべてfの呼び出しによってiのコピーが取られるようにするものです。

一方、fは変更しないが、fの呼び出し時のcの値a bを表示したいということであれば、cの要素のコピーを明示的に作成しそのアドレスをfに渡せばいいということになります。

    for _, i := range c {
        t := i
        b = append(b, t.f())
    }

コピーは一回にしておきたいということであれば、rangeで値ではなくインデックスを受け取ることで可能です。

    for k := range c {
        t := c[k]
        b = append(b, t.f())
    }

a cを出力する

この場合は、fに問題はなさそうです。fに渡されているのがiへのポインターであって、cの要素へのポインターでないことが原因です。

cの要素へのポインターを渡せばいいわけですから、素直に書くと次のようになります。

    for k := range c {
        b = append(b, (&c[k]).f())
    }

ごちゃっとしていますが、レシーバーのポインターへの変換は自動的に行われることが規定されているため、

As with method calls, a reference to a non-interface method with a pointer receiver using an addressable value will automatically take the address of that value: t.Mp is equivalent to (&t).Mp.
Go言語仕様/Method values

    for k := range c {
        b = append(b, c[k].f())
    }

とすっきりと書けます。しかし、この仕様が混乱の元となっているような気もします。

関数形で書いた場合は、関数の引数は自動的には変換されないためポインターへの変換が必要です。

func f(s *a) func() {
    return func() {
        fmt.Printf("%s\n", s.N)
    }
}

func main() {
    b := []func(){}
    c := []a{{"a"}, {"b"}}
    for k := range c {
        b = append(b, f(&c[k]))
    }

あるいは元のデータ構造から変更が可能であれば、cを*aの配列とすれば、iは参照型になります。cがここだけでしか使われないものであればこれも可能かと思いますが、一般にはcは与えられるものでしょうか。

    c := []*a{{"a"}, {"b"}}
    for _, i := range c {
        b = append(b, i.f())
    }

まとめ

以下の点がよくある間違い2のもとになっているようです。

  1. rangeによって代入されるものは値であってポインターではない。
  2. for i, v := range c { } で作られる変数i, vはループの際に再利用される。
  3. 参照型のレシーバーに値型をおいた場合、参照型へ自動的に変換される。

最後のものはよく知られているのですが、問題は参照型だと思っていたら実は値型だったときに型エラーがでないため気づくことができないというところにあります。

多くの修正は1行だけですが、その修正の意図するところには違いがありました。適切な修正を行なうための参考になればと思います。


  1. コードレビュー受けたらvにしてくれとたぶんいいます。 

  2. 2はGoの公式Wikiでも唯一つのよくある間違いとしてあげられています。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした