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

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

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

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

スポンサーリンク

背景

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

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

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

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

select文(Select statements)

select 文は、可能な送信操作または受信操作の集合のうち、どれが進行するかを選択する。switch 文に似ているが、ケースがすべて通信操作を参照する点が異なる。

SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt   = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr   = Expression .

RecvStmt を持つケースは、RecvExpr の結果を1つまたは2つの変数に代入することができ、短縮変数宣言を使用して宣言することもできる。RecvExpr は(括弧で囲まれていてもよい)受信操作でなければならない。default ケースは最大1つであり、ケースリストのどこにでも配置できる。

select 文の実行はいくつかのステップで進行する:

  1. 文内のすべてのケースについて、受信操作のチャネルオペランドと、送信文のチャネルおよび右辺の式は、select 文に入った時点でソース順に正確に一度だけ評価される。結果は受信元または送信先のチャネルの集合と、対応する送信値である。この評価における副作用は、どの通信操作が進行するよう選択されるか(もしあれば)に関わらず発生する。短縮変数宣言または代入を持つ RecvStmt の左辺の式はまだ評価されない。
  2. 通信の1つ以上が進行可能な場合、進行可能な1つが一様な擬似ランダム選択によって選ばれる。そうでなければ、default ケースがあればそのケースが選ばれる。default ケースがなければ、select 文は少なくとも1つの通信が進行可能になるまでブロックする。
  3. 選択されたケースが default ケースでない限り、対応する通信操作が実行される。
  4. 選択されたケースが短縮変数宣言または代入を持つ RecvStmt である場合、左辺の式が評価され、受信した値(または複数の値)が代入される。
  5. 選択されたケースの文リストが実行される。

nil チャネルでの通信は決して進行できないため、nil チャネルのみを持ち default ケースを持たない select は永遠にブロックする。

var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
	print("received ", i1, " from c1\n")
case c2 <- i2:
	print("sent ", i2, " to c2\n")
case i3, ok := (<-c3):  // i3, ok := <-c3 と同じ
	if ok {
		print("received ", i3, " from c3\n")
	} else {
		print("c3 is closed\n")
	}
case a[f()] = <-c4:
	// 以下と同じ:
	// case t := <-c4
	//	a[f()] = t
default:
	print("no communication\n")
}

for {  // ランダムなビット列を c に送信する
	select {
	case c <- 0:  // 注意: 文なし、fallthrough なし、ケースの統合なし
	case c <- 1:
	}
}

select {}  // 永遠にブロック

解説

select文ってなに?

select 文は、複数のチャネル操作のうち、準備ができたものを1つ選んで実行する仕組みです。チャネル専用の switch と考えるとわかりやすいです。

select {
case msg := <-ch1:
    fmt.Println("ch1 から受信:", msg)
case msg := <-ch2:
    fmt.Println("ch2 から受信:", msg)
case ch3 <- value:
    fmt.Println("ch3 に送信")
}

「どのチャネルが先に準備できるかわからないけど、準備できたものから処理したい」という場面で使います。

実行の流れを理解する

select の動作は5ステップですが、実用上覚えておくべきことは3つです。

1. すべてのチャネル式が先に評価される

select {
case <-getChan():     // getChan() はここで呼ばれる
case sendChan() <- getValue():  // sendChan() と getValue() もここで呼ばれる
}
// どのケースが選ばれるかに関係なく、全部評価される

2. 準備できたケースからランダムに1つ選ばれる

ch := make(chan int, 1)
ch <- 42

select {
case v := <-ch:
    fmt.Println("受信:", v)
case ch <- 100:
    fmt.Println("送信")
}
// バッファが1なので、受信も送信もできる → どちらが選ばれるかはランダム

複数のケースが同時に進行可能な場合、特定のケースが優先されることはありません。公平にランダムに選ばれます。

3. どのケースも進行できなければブロックする

select {
case v := <-ch1:  // ch1 に値がない
    fmt.Println(v)
case v := <-ch2:  // ch2 にも値がない
    fmt.Println(v)
}
// どちらかに値が届くまでブロック

default ケース:ノンブロッキング操作

default をつけると、どのチャネルも準備できていないときに即座に default が実行されます。つまりブロックしません

select {
case msg := <-ch:
    fmt.Println("受信:", msg)
default:
    fmt.Println("まだ何も届いていない")
}

これを使って、ノンブロッキングな送受信を実現できます。

// ノンブロッキング送信:バッファが満杯なら諦める
select {
case ch <- value:
    fmt.Println("送信成功")
default:
    fmt.Println("バッファが満杯、スキップ")
}

よくある使い方

1. タイムアウト

select {
case result := <-ch:
    fmt.Println("結果:", result)
case <-time.After(5 * time.Second):
    fmt.Println("タイムアウト")
}

2. キャンセル(context との組み合わせ)

select {
case result := <-ch:
    fmt.Println("結果:", result)
case <-ctx.Done():
    fmt.Println("キャンセルされた:", ctx.Err())
}

3. 複数のソースからの受信を多重化する

for {
    select {
    case msg := <-emailCh:
        handleEmail(msg)
    case msg := <-smsCh:
        handleSMS(msg)
    case <-quit:
        return
    }
}

4. ランダムな値の生成

原文の例にある「ランダムなビット列」のパターンです。

for {
    select {
    case c <- 0:
    case c <- 1:
    }
}

両方のケースが常に進行可能(受信者がいれば)なので、ランダムに 0 か 1 が選ばれます。

nil チャネルの活用

nil チャネルでの通信は永遠にブロックするので、select のケースを動的に無効化するテクニックに使えます。

var ch1, ch2 <-chan int = activeCh1, activeCh2

for {
    select {
    case v, ok := <-ch1:
        if !ok {
            ch1 = nil  // ch1 を nil にすると、このケースは二度と選ばれない
            continue
        }
        process(v)
    case v, ok := <-ch2:
        if !ok {
            ch2 = nil
            continue
        }
        process(v)
    }
    if ch1 == nil && ch2 == nil {
        break  // 両方閉じたら終了
    }
}

select {} で永遠にブロック

空の select はケースが1つもないので、永遠にブロックします。

select {}  // プログラムをここで止める

メインのゴルーチンを終了させずに、他のゴルーチンを走り続けさせたいときに使うことがあります。ただし、通常は sync.WaitGroup やチャネルで待機するほうが意図が明確です。

switch との違い

見た目は似ていますが、重要な違いがあります。

switchselect
対象値や型チャネル操作
評価順序上から下(最初の一致)ランダム(進行可能なものから)
fallthrough使える(式switchのみ)使えない
ブロックしないdefault がなければする

おわりに 

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

よっしー
よっしー

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

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

コメント

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