Go言語入門:言語仕様 -Vol.93-

スポンサーリンク
Go言語入門:言語仕様 -Vol.93- 用語解説
Go言語入門:言語仕様 -Vol.93-
この記事は約6分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

受信演算子(Receive operator)

チャネル型のオペランド ch に対して、受信操作 <-ch の値はチャネル ch から受信された値である。チャネルの方向は受信操作を許可しなければならず、受信操作の型はチャネルの要素型である。式は値が利用可能になるまでブロックする。nil チャネルからの受信は永遠にブロックする。閉じられたチャネルに対する受信操作は常に即座に進行でき、以前に送信されたすべての値が受信された後は要素型のゼロ値を生成する。

v1 := <-ch
v2 = <-ch
f(<-ch)
<-strobe  // クロックパルスを待ち、受信した値を破棄する

オペランドの型が型パラメータである場合、その型集合のすべての型は受信操作を許可するチャネル型でなければならず、すべて同じ要素型を持たなければならない。その要素型が受信操作の型となる。

以下の特別な形式の代入文または初期化で使用される受信式は

x, ok = <-ch
x, ok := <-ch
var x, ok = <-ch
var x, ok T = <-ch

通信が成功したかどうかを報告する追加の型なし真偽値の結果を生成する。受信した値がチャネルへの成功した送信操作によって配信された場合、ok の値は true であり、チャネルが閉じられて空であるためにゼロ値が生成された場合は false である。


解説

受信演算子ってなに?

<-ch は、チャネルから値を受け取る演算子です。Go の並行処理の核となる操作ですね。

ch := make(chan int)

// 別のゴルーチンから値を送信
go func() {
    ch <- 42  // 送信
}()

// メインのゴルーチンで受信
v := <-ch     // 42 を受け取る
fmt.Println(v) // 42

チャネルは、ゴルーチン間で安全にデータを受け渡す「パイプ」のようなものです。<- はそのパイプから値を取り出す操作です。

ブロックする挙動

受信操作は、値が届くまで**ブロック(待機)**します。これは非常に重要な特徴です。

ch := make(chan string)

go func() {
    time.Sleep(2 * time.Second)
    ch <- "done"  // 2秒後に送信
}()

fmt.Println("待機中...")
msg := <-ch          // ここで2秒間ブロックされる
fmt.Println(msg)     // "done"

プログラムは <-ch の行で自動的に止まり、値が届いたら再開します。明示的なポーリングやスリープが不要なのが、チャネルの便利なところです。

nilチャネルと閉じたチャネル

この2つの挙動はよく混同されるので、しっかり区別しましょう。

nilチャネル:永遠にブロック

var ch chan int  // nil チャネル(初期化されていない)
v := <-ch       // ここで永遠にブロック(デッドロック)

一見バグに見えますが、select 文と組み合わせて「このケースを無効にする」というテクニックに使われることがあります。

閉じたチャネル:ゼロ値が即座に返る

ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch)

fmt.Println(<-ch)  // 1(送信済みの値)
fmt.Println(<-ch)  // 2(送信済みの値)
fmt.Println(<-ch)  // 0(閉じたチャネルからのゼロ値)
fmt.Println(<-ch)  // 0(何度読んでもゼロ値)

閉じたチャネルからの受信はブロックせずに即座に返ります。送信済みの値がすべて読まれた後は、要素型のゼロ値が返り続けます。

v, ok パターンで閉じたチャネルを検出

受信した値が「本物の送信値」なのか「閉じたチャネルのゼロ値」なのかを区別するには、2値で受け取ります。

ch := make(chan int, 2)
ch <- 10
close(ch)

v, ok := <-ch
fmt.Println(v, ok)  // 10, true(本物の値)

v, ok = <-ch
fmt.Println(v, ok)  // 0, false(チャネルが閉じて空)

okfalse なら「チャネルが閉じられていて、もう値はない」という意味です。

map のアクセスや型アサーションで学んだ v, ok パターンと同じ発想ですね。Go では「失敗する可能性がある操作は2値で結果を返す」という設計が一貫しています。

よくある使い方

1. range でチャネルからすべての値を受け取る

ch := make(chan int)

go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)  // 送信が終わったら閉じる
}()

for v := range ch {
    fmt.Println(v)  // 0, 1, 2, 3, 4
}
// チャネルが閉じると range ループは自動的に終了

range を使うと、v, ok のチェックを自分で書かなくて済みます。

2. シグナルとしてのチャネル(値を使わない)

done := make(chan struct{})

go func() {
    // 何か処理をする
    time.Sleep(time.Second)
    close(done)  // 完了を通知
}()

<-done  // 完了を待つ(値は不要なので変数に入れない)

原文の <-strobe の例と同じパターンです。受信した値自体は使わず、「何かが届いた」という事実だけを利用します。要素型を struct{}(ゼロサイズ型)にするのは、メモリを無駄にしないための慣例です。

3. チャネルの方向制限

受信専用・送信専用のチャネル型を関数の引数に使うことで、誤用を防げます。

func producer(ch chan<- int) {  // 送信専用
    ch <- 42
    // <-ch  // コンパイルエラー! 送信専用なので受信できない
}

func consumer(ch <-chan int) {  // 受信専用
    v := <-ch
    // ch <- 1  // コンパイルエラー! 受信専用なので送信できない
    fmt.Println(v)
}

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました