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

Goのfor文で関数のリストを作ろうとして嵌った話

More than 3 years have passed since last update.

まずはこれを見てくれ。コイツをどう思う?

func main() {
   xs := []int{1, 2, 3}

   var cmds []func()
   for _, x := range xs {
      cmds = append(cmds, func() { fmt.Println(x) })

      fmt.Println("append: " + strconv.Itoa(x))
      for _, cmd := range cmds {
         fmt.Print("func eval: ")
         cmd()
      }
      fmt.Println()
   }
}

リストxsの値を利用する無名関数xsを作る。まあ、それなりにオーソドックスな処理だと思います。わたしの期待値としては、こうなるはずでした。

$ go run example_lambda.go
append: 1
func eval: 1

append: 2
func eval: 1
func eval: 2

append: 3
func eval: 1
func eval: 2
func eval: 3

しかし、現実はこうでした。

$ go run example_lambda.go
append: 1
func eval: 1

append: 2
func eval: 2
func eval: 2

append: 3
func eval: 3
func eval: 3
func eval: 3

あれ、各関数の実行結果が全部同じに!? そのループ内で触ってないのに値変わってるんだけど... ダイナミックスコープみたいな動きしてるけど、goってクロージャーだからレキシカルスコープだよね???
と混乱していたところ、@tutumingさんより

と、指摘をいただきました。
むむ? ということはXはループ毎に別の変数じゃなくて、ポインタアドレスは全部一緒ってこと?
とりあえず、アドレスを表示してみると...

go run example_lambda.go
append: 1
x addr = %!s(*int=0xc20800a200)
func eval: 1

append: 2
x addr = %!s(*int=0xc20800a200)
func eval: 2
func eval: 2

append: 3
x addr = %!s(*int=0xc20800a200)
func eval: 3
func eval: 3
func eval: 3

おお、確かに一緒だ。でも、ここまでわかれば大丈夫。Goは確か代入時は値で渡すはずだから、こんな感じに変えました。

func main() {
   xs := []int{1, 2, 3}

   var cmds []func()
   for _, x := range xs {
      tmp := x
      cmds = append(cmds, func() { fmt.Println(tmp) })

      fmt.Println("append: " + strconv.Itoa(tmp))
      fmt.Printf("x addr = %s, tmp addr = %s\n", &x, &tmp)
      for _, cmd := range cmds {
      fmt.Print("func eval: ")
        cmd()
      }
      fmt.Println()
   }
}

forループの中で宣言したtmpに一度代入して、それを使うようにします。実行結果は以下の通り。

$ go run example_lambda.go
append: 1
x addr = %!s(*int=0xc20800a200), tmp addr = %!s(*int=0xc20800a208)
func eval: 1

append: 2
x addr = %!s(*int=0xc20800a200), tmp addr = %!s(*int=0xc20800a2b8)
func eval: 1
func eval: 2

append: 3
x addr = %!s(*int=0xc20800a200), tmp addr = %!s(*int=0xc20800a3a0)
func eval: 1
func eval: 2
func eval: 3

期待通り、関数内で参照している値が変わりました。tmp変数のアドレスがx変数とは異なり毎回違うのも確認できます。

しかし、分かってしまえば何ともないかもですが、foreach文でxのアドレスが一緒なのは嵌りそうだな... もっとGo力を鍛えないとですね。

それではHappy Hacking!

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
ユーザーは見つかりませんでした