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 の並行処理を安全に書くための基本的なテクニックです。積極的に活用しましょう。