LoginSignup
24
24

More than 5 years have passed since last update.

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

Posted at

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

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!

24
24
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
24