こんにちは、ナナオです。

前回の続きでチュートリアルをやっていこうと思います。

Goroutines

goroutine(ゴルーチン)はGoのランタイムで管理される軽量なスレッドです。

複雑な記法は必要ありません。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("world")
	say("hello")
}

Channels

チャネル型(Channel)は、チャネルオペレータ(<-)を使って値の送受信を行うことができます。

これを使うことでゴルーチンの値を簡単に取得することができます。

package main

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // send sum to c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c

	fmt.Println(x, y, x+y)
}

selectステートメント

selectステートメント、これ最初に見たときは一見意味が分かりませんでした。

package main

import "fmt"

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() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

ここで理解できなかったのは以下の点です。

  • cがラムダ関数内でPrintlnされているが、なぜこれで出力されるのか?
  • quitにラムダ関数内で値を入れているが、なぜこれで実行終了と判定されるのか?

cがラムダ関数内でPrintlnされているが、なぜこれで出力されるのか?

まず、fibonacci関数が c <- x(送信)しようとしますが、最初は受け取り手がいないので、ここで一旦停止します。

一方でmain関数内で実行されるゴルーチンの関数はfmt.Println(<-c)を実行しようとします。

ここで送り手と受け手がつながります。

つながった瞬間にxの値がチャネルを通ってPrintlnで出力されます。

送信が完了したらx, y = y, x+yを実行します。

quitにラムダ関数内で値を入れているが、なぜこれで実行終了と判定されるのか?

上記の送り手と受け手の概念を理解すれば、こちらも同じような挙動になっていることが分かります。

ゴルーチンの中のforループが終わったらquitにチャンネルで値が送られます。

するとfibonacci関数のselect内のcase <- quitが準備完了の状態になります。

selectはquitから値を受け取り、fibonacci関数は終了します。

うーん、でもなんかまだ感覚的には理解できないなぁ。

Exercise: Equivalent Binary Trees

エキササイズを解いてみました。

(AIに助けてもらいながらではありますが)

package main

import (
	"fmt"
	"golang.org/x/tour/tree"
)

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
	if t == nil {
		return
	}
	Walk(t.Left, ch)
	ch <- t.Value
	Walk(t.Right, ch)
}

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {;
	ch1, ch2 := make(chan int), make(chan int)
	go func() {
		Walk(t1, ch1)
		close(ch1)
	}()
	go func() {
		Walk(t2, ch2)
		close(ch2)
	}()
	
	for {
		// カンマ ok 形式で「値」と「チャネルが開いているか」を受け取る
		v1, ok1 := <-ch1
		v2, ok2 := <-ch2

		// 両方が閉じたら、最後まで不一致がなかったということ
		if !ok1 && !ok2 {
			break
		}
		// どちらか一方が先に閉じた、または値が違う場合は false
		if v1 != v2 || ok1 != ok2 {
			return false
		}
	}
	return true
}

func main() {
	ch := make(chan int)
	// ラップしたゴルーチン内で実行
	go func() {
		Walk(tree.New(1), ch)
		close(ch) // 送信が終わったら、ここで閉じる!
	}()

	for i := range ch {
		fmt.Println(i)
	}
	
	result := Same(tree.New(1), tree.New(1))
	fmt.Println("result: ", result)
}

まとめ

今日はこの辺でおわり!