29
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go言語】for文の奇妙な動作

Last updated at Posted at 2024-01-25

奇妙な動作をするfor文

まず、最初に奇妙な動作をするコードの例を出します。
以下のコードでは、for文の中でgoroutineを実行している。 main のgoroutineが終了するのを防ぐために sync.WaitGroup を使用している。 (sync.WaitGroupを知らない人はGoのWaitGroupを理解するを参考)

func main() {
	var wg = &sync.WaitGroup{}
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			fmt.Println(i)
			wg.Done()
		}()
	}
	wg.Wait()
}

Go Playground

これを実行すると、

実行結果
10
10
10
10
・・・・
10
10

i < 9 と設定しているので、出力されるのは9以下であるはずである。にもかかわらず、10が出力されて、ループは続行する。

ちなみに、JavaScript でも似たような事象を確認できる。以下コードでは、 setTimeout()という非同期メソッドを使っている。

for (var i = 0; i < 10; i++) {
  setTimeout(() => console.log(i))
}
実行結果
10
10
10
10
・・・・
10
10

解説

for文は、以下 3 つの主要なコンポーネントで構成される。

  • キーワードfor
  • たとえばi := 0; i < foo; i++のようなステートメント
  • ループする処理{}の中身

goroutineが生成されると、各ループでのスケジューリングのためにキューに入れられる。
一方、for文はgoroutineがスケジュールされて、実行される前に終了する。
for文の最後の実行では、forのステートメント自身はいつ停止する必かが分からない。したがって、i=9 のときでも、インクリメントされる。そして、i=10になると、for文のi < 10の結果が false になり、ループによる本体の実行が停止される。(下図参照)

image.png

この奇妙な動作は、Go言語だけではなく、C言語から派生したすべてのプログラミング言語に当てはまる。

Golang では、インデックス変数はポインタとして渡される。

先ほどの、JavaScriptの例では、古いvar宣言を使用していた。これは、Golang の var と同様にインデックス変数はポインタとして扱われる。
それに対して、letはループするごとに変数に更新された値をコピーする。(これをシャドーイングと呼ぶ)

以下はletで直した例である。

- for (var i = 0; i < 10; i++) {
+ for (let i = 0; i < 10; i++) {
  setTimeout(() => console.log(i))
}

setTimeout()は非同期であるが、マルチスレッドでは動かないので、0から順番に出力される。

実行結果
0
1
2
3
4
5
6
7
8
9

Golangでは、以下のようにインデックス変数を goroutine の引数に渡すと解決できる。

var wg = &sync.WaitGroup{}
for i := 0; i < 10; i++ {
	wg.Add(1)
	go func(i int) {
		fmt.Print(i)
		wg.Done()
	}(i)
}
wg.Wait()

Go Playground

並行処理なので、順番は不順だが、0 ~ 9 までの数値が以下のように出力される。

実行結果
9
4
0
1
2
3
7
5
8
6

また、シャドーイングを使用しても、同じことができる。(mattnさんからコメントいただきました。ありがとうございます。)

func main() {
	var wg = &sync.WaitGroup{}
	for i := 0; i < 10; i++ {
+		i := i  // 外側のiをシャドーイング
		wg.Add(1)
		go func() {
			fmt.Println(i)
			wg.Done()
		}()
	}
	wg.Wait()
}

Go Playground

終わりに

Goの v1.22 でこのGolangの振る舞いについて、改善しつつあるらしいです。

v1.22で最初と同じ以下のコードを実行すると

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg = &sync.WaitGroup{}
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			fmt.Println(i)
			wg.Done()
		}()
	}
	wg.Wait()
}
9
5
6
4
2
3
7
8
0
1

となる

29
23
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
29
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?