1. Home
  2. Golang
  3. GuideToBecomingGoDeveloper
  4. GoingDeeper
  5. チャネル(Channels) - Golang learning step 2-5

チャネル(Channels) - Golang learning step 2-5

  • 公開日
  • カテゴリ:GoingDeeper
  • タグ:Golang,roadmap.sh,学習メモ
チャネル(Channels) - Golang learning step 2-5

roadmap.sh > Go > Going Deeper > Channels の学習を進めていきます。

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

contents

  1. 開発環境
  2. 参考 URL
  3. チャネル(Channels)
  4. チャネルの基本構文
  5. チャネルへの送信と受信
  6. チャネルの特徴
  7. チャネルのブロッキング
  8. バッファなしチャネル
  9. バッファありチャネル
  10. チャネルのクローズ
  11. Select 文
    1. Select 文の基本動作
    2. 基本的な使用例
    3. よくある使用パターン
  12. チャネルの注意点
  13. ベストプラクティス

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

参考 URL

チャネル(Channels)

Go のチャネル(channels)は、ゴルーチン間でデータを安全かつ効率的に送受信するための機構です。Go の並行処理の基本的なコンセプトである CSP(Communicating Sequential Processes)に基づいており、チャネルを使ってゴルーチン間の通信と同期が可能になります。チャネルはゴルーチン間でデータを安全に受け渡すためのパイプラインとして機能します。

チャネルの基本構文

チャネルは make 関数を使って作成します。たとえば、整数型のチャネルを作成する場合は以下のようにします。

ch := make(chan int)

このコードで作成した ch というチャネルは、int 型のデータを送受信できます。

チャネルへの送信と受信

チャネルにデータを送信するには、以下のように使います。

ch <- 10 // 10をチャネルに送信

逆に、チャネルからデータを受信するには以下のようにします。

value := <-ch // チャネルからデータを受信

一連の流れを記すと以下になります。

package main

import "fmt"

func main() {
  ch := make(chan int) // バッファなしチャネル

  go func() {
    ch <- 10
  }()
∂
  value := <-ch

  fmt.Printf("受信した値は %d です\n", value)
}

チャネルの特徴

  1. 型安全性
    • チャネルは特定の型のデータのみを扱うことができる
    • コンパイル時に型チェックが行われる
  2. 同期機能
    • デフォルト(バッファサイズを指定しない場合)では送受信操作がブロッキングされる
    • 送信側と受信側の同期を自動的に行う

チャネルのブロッキング

  • 送信のブロック
    • あるゴルーチンがチャネルにデータを送信しようとする際、受信する側の準備ができていない場合、送信を行っているゴルーチンは一時停止します。具体的には、受信側がチャネルからデータを受け取るまで、そのゴルーチンは次の処理に進めません。
  • 受信のブロック
    • 同様に、あるゴルーチンがチャネルからデータを受信しようとする際、送信する側の準備ができていなければ、そのゴルーチンは停止します。送信側がデータを送信するまで、受信ゴルーチンは待機して次の処理に進むことができません。

この「ブロッキング」動作によって、チャネルはゴルーチン間の同期を自然に実現します。たとえば、あるゴルーチンがデータを生成し、別のゴルーチンがそのデータを処理する際、受信側はデータが用意されるまで待機し、逆にデータを送信する側は受信の準備ができるまで待機します。これにより、ゴルーチン間でデータのタイミングが自動的に調整されるため、安全で効率的な並行処理が可能になります。

このように、チャネルを介してゴルーチン間でデータの受け渡しが行われる際、送信側や受信側が互いの準備が整うまで処理を一時停止するため、安全なデータ同期が可能になります。

バッファなしチャネル

バッファなしチャネルは、バッファ(容量)を指定しないチャネルです。バッファを指定しない場合、チャネルの送信と受信は必ず 1 対 1 で行われる制約の元に動作します。例えば、受信しない限り送信側で前述したブロッキング(待機)が発生します。

バッファなしチャネルの場合、送信側がデータを送信するためには受信側が必ず待機している必要があります。つまり、受信側の準備が整うまでは、送信側のゴルーチンは次の処理に進めず、一時停止(ブロック)されます。これは、チャネルにデータを送信するためには、受信側がデータを受け取る「準備」ができていることが前提条件であるためです。この性質により、チャネルを介したゴルーチン間の同期が保証され、安全なデータ受け渡しが実現します。

// バッファなしチャネル
ch := make(chan int)

【送信側】       【チャネル】 【受信側】
ch <- 42    --×->|    |            
(ブロック中)       |    |            
                 |    |   <-ch で受信待ち(value := <-ch)
(ブロック解除)     |    |
送信完了     ---->|    |
                 |    |   42 を受信
package main

import (
  "fmt"
  "time"
)

