Go言語でチャネルを使って平行処理を書く際、
保守性を担保し、かつ後で見ても分かりやすい形でコーディングするコツの一つとして、
チャネルに対する権限をコンテキストごとに明確に区別する
というものがあると思っています。
具体的には、以下2つの権限を区別して実装すればスッキリして分かり良いです。
1. 管理者
– チャネルの初期化とクローズを行う
– チャネルへの書き込みを行う
– 管理者は単体
2. 閲覧者
– チャネルから値を読み出す
– 閲覧者は複数共存可
コードは例えば以下のような感じになります。
// チャネルの管理者権限をもつ関数
// ここ以外でのチャネルの作成、書き込み、クローズは行わない
manageStream := func() <-chan int {
stream := make(chan int, 100)
go func() {
defer close(stream)
for i := 0; i < 100; i++ {
stream <- i
}
}()
return stream
}
// チャネルの閲覧者権限をもつ関数
// 閲覧者は複数共存可能
readStream := func(stream <-chan int) {
for c := range stream {
fmt.Println("Read from stream:", c)
}
}
stream := manageStream()
readStream(stream)
fmt.Println("Done.")
manageStream関数がチャネルの初期化、書き込み、クローズまで行っています(管理者権限)。そしてreadStream関数はチャネルからの読み込みしか行いません(閲覧者権限)。
チャネルへの書き込みとチャネルクローズを一手に引き受けるのはmanageStream関数なので、
例えば
manageStream関数内でチャネルがクローズされた後、別の関数内で同チャネルへの書き込みが行われてpanicした、ということが起こり得ません。
またmanageStream関数以外の箇所でチャネルクローズを行うということがないため、クローズを別の箇所で複数回していてpanicになる、ということも起こらないです。
このように、管理者権限としてチャネルの作成、書き込み、クローズを担当する単体のコンテキストを定めることは、
プログラムのデッドロックや思わぬ平行処理エラーを防ぐ上で非常に有効な実装手段の一つだと思います。
もちろん必ずしも上記コード例のように実装できないケースもあります(複数のゴルーチンから平行的に書き込む必要がある場合など)。
しかし、チャネルに対する権限をコンテキストごとに分離するという考えはいつも持っておくと良さそうですね。
コードの保守性やデバッグのしやすさにかなり直結してくると思います。
コメント