Go 言語におけるチャネルの基本要素

GolangGolangBeginner
今すぐ練習

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

Go言語の最も魅力的な特徴の一つは、並列プログラミングに対するネイティブなサポートです。並列性、並列プログラミング、およびネットワーク通信に対する強力なサポートがあり、マルチコアプロセッサをより効率的に利用できます。

共有メモリを通じて並列性を達成する他の言語とは異なり、Goは異なるgoroutine間の通信にチャネルを使用することで並列性を達成します。

GoroutineはGoにおける並列性の単位であり、軽量スレッドと考えることができます。従来のスレッドと比較して、それらの間を切り替える際に消費するリソースが少なくなります。

チャネルとgoroutineは一緒にGoにおける並列性のプリミティブを構成します。このセクションでは、この新しいデータ構造であるチャネルについて学びます。

ポイント

  • チャネルの種類
  • チャネルの作成
  • チャネルの操作
  • チャネルのブロッキング
  • 片方向チャネル

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("Golang")) -.-> go/DataTypesandStructuresGroup(["Data Types and Structures"]) go(("Golang")) -.-> go/ConcurrencyGroup(["Concurrency"]) go(("Golang")) -.-> go/BasicsGroup(["Basics"]) go/BasicsGroup -.-> go/values("Values") go/BasicsGroup -.-> go/variables("Variables") go/DataTypesandStructuresGroup -.-> go/structs("Structs") go/ConcurrencyGroup -.-> go/goroutines("Goroutines") go/ConcurrencyGroup -.-> go/channels("Channels") go/ConcurrencyGroup -.-> go/select("Select") subgraph Lab Skills go/values -.-> lab-149096{{"Go 言語におけるチャネルの基本要素"}} go/variables -.-> lab-149096{{"Go 言語におけるチャネルの基本要素"}} go/structs -.-> lab-149096{{"Go 言語におけるチャネルの基本要素"}} go/goroutines -.-> lab-149096{{"Go 言語におけるチャネルの基本要素"}} go/channels -.-> lab-149096{{"Go 言語におけるチャネルの基本要素"}} go/select -.-> lab-149096{{"Go 言語におけるチャネルの基本要素"}} end

チャネルの概要

チャネルは、主にgoroutine間の通信に使用される特殊なデータ構造です。

他の並列モデルがミューテックスやアトミック関数を使用してリソースへの安全なアクセスを行うのとは異なり、チャネルは複数のgoroutine間でデータを送信および受信することによって同期を可能にします。

Goでは、チャネルは、最初に入ったものが最初に出る(FIFO)ルールに従うコンベアベルトやキューのようなものです。

チャネルの型と宣言

チャネルは参照型であり、その宣言形式は以下の通りです。

var channelName chan elementType

各チャネルには特定の型があり、同じ型のデータのみを転送できます。いくつかの例を見てみましょう。

~/project ディレクトリに channel.go という名前のファイルを作成します。

cd ~/project
touch channel.go

たとえば、int 型のチャネルを宣言しましょう。

var ch chan int

上記のコードは、ch という名前の int チャネルを宣言します。

これに加えて、他にも多くの一般的なチャネル型があります。

channel.go に以下のコードを書きます。

package main

import "fmt"

func main() {
    var ch1 chan int   // 整数型のチャネルを宣言
    var ch2 chan bool  // ブール型のチャネルを宣言
    var ch3 chan []int // 整数のスライスを転送するチャネルを宣言
    fmt.Println(ch1, ch2, ch3)
}

上記のコードは、単に3つの異なる型のチャネルを宣言しています。

以下のコマンドを使用してプログラムを実行します。

go run channel.go

プログラムの出力は以下の通りです。

<nil> <nil> <nil>

チャネルの初期化

マップと同様に、チャネルは宣言した後で使用できるようになる前に初期化する必要があります。

チャネルを初期化する構文は以下の通りです。

make(chan elementType)

channel.go に以下のコードを書きます。

package main

import "fmt"