func main() {
    ch := make(chan int) // バッファなしチャネル

    // 送信側の処理
    go func() {
        fmt.Println("送信準備完了")
        ch <- 42  // ここでブロックされる
        fmt.Println("送信完了") // 受信されるまでここには到達しない
    }()

    // メインルーチンで少し待機
    time.Sleep(2 * time.Second)
    
    // 受信側の処理
    fmt.Println("受信準備完了")
    value := <-ch  // 受信処理
    fmt.Println("受信した値:", value)
    
    time.Sleep(1 * time.Second) // 送信完了のメッセージを確認するため待機
}
// 出力:
// 送信準備完了
// 受信準備完了
// 受信した値: 42
// 送信完了

バッファありチャネル

通常、チャネルはバッファなし(容量ゼロ)ですが、バッファありチャネルも作成可能です。バッファありチャネルの場合、受信せずとも指定したバッファの数までは送信が可能になります。以下は容量を 2 にしたバッファ付きチャネルの例です。

ch := make(chan int, 2) // バッファサイズ 2 のチャネル

// バッファに空きがある限りブロックされない
ch <- 1  // ブロックされない
ch <- 2  // ブロックされない
ch <- 3  // ここでブロックされる(バッファが満杯のため)

バッファ付きチャネルの場合、バッファが満たされるまでは送信はブロックされません。バッファがいっぱいになると、送信側はブロックされ、バッファに空きができるまで待機します。

// バッファサイズ2のチャネル
ch := make(chan int, 2)

【送信側】        【チャネル(バッファ)】     【受信側】
ch <- 1    ----> |[1][ ]|                 まだ受信していない
                 |[1][ ]|                
ch <- 2    ----> |[1][2]|                 まだ受信していない
                 |[1][2]|
ch <- 3    --×-> |[1][2]|                (バッファ満杯なのでブロック)
                 |[2][ ]|                 v := <- ch
ch <- 3    ----> |[2][3]|                 
package main

import (
  "fmt"
  "time"
)

func main() {
  bufCh := make(chan int, 2)

  go func() {
    bufCh <- 1 // ブロックされない
    fmt.Println("1つ目の送信完了")

    bufCh <- 2 // ブロックされない
    fmt.Println("2つ目の送信完了")

    bufCh <- 3 // ブロックされる
    fmt.Println("3つ目の送信完了")
  }()

  time.Sleep(2 * time.Second)

  v := <-bufCh
  fmt.Println(v)

  time.Sleep(1 * time.Second)

  v = <-bufCh
  fmt.Println(v)

  time.Sleep(1 * time.Second)

  v = <-bufCh
  fmt.Println(v)

  time.Sleep(1 * time.Second)
}
// 出力: 
// 1つ目の送信完了
// 2つ目の送信完了
// 1
// 3つ目の送信完了
// 2
// 3

チャネルのクローズ

close 関数を使ってチャネルをクローズすることができます。クローズされたチャネルには送信できませんが、受信は可能です。クローズされたことを確認するために、for ループと range を使って受信し続けることができます。

package main

import (
  "fmt"
  "time"
)

func main() {
  ch := make(chan int, 5)

  // データを送信するゴルーチン
  go func() {
    for i := 0; i < 5; i++ {
      ch <- i
      fmt.Printf("%d を送信しました\n", i)
    }
    close(ch) // チャネルをクローズ
    fmt.Println("チャネルをクローズしました")
  }()

  time.Sleep(2 * time.Second)
  fmt.Println("今から受信していきます")

  // チャネルからデータを受信し続ける
  for value := range ch {
    fmt.Println("受信:", value)
  }

  fmt.Println("受信が終了しました")
}
// 出力:
// 0 を送信しました
// 1 を送信しました
// 2 を送信しました
// 3 を送信しました
// 4 を送信しました
// チャネルをクローズしました
// 今から受信していきます
// 受信: 0
// 受信: 1
// 受信: 2
// 受信: 3
// 受信: 4
// 受信が終了しました

Select 文

複数のチャネルを同時に扱う場合、select 文を使用すると便利です。select を使うと、複数のチャネルからの送受信を待機し、どれか一つのケースが準備できたらそれを実行します。

Select 文の基本動作

  1. 複数のチャネル操作を待ち受ける
  2. 最初に準備できた操作を実行する
  3. どの操作も準備できていない場合はブロック
  4. 複数の操作が同時に準備できた場合はランダムに1つ選択

基本的な使用例

package main

import (
  "fmt"
  "time"
)

func main() {
  ch1 := make(chan string)
  ch2 := make(chan string)

  // 送信側ゴルーチン1
  go func() {
    time.Sleep(2 * time.Second) // 2 秒待機
    ch1 <- "one"
  }()

  // 送信側ゴルーチン2
  go func() {
    time.Sleep(1 * time.Second) // 1 秒待機(こちらが先に送信される)
    ch2 <- "two"
  }()

  // 受信側(select)
  select {
  case msg1 := <-ch1:
    fmt.Println("受信ch1:", msg1)
  case msg2 := <-ch2:
    fmt.Println("受信ch2:", msg2)
  }
}
// 出力: 
// 受信ch2: two

