A Tour of Go やってみた Part5
前回
「A Tour of Go」の"Methods and interfaces" までを終わらせたので、今回は "Concurrency" をやる
1. Goroutines
**goroutine(ゴルーチン)**はGoランタイムで使える軽量スレッドのこと。goではgoroutineを使って、並行処理を行う。
以下のように関数の前にgo
をつければ、
go f(x, y, z)
f(x, y, z)
が「新しい」goroutineとして実行される。なお「新しい」とあるのは、実行元もgoroutineだからで、関数の評価などは実行元のgoroutineで行われる。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("世界!")
say("こんにちは!")
}
こんにちは!
世界!
世界!
こんにちは!
こんにちは!
世界!
こんにちは!
世界!
世界!
こんにちは!
なお、gorutineは同じメモリアドレス空間で実行される。つまり共有メモリにアクセスする場合は同期が必要になり、このためにsync
パッケージが用意されていて、これを使うことで排他制御や他のgoroutineを待つようなことができる。ただし、実際にはGo二は別の手法があるため、これを使うことは少ないらしい。それは次で。
2. Channels
チャネル(Channel
)型 を使うと、goroutine同士でデータをやりとりすることができる。基本的な使い方は以下の通り。
int
型のチャネルを作成
ch := make(chan int)
チャネルch
に42
を送る
ch <- 42
チャネルから値を受け取る
v := <=ch
チャネルの特徴として、デフォルトでは、送信・受信は相手側の準備が揃うまでブロックされるため、明示的なロックや条件変数を用意しなくても、同期されることになる。
package main
import "fmt"
// チャネルを使って、2つのgoroutineで配列の合計を計算する
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // チャネルに合計を送信
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) // チャネルを作成
go sum(s[:len(s)/2], c) // 前半の要素をsum関数に渡す
go sum(s[len(s)/2:], c) // 後半の要素をsum関数に渡す
x, y := <-c, <-c // チャネルから合計を受信
fmt.Println(x, y, x+y)
}
-5 17 12
なるほど、goroutineの実行順は保証されないみたい。あと、同じチャネルに複数から受信されているけど、キューっぽい感じで取り出せるのね
3. Buffered Channels
ここまでの使い方だとチャネルは送信・受信が両方揃わないとブロックされていた。バッファ付きチャネルにすると、チャネルに一時的に値をためておくことができる。
バッファ付きチャネルは以下のようにmake
の引数でバッファの長さを指定することで初期化できる。
ch := make(chan int, 3) // int型、容量3のバッファ付きチャネル
上記の場合、受信側がなくても3つまではブロックされることなく送信ができる。ただしバッファがたまり切るとブロックされる。
例えば以下のように書くと・・・
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
ch <- 4
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
fatal error: all goroutines are asleep - deadlock!
なるほど、すべてのgoroutineがチャネル送受信で止まっている=デッドロック、ということなのだろう。
余談だが、この状態はバッファがなくても発生する。
package main
func main() {
ch := make(chan int)
ch <- 1
}
チャネルの受信側がいないたため、デッドロックということになる。
4. Range and Close
チャネルに値を送信する側が、送信する値がないことを宣言するために使うのがclose
。
close(c)
受信側は受信時に2つの値を受け取るようにすると、チャネルがクローズされているかを確認できる。
v, ok <- c
上記だと、受信する値がないか、もしくは、チャネルがクローズされている場合は ok
に false
が返るのでこれを使えば良い。
例えばこんな感じ
package main
import "fmt"
func main() {
ch := make(chan int)
// チャネルに値を送信する無名関数をgoroutineで実行
go func() {
ch <- 1
ch <- 2
ch <- 3
close(ch)
}()
// チャネルから値を無限ループで受信し、チャネルが閉じられたら終了
for {
v, ok := <-ch
if !ok {
fmt.Println("チャネルが閉じられました!")
break
}
fmt.Println("受信した値:", v)
}
}
受信した値: 1
受信した値: 2
受信した値: 3
チャネルが閉じられました!
range
と組み合わせると以下のような書き方ができる。
for i := range ch {
fmt.Println(v)
}
これで、受信時に結果をチェックする必要もないし、チャネルが閉じられるまで自動で受信、チャネルがクローズされたらループを抜けるということになる。
package main
import "fmt"
// フィボナッチ数列を生成し、チャネルに送信する関数
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
ch := make(chan int, 10)
// 上記の関数をgoroutineで実行
go fibonacci(cap(ch), ch)
// チャネルから値を受信し、チャネルが閉じられたら終了
for i := range ch {
fmt.Println(i)
}
}
0
1
1
2
3
5
8
13
21
34
ただし以下には注意
- 受信側が
close()
するとpanic
になるので、close()
するのは送信側のみ - チャネルは基本的に
close()
する必要はない。ただし、受信側がもう値が来ないことを知る必要がある場合には必要になる。具体的には-
range
ループでチャネルから値を受け取っている場合。この場合は必ずclose
が必要。なければデッドロックで止まる。
-
5. Select
select
を使うと、複数のチャネル操作を、どれが1つが準備できるまで待たせることができる。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(time.Second * 10) // 10秒後にバナナを送信
ch2 <- "バナナ"
}()
go func() {
time.Sleep(time.Second * 5) // 5秒後にリンゴを送信
ch1 <- "リンゴ"
}()
fmt.Println(time.Now(), ": 開始")
// selectで先に受信できたものを処理
select {
case v := <-ch1:
fmt.Println(time.Now(), ":", v, "が届きました")
case v := <-ch2:
fmt.Println(time.Now(), ":", v, "が届きました")
}
}
2025-07-06 05:25:24.29732 +0900 JST m=+0.000173793 : 開始
2025-07-06 05:25:29.298537 +0900 JST m=+5.001376126 : リンゴ が届きました
caseの準備ができるまではブロックされ、準備ができたcaseから実行される。今回の例だとリンゴのほうが先に必ず実行される。
なお、どちらのcaseも準備できている場合はランダムとなる。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "バナナ"
}()
go func() {
ch1 <- "リンゴ"
}()
fmt.Println(time.Now(), ": 開始")
select {
case v := <-ch1:
fmt.Println(time.Now(), ":", v, "が届きました")
case v := <-ch2:
fmt.Println(time.Now(), ":", v, "が届きました")
}
}
2025-07-06 05:27:57.972439 +0900 JST m=+0.000035959 : 開始
2025-07-06 05:27:57.972521 +0900 JST m=+0.000117876 : バナナ が届きました
2025-07-06 05:27:58.676206 +0900 JST m=+0.000041126 : 開始
2025-07-06 05:27:58.676289 +0900 JST m=+0.000124043 : バナナ が届きました
2025-07-06 05:27:59.410704 +0900 JST m=+0.000029876 : 開始
2025-07-06 05:27:59.410783 +0900 JST m=+0.000108085 : リンゴ が届きました
2025-07-06 05:28:00.117503 +0900 JST m=+0.000026334 : 開始
2025-07-06 05:28:00.117572 +0900 JST m=+0.000094959 : リンゴ が届きました
サンプルで紹介されているコードも。
package main
import "fmt"
// チャネルcに、フィボナッチ数列を生成して送信
// チャネルquitが閉じられたら終了
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit") // チャネルが閉じられたら終了
return
}
}
}
func main() {
ch := make(chan int)
quit := make(chan int)
// goroutineでchチャネルを受信して、
// 10個受信したらquitチャネルを送信
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
quit <- 0
}()
// フィボナッチ数列を生成
fibonacci(ch, quit)
}
0
1
1
2
3
5
8
13
21
34
quit
6. Default Selection
select
ではどのケースにも該当しない場合はdefault
が実行されるが、これを使うと、どのcase
のチャネルも準備ができていない場合に、非ブロッキングな処理ができる。
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("ピッ...")
case <-boom:
fmt.Println("ポーン!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
.
.
ピッ...
.
.
ピッ...
.
.
ピッ...
.
.
ピッ...
.
.
ピッ...
ポーン!
7&8 Exercise: Equivalent Binary Trees
スキップ
9. sync.Mutex
mutexはmutual exclusion(相互排他)の略で、1つの変数に複数のgoroutineが同時にアクセスしないようにすることができる。
Goにはsync.Mutex
が用意されており、以下の2つのメソッドが用意されている。
-
Lock()
: 他のgoroutineは待機する -
Unlock()
: 他のgoroutineが続けることができる
これらで囲めば排他制御ができる。
また、deferを使えば必ず解除されることが保証される。
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounterは、並行処理で安全にアクセスできるカウンター
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Incは、指定されたキーのカウンターをインクリメント
func (c *SafeCounter) Inc(key string) {
// ロック→1つのgoroutineだけがmap c.vにアクセスできるようになる
c.mu.Lock()
c.v[key]++
// アンロック→他のgoroutineがmap c.vにアクセスできるようになる
c.mu.Unlock()
}
// Valueは、指定されたキーのカウンターの現在の値を返す
func (c *SafeCounter) Value(key string) int {
// ロック→1つのgoroutineだけがmap c.vにアクセスできるようになる
c.mu.Lock()
// アンロック→他のgoroutineがmap c.vにアクセスできるようになる
defer c.mu.Unlock() // deferでアンロックを保証する
return c.v[key]
}
func main() {
// カウンターの初期化
c := SafeCounter{v: make(map[string]int)}
// 1000個のgoroutineでカウンターをインクリメント
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
1000
10. Exercise: Web Crawler
スキップ
まとめ
Goを触ったばかりだけど、これは強力だと感じる。なるほど。
まあもう少し書いてみないとわからないので、慣れも含めてしばらく色々書いてみようと思う。