Closed11

A Tour of Go やってみた Part5

kun432kun432

1. Goroutines

**goroutine(ゴルーチン)**はGoランタイムで使える軽量スレッドのこと。goではgoroutineを使って、並行処理を行う。

以下のように関数の前にgoをつければ、

go f(x, y, z)

f(x, y, z)が「新しい」goroutineとして実行される。なお「新しい」とあるのは、実行元もgoroutineだからで、関数の評価などは実行元のgoroutineで行われる。

1.go
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二は別の手法があるため、これを使うことは少ないらしい。それは次で。

kun432kun432

2. Channels

チャネル(Channel)型 を使うと、goroutine同士でデータをやりとりすることができる。基本的な使い方は以下の通り。

int型のチャネルを作成

ch := make(chan int)

チャネルch42を送る

ch <- 42

チャネルから値を受け取る

v := <=ch

チャネルの特徴として、デフォルトでは、送信・受信は相手側の準備が揃うまでブロックされるため、明示的なロックや条件変数を用意しなくても、同期されることになる。

2.go
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の実行順は保証されないみたい。あと、同じチャネルに複数から受信されているけど、キューっぽい感じで取り出せるのね

kun432kun432

3. Buffered Channels

ここまでの使い方だとチャネルは送信・受信が両方揃わないとブロックされていた。バッファ付きチャネルにすると、チャネルに一時的に値をためておくことができる。

バッファ付きチャネルは以下のようにmakeの引数でバッファの長さを指定することで初期化できる。

ch := make(chan int, 3) // int型、容量3のバッファ付きチャネル

上記の場合、受信側がなくても3つまではブロックされることなく送信ができる。ただしバッファがたまり切るとブロックされる。

例えば以下のように書くと・・・

3.go
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
}

チャネルの受信側がいないたため、デッドロックということになる。

kun432kun432

4. Range and Close

チャネルに値を送信する側が、送信する値がないことを宣言するために使うのがclose

close(c)

受信側は受信時に2つの値を受け取るようにすると、チャネルがクローズされているかを確認できる。

v, ok <- c

上記だと、受信する値がないか、もしくは、チャネルがクローズされている場合は okfalse が返るのでこれを使えば良い。

例えばこんな感じ

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)
}

これで、受信時に結果をチェックする必要もないし、チャネルが閉じられるまで自動で受信、チャネルがクローズされたらループを抜けるということになる。

4.go
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が必要。なければデッドロックで止まる。
kun432kun432

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 : リンゴ が届きました

サンプルで紹介されているコードも。

5.go
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
kun432kun432

6. Default Selection

selectではどのケースにも該当しない場合はdefaultが実行されるが、これを使うと、どのcaseのチャネルも準備ができていない場合に、非ブロッキングな処理ができる。

6.go
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)
        }
    }
}
出力
    .
    .
ピッ...
    .
    .
ピッ...
    .
    .
ピッ...
    .
    .
ピッ...
    .
    .
ピッ...
ポーン!
kun432kun432

9. sync.Mutex

mutexはmutual exclusion(相互排他)の略で、1つの変数に複数のgoroutineが同時にアクセスしないようにすることができる。

Goにはsync.Mutexが用意されており、以下の2つのメソッドが用意されている。

  • Lock(): 他のgoroutineは待機する
  • Unlock(): 他のgoroutineが続けることができる

これらで囲めば排他制御ができる。

また、deferを使えば必ず解除されることが保証される。

9.go
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
kun432kun432

まとめ

Goを触ったばかりだけど、これは強力だと感じる。なるほど。

まあもう少し書いてみないとわからないので、慣れも含めてしばらく色々書いてみようと思う。

このスクラップは2ヶ月前にクローズされました