func main() {
    // 整数データを格納するチャネル
    ch1 := make(chan int)

    // ブールデータを格納するチャネル
    ch2 := make(chan bool)

    // []int データを格納するチャネル
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

以下のコマンドを使用してプログラムを実行します。

go run channel.go

プログラムの出力は以下の通りです。

0xc0000b4000 0xc0000b4040 0xc0000b4080

上記のコードでは、3つの異なる型のチャネルが定義されて初期化されています。

出力を見ることで、マップとは異なり、チャネル自体を直接印刷するとその内容が明らかにならないことがわかります。

チャネルを直接印刷すると、チャネル自体のメモリアドレスが表示されます。これは、ポインタと同じように、チャネルはメモリアドレスへの参照にすぎないためです。

では、チャネル内の値にアクセスして操作するにはどうすればよいでしょうか。

チャネルの操作

データの送信

チャネルの操作には独自の方法があります。一般的に、他のデータ型を扱う際には、データにアクセスして操作するために = とインデックスを使用します。

しかし、チャネルの場合、送信と受信の両方の操作に <- を使用します。

チャネルでは、データを書き込むことを送信と呼び、チャネルに1つのデータを送信することを意味します。

では、どのようにデータを送信するのでしょうか。

channel.go に以下のコードを書きます。

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    fmt.Println(ch)
}

以下のコマンドを使用してプログラムを実行します。

go run channel.go

プログラムの出力は以下の通りです。

0xc0000b4000

チャネルにデータを入れましたが、チャネル値の出力から直接は見えません。

これは、チャネル内のデータは使用される前にまだ受信される必要があるためです。

データの受信

チャネルから受信するとはどういう意味でしょうか。マップやスライスについて学ぶ際、これらのデータ型からインデックスやキーを使って任意のデータを取得できます。

チャネルの場合、チャネルに最初に入った値のみを取得できます。

値が受信されると、チャネル内にはもはや存在しません。

言い換えると、チャネルからの受信は、他のデータ型でデータを照会して削除するのと同じようなものです。

channel.go に以下のコードを書きます。

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

以下のコマンドを使用してプログラムを実行します。

go run channel.go

プログラムの出力は以下の通りです。

10
20

最初にチャネルに入った値 10 が最初に出力され、出力された後はチャネル内には存在しなくなります。

後に入った値 20 は後で出力され、チャネルの最初に入ったものが最初に出る原則が明らかになります。

チャネルのクローズ

チャネルがもはや必要なくなったら、クローズする必要があります。ただし、チャネルをクローズする際には、以下の点に注意する必要があります。

  • クローズされたチャネルに値を送信すると、ランタイムエラーが発生します。

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
    
        fmt.Println(<-ch)
    
        close(ch)
        ch <- 20
    }

    プログラムの出力は以下の通りです。

    10
    panic: send on closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:10 +0x1a0
    exit status 2
  • クローズされたチャネルから受信すると、空になるまで値を取得し続けます。

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    以下のコマンドを使用してプログラムを実行します。

    go run channel.go

    プログラムの出力は以下の通りです。

    10
    20
    30
  • クローズされた空のチャネルから受信すると、対応するデータ型のデフォルトの初期値が受信されます。

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    以下のコマンドを使用してプログラムを実行します。

    go run channel.go

    プログラムの出力は以下の通りです。

    10
    20
    30
    0
    0
  • 既にクローズされたチャネルをクローズすると、エラーが発生します。

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
        close(ch)
        close(ch)
        fmt.Println(<-ch)
    }

    プログラムの出力は以下の通りです。

    panic: close of closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:9 +0x75
    exit status 2

チャネルのブロッキング

注意深い学生たちは、チャネルの宣言と初期化を紹介した際に、チャネルの容量を指定していないことに気付いたかもしれません。

package main

import "fmt"