よくある使用パターン

1. タイムアウト処理

package main

import (
  "fmt"
  "time"
)

func main() {
  ch := make(chan string)

  go func() {
    time.Sleep(2 * time.Second) // 2 秒待機
    ch <- "処理完了"
  }()

  select {
  case msg := <-ch:
    fmt.Println("受信:", msg)
  case <-time.After(1 * time.Second): // 1 秒経過でタイムアウトになる
    fmt.Println("タイムアウト")
  }
}
// 出力:
// タイムアウト

2. デフォルトケース(ノンブロッキング操作)

package main

import (
  "fmt"
)

func main() {
  ch := make(chan string)

  select {
  case msg := <-ch:
    fmt.Println("受信:", msg)
  default:
    fmt.Println("データなし")  // チャネルが空の場合すぐに実行
  }
}
// 出力:
// データなし

3. 送受信の両方を待ち受ける

package main

import (
  "fmt"
  "time"
)

func main() {
  ch := make(chan string) // メッセージ送信用チャネル
  done := make(chan bool) // 処理中断用チャネル

  // 受信側ゴルーチン(別のゴルーチンで待ち受ける人)
  go func() {
    time.Sleep(2 * time.Second) // 2秒後に受信する想定
    msg := <-ch
    fmt.Println("受信完了:", msg)
  }()

  // 送信用ゴルーチン
  go func() {
    time.Sleep(3 * time.Second) // 3秒後にメッセージ送信
    ch <- "hello"
  }()

  // 中断通知用ゴルーチン
  go func() {
    time.Sleep(1 * time.Second) // 1秒後に中断通知
    done <- true
  }()

  select {
  case msg := <-ch: // チャネルからの受信を待つ
    fmt.Printf("送信成功 %s", msg)
  case <-done: // 中断信号を受け取る
    fmt.Println("処理中断")
  }
}
// 出力:
// 処理中断

4. 無限ループでの使用

package main

import (
  "fmt"
  "time"
)

func main() {
  ch1 := make(chan string)
  ch2 := make(chan string)
  done := make(chan bool)

  // 送信側ゴルーチン
  go func() {
    for i := 0; i < 3; i++ {
      time.Sleep(1 * time.Second)
      ch1 <- fmt.Sprintf("ch1: %d", i)
    }
     done <- true
  }()

  go func() {
    for i := 0; i < 3; i++ {
      time.Sleep(2 * time.Second)
      ch2 <- fmt.Sprintf("ch2: %d", i)
    }
    done <- true
  }()

  // 受信側
  finished := 0
  for {
    select {
    case msg1 := <-ch1:
        fmt.Println("受信1:", msg1)
    case msg2 := <-ch2:
        fmt.Println("受信2:", msg2)
    case <-done:
        finished++
        if finished == 2 {
          fmt.Println("全ての処理が完了")
          return
        }
    }
  }
}
// 出力:
// 受信1: ch1: 0
// 受信2: ch2: 0
// 受信1: ch1: 1
// 受信1: ch1: 2
// 受信2: ch2: 1
// 受信2: ch2: 2
// 全ての処理が完了

チャネルの注意点

  1. チャネルのクローズ
    • 送信側が送信完了後にチャネルをクローズする
    • クローズ済みのチャネルへの送信はパニックを引き起こす
  2. デッドロックの防止
    • 送受信の数が合わない場合にデッドロックが発生
    • バッファサイズの適切な設定が重要
  3. nil チャネル
    • nil チャネルへの送受信は永久にブロックされる
    • チャネルの初期化忘れに注意

ベストプラクティス

  1. チャネルは可能な限り細かいスコープで使用する
  2. 送信側でチャネルをクローズする
  3. 受信側でチャネルが閉じられているかチェックする
  4. バッファサイズは必要最小限に設定する
  5. エラー処理を適切に行う

まとめ

  • チャネルはゴルーチン間でデータを安全に送受信するためのGoの並行処理機構
  • 送受信操作はブロッキングを基本とし、ゴルーチン間の自動的な同期を実現
  • バッファなしチャネルは 1 対 1 の同期通信、バッファありは指定容量まで非同期通信が可能
  • チャネルは型安全で、コンパイル時に型チェックが行われる
  • close 関数でチャネルを閉じることができ、閉じた後は送信不可だが受信は可能
  • select 文で複数のチャネル操作を同時に待ち受け、タイムアウトなどの制御が可能
  • デッドロック、nil チャネル、クローズ済みチャネルへの送信には注意が必要
  • 適切なスコープ、バッファサイズ、エラー処理の実装がベストプラクティス


[Next] Step 2-6: バッファー(Buffer)

[Prev] Step 2-4: ゴルーチン(Goroutines)

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級