去る4月24日に開催されたGo Conference Online 2021 Springにて、 「ホットリロードツールの作り方」という発表をしました。

当日の動画もYoutubeでご覧いただけます。

また、発表中で紹介している自作ホットリロードツール「arelo」もGitHubで公開しています。

発表の補足:容量1チャネルを使ったトリック

当日質問も頂いたのですが、少しわかりにくかったと思うので補足します。 また、ちょうど裏番組だったセッションでも似たような方法が チャネルの活用方法のひとつとして紹介されていました。

	trg := make(chan struct{}, 1)

	go func() {
		for {
			<-trigger
			select { // (1)
			case trg <- struct{}{}:
			default:
			}
		}
	}()

	for {
		cmd := runCmd()

		select { // (2)
		case <-trg:
		default:
		}
		<-trg // (3)
		<-time.NewTimer(delay).C
		killCmd(cmd)
	}

まずチャネルtrgについてです。 容量(capacity)1で初期化しています。

続いて前半のgoroutineでは、チャネルtriggerにメッセージが届いたら(1)のselect文でtrgへ空のstructの投入を試みています。 このときtrgは容量が1なので空のときは投入できますが、すでに1つ入っているときはdefaultに処理が移り何も投入せず、結果としてメッセージは捨てられます。

次に後半のループでは、コマンドを起動した後(3)でtrgチャネルにメッセージが届くのを待ち、 メッセージが届いたら一定時間待ってコマンドを停止することを繰り返しています。

このなかでtrgチャネルを待つ前(3)の前に、(2)のselect文でtrgチャネルからメッセージを取り出そうと試みています。 もしtrgにメッセージが入っているなら取り出され、空ならdefaultによりなにもしません。 trgは容量1なので、高々1つしかメッセージは入っていません。 つまり、(2)のselectによりtrgは確実に空になり、(3)で待機することになります。

前半のgoroutineでは、trgが空の時しかメッセージを投入できませんでした。 そして、後半のループではtrgが必ず空の状態で(3)の待機をしています。 これらにより、(3)で待ち始めてから最初のtriggerへのメッセージだけが(3)によって取り出され、 次にまた(3)で待ち始めるまでのメッセージは捨てることになります。

まとめ

裏番組でも言及されていましたが、Goのチャネルはselectと組み合わせることで真価を発揮します。 そしてContextやタイマーもチャネルになっています。 selectという糊でこれらをうまく連携させていると、いかにもGoらしいコードだなと個人的には感じます。

ところで、容量付きチャネルとselect-defaultを組み合わせるテクニックはとても有用で色々な場面で使われていると思うのですが、なにか名前は付いていないのでしょうか。 知っていたら教えてください。