func main() {
    // チャネルに整数データを格納する
    ch1 := make(chan int) // 以下も同じ

    // チャネルにブールデータを格納する
    ch2 := make(chan bool)

    // チャネルに []int データを格納する
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

しかし、チャネルの操作を示す際には、チャネルの容量を指定していました。

package main

import "fmt"

func main() {
    // チャネル容量を3に指定する
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

容量が指定されていないチャネルは、バッファリングされていないチャネルと呼ばれ、バッファ空間がありません。

送信者または受信者が準備ができていない場合、最初の操作は、もう一方の操作が準備できるまでブロックされます。

chan1 := make(chan int) // int型のバッファリングされていないチャネル

容量とバッファ空間が指定されているチャネルは、バッファリングされたチャネルと呼ばれます。

chan2 := make(chan int, 5) // 容量5のint型のバッファリングされたチャネル

これらはキューのように機能し、最初に入ったものが最初に出る(FIFO)ルールに従います。

バッファリングされたチャネルの場合、送信操作を使用してキューの末尾に要素を追加し、受信操作を使用してキューの先頭から要素を削除できます。

バッファリングされたチャネルに容量を超えるデータを入れるとどうなるでしょうか。確認してみましょう。channel1.go ファイルに以下のコードを書き込みます。

cd ~/project
touch channel1.go
package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    ch <- 20
    fmt.Println("succeed")
}

以下のコマンドを使用してプログラムを実行します。

go run channel1.go

プログラムの出力は以下の通りです。

fatal error: all goroutines are asleep - deadlock!

チャネルが既に満杯であるため、プログラムがデッドロックすることがわかります。

この問題を解決するには、まずチャネルからデータを取り出すことができます。channel.go に以下のコードを書きます。

package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    fmt.Println("Data extracted:", <-ch)

    ch <- 20
    fmt.Println("succeed")
}

以下のコマンドを使用してプログラムを実行します。

go run channel.go

プログラムの出力は以下の通りです。

Data extracted: 10
succeed

取り出し、使用し、再び取り出し、再び使用するという戦略を採用することで、限られた容量のチャネルを継続的に使用することができます。

片方向チャネル

デフォルトでは、チャネルは双方向的で、読み取りまたは書き込みが可能です。時々、チャネルの使用を制限する必要があります。たとえば、特定の関数内でチャネルが書き込み専用または読み取り専用になるようにしたい場合があります。これをどのように実現できるでしょうか。

初期化する前に、使用制限のあるチャネルを明示的に宣言することができます。

書き込み専用チャネル

書き込み専用チャネルとは、特定の関数内でチャネルに書き込みのみが可能で、読み取りはできないことを意味します。int型の書き込み専用チャネルを宣言して使用してみましょう。

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 10
    fmt.Println("Data written to channel")
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    fmt.Println("Data received:", <-ch)
}

以下のコマンドを使用してプログラムを実行します。

go run channel.go

プログラムの出力は以下の通りです。

Data written to channel
Data received: 10

この例では、writeOnly関数がチャネルの使用を書き込み専用に制限しています。

読み取り専用チャネル

読み取り専用チャネルとは、特定の関数内でチャネルからの読み取りのみが可能で、書き込みはできないことを意味します。int型の読み取り専用チャネルを宣言して使用してみましょう。

package main

import "fmt"

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    ch <- 20
    readOnly(ch)
}

以下のコマンドを使用してプログラムを実行します。

go run channel.go

プログラムの出力は以下の通りです。

Data received from channel: 20

この例では、readOnly関数がチャネルの使用を読み取り専用に制限しています。

書き込み専用と読み取り専用チャネルの組み合わせ

書き込み専用チャネルと読み取り専用チャネルを組み合わせて、制限されたチャネルを通じたデータの流れを示すことができます。

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 30
    fmt.Println("Data written to channel")
}

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    readOnly(ch)
}

以下のコマンドを使用してプログラムを実行します。

go run channel.go

プログラムの出力は以下の通りです。

Data written to channel
Data received from channel: 30

この例は、書き込み専用チャネルがデータを読み取り専用チャネルに渡す方法を示しています。

まとめ

この実験では、チャネルの基本を学びました。それには以下が含まれます。

  • チャネルの種類と宣言方法
  • チャネルを初期化する方法
  • チャネルの操作
  • チャネルブロッキングの概念
  • 片方向チャネルの宣言