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

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

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

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

スポンサーリンク

背景

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

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

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

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

型switch(Type switches)

型switch は値ではなく型を比較する。それ以外は式switch と類似している。型switch は、実際の型の代わりにキーワード type を使用した型アサーションの形式を持つ特別なswitch式で示される:

switch x.(type) {
// ケース
}

ケースは、式 x の動的型に対して実際の型 T を照合する。型アサーションと同様に、x はインターフェース型でなければならないが、型パラメータであってはならない。また、ケースに列挙された各非インターフェース型 Tx の型を実装しなければならない。型switch のケースに列挙される型はすべて異なっていなければならない。

TypeSwitchStmt  = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause  = TypeSwitchCase ":" StatementList .
TypeSwitchCase  = "case" TypeList | "default" .

TypeSwitchGuard は短縮変数宣言を含むことができる。その形式が使用される場合、変数は各節の暗黙的ブロック内の TypeSwitchCase の終わりで宣言される。ちょうど1つの型を列挙するケースを持つ節では、変数はその型を持つ。そうでなければ、変数は TypeSwitchGuard の式の型を持つ。

型の代わりに、ケースは事前宣言された識別子 nil を使用できる。そのケースは、TypeSwitchGuard の式が nil のインターフェース値である場合に選択される。nil ケースは最大1つである。

interface{} の式 x が与えられたとき、以下の型switch は:

switch i := x.(type) {
case nil:
	printString("x is nil")                // i の型は x の型(interface{})
case int:
	printInt(i)                            // i の型は int
case float64:
	printFloat64(i)                        // i の型は float64
case func(int) float64:
	printFunction(i)                       // i の型は func(int) float64
case bool, string:
	printString("type is bool or string")  // i の型は x の型(interface{})
default:
	printString("don't know the type")     // i の型は x の型(interface{})
}

以下のように書き換えることができる:

v := x  // x は正確に一度だけ評価される
if v == nil {
	i := v                                 // i の型は x の型(interface{})
	printString("x is nil")
} else if i, isInt := v.(int); isInt {
	printInt(i)                            // i の型は int
} else if i, isFloat64 := v.(float64); isFloat64 {
	printFloat64(i)                        // i の型は float64
} else if i, isFunc := v.(func(int) float64); isFunc {
	printFunction(i)                       // i の型は func(int) float64
} else {
	_, isBool := v.(bool)
	_, isString := v.(string)
	if isBool || isString {
		i := v                         // i の型は x の型(interface{})
		printString("type is bool or string")
	} else {
		i := v                         // i の型は x の型(interface{})
		printString("don't know the type")
	}
}

型パラメータまたはジェネリック型をケースの型として使用できる。インスタンス化時にその型がswitch内の別のエントリと重複することが判明した場合、最初に一致するケースが選択される。

func f[P any](x any) int {
	switch x.(type) {
	case P:
		return 0
	case string:
		return 1
	case []P:
		return 2
	case []byte:
		return 3
	default:
		return 4
	}
}

var v1 = f[string]("foo")   // v1 == 0
var v2 = f[byte]([]byte{})  // v2 == 2

型switch ガードの前に単純文を置くことができ、ガードが評価される前に実行される。

fallthrough 文は型switch では許可されない。


解説

型switchの基本

型switchは、インターフェースの中身がどの型かを判定して分岐する仕組みです。

var x interface{} = 42

switch x.(type) {
case int:
    fmt.Println("整数です")
case string:
    fmt.Println("文字列です")
default:
    fmt.Println("その他の型です")
}
// 出力: 整数です

以前学んだ型アサーション x.(int) の発展版です。型アサーションは「この型だよね?」と1つの型を確認するのに対し、型switchは「何の型?」と複数の候補を一度にチェックできます。

変数を受け取る形式

switch i := x.(type) のように書くと、各ケースの中で iそのケースの型に変換された状態で使えます。

func describe(x interface{}) {
    switch v := x.(type) {
    case int:
        fmt.Printf("整数: %d\n", v)      // v は int 型
    case string:
        fmt.Printf("文字列: %s\n", v)     // v は string 型
    case bool:
        fmt.Printf("真偽値: %t\n", v)     // v は bool 型
    default:
        fmt.Printf("不明な型: %T\n", v)   // v は interface{} 型
    }
}

describe(42)       // 整数: 42
describe("hello")  // 文字列: hello
describe(true)     // 真偽値: true

これが型switchの最大の便利さです。型アサーションを自分で書く手間が省け、各ケースの中では安全にその型として扱えます。

変数 i の型がケースによって変わる

ここがちょっとややこしいポイントです。i の型は、ケースにちょうど1つの型が書かれているか、複数の型が書かれているかで変わります。

switch i := x.(type) {
case int:
    // i は int 型 ← 1つの型なので、その型になる
case bool, string:
    // i は interface{} 型 ← 複数の型なので、元のインターフェース型のまま
default:
    // i は interface{} 型
}

複数の型をまとめた case bool, string: では、ibool なのか string なのか確定できないため、元のインターフェース型のままになります。

nil ケース

インターフェース値が nil かどうかもチェックできます。

switch v := x.(type) {
case nil:
    fmt.Println("nil です")  // v は interface{} 型
case int:
    fmt.Println("int:", v)
}

nil は型ではなく値なので、通常の型アサーションでは直接扱えませんが、型switchでは特別に case nil と書けます。

型switchと式switchの違い

いくつかの重要な違いがあります。

1. fallthrough が使えない

switch x.(type) {
case int:
    fmt.Println("int")
    fallthrough        // コンパイルエラー! 型switchでは使えない
case string:
    fmt.Println("string")
}

型switchで fallthrough が禁止されているのは、次のケースで v の型が変わってしまい、型安全性が壊れるからです。

2. 型パラメータには使えない

func f[T any](x T) {
    switch x.(type) {  // コンパイルエラー! T はインターフェース型ではない
    }
}

x はインターフェース型でなければなりません。型パラメータは使えません。

ジェネリクスとの組み合わせ

Go 1.18以降、ケースに型パラメータを使えるようになりました。

func f[P any](x any) int {
    switch x.(type) {
    case P:            // 型パラメータをケースに使える
        return 0
    case string:
        return 1
    }
    return -1
}

f[string]("foo")  // P=string なので case P と case string が重複 → 先に書かれた case P が選ばれる → 0

インスタンス化でケースが重複した場合は、先に書かれたケースが優先されます。

よくある使い方

1. エラーの種類で処理を分ける

func handleError(err error) {
    switch e := err.(type) {
    case *os.PathError:
        fmt.Println("パスエラー:", e.Path)
    case *net.OpError:
        fmt.Println("ネットワークエラー:", e.Op)
    default:
        fmt.Println("一般エラー:", e)
    }
}

2. JSON のパースで型を判定する

func processValue(v interface{}) {
    switch val := v.(type) {
    case float64:
        fmt.Printf("数値: %f\n", val)
    case string:
        fmt.Printf("文字列: %s\n", val)
    case []interface{}:
        fmt.Printf("配列(要素数: %d)\n", len(val))
    case map[string]interface{}:
        fmt.Printf("オブジェクト(キー数: %d)\n", len(val))
    case nil:
        fmt.Println("null")
    case bool:
        fmt.Printf("真偽値: %t\n", val)
    }
}

encoding/json はデフォルトで interface{} にデコードするため、型switchで中身を判定するパターンがよく使われます。

おわりに 

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

よっしー
よっしー

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

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

コメント

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