Go の channel で送信専用・受信専用を型で制限する
Go の channel は、送信専用や受信専用に型を制限できます。関数の引数で方向を指定することで、意図しない操作を防ぎ、コードの意図を明確にできます。
channel の方向指定
通常の channel は双方向ですが、型に <- を付けることで方向を制限できます。
chan T // 双方向(送信も受信も可能)
chan<- T // 送信専用(送ることしかできない)
<-chan T // 受信専用(受け取ることしかできない)矢印の位置で方向が決まります。chan<- は「channel へ送る」、<-chan は「channel から受け取る」と覚えると分かりやすいでしょう。
関数の引数で方向を制限する
関数の引数に方向指定した channel を使うことで、その関数内での操作を制限できます。
package main
import "fmt"
// 送信専用:この関数は channel に送ることしかできない
func send(ch chan<- int, value int) {
ch <- value
// <-ch // コンパイルエラー:受信できない
}
// 受信専用:この関数は channel から受け取ることしかできない
func receive(ch <-chan int) int {
return <-ch
// ch <- 1 // コンパイルエラー:送信できない
}
func main() {
ch := make(chan int, 1)
send(ch, 42)
result := receive(ch)
fmt.Println(result)
}双方向の channel を関数に渡すと、自動的に指定された方向に変換されます。逆に、送信専用を双方向に戻すことはできません。
なぜ方向を制限するのか
方向を制限することで、以下のメリットがあります。
関数が送信側なのか受信側なのかが型を見ただけで分かる。コードの可読性が向上する。
受信専用の channel に送信しようとするとコンパイルエラーになる。バグを未然に防げる。
特に複数の goroutine が連携する処理では、どの goroutine が送信担当でどれが受信担当なのかを明確にすることが重要です。
実践例:ワーカーパターン
ジョブを送る channel とを結果を受け取る channel を分けるパターンです。
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
results <- job * 2
fmt.Printf("worker %d: processed job %d\n", id, job)
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
var wg sync.WaitGroup
// ワーカーを起動
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
// ジョブを送信
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// ワーカーの終了を待つ
wg.Wait()
close(results)
// 結果を収集
for result := range results {
fmt.Println("result:", result)
}
}worker 関数は jobs からしか受信できず、results にしか送信できません。この制限により、ワーカーがジョブを横取りしたり、結果を誤って受け取ったりすることが構造的に不可能になります。
戻り値での方向指定
関数が channel を返す場合にも方向を指定できます。
package main
import (
"fmt"
"time"
)
// 受信専用 channel を返す
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
time.Sleep(100 * time.Millisecond)
}
close(out)
}()
return out
}
func main() {
ch := generator(1, 2, 3, 4, 5)
// ch は受信専用なので送信しようとするとコンパイルエラー
// ch <- 100
for v := range ch {
fmt.Println(v)
}
}generator 関数は内部で双方向の channel を作りますが、戻り値は受信専用として返します。呼び出し側は受け取ることしかできないので、generator の動作を外部から妨害される心配がありません。
方向指定は Go の並行処理を安全に書くための基本的なテクニックです。積極的に活用しましょう。