
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
型switch(Type switches)
型switch は値ではなく型を比較する。それ以外は式switch と類似している。型switch は、実際の型の代わりにキーワード type を使用した型アサーションの形式を持つ特別なswitch式で示される:
switch x.(type) {
// ケース
}
ケースは、式 x の動的型に対して実際の型 T を照合する。型アサーションと同様に、x はインターフェース型でなければならないが、型パラメータであってはならない。また、ケースに列挙された各非インターフェース型 T は x の型を実装しなければならない。型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: では、i が bool なのか 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言語の言語仕様について解説しました。

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

コメント