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

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

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

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

スポンサーリンク

背景

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

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

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

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

型アサーション(Type assertions)

型パラメータではないインターフェース型の式 x と型 T に対して、一次式

x.(T)

は、xnil でないこと、および x に格納されている値が型 T であることを表明する。表記 x.(T) は型アサーションと呼ばれる。

より正確には、T がインターフェース型でない場合、x.(T)x の動的型が型 T と同一であることを表明する。この場合、Tx の(インターフェース)型を実装していなければならない。そうでなければ、x が型 T の値を格納することは不可能であるため、型アサーションは無効である。T がインターフェース型である場合、x.(T)x の動的型がインターフェース T を実装することを表明する。

型アサーションが成立する場合、式の値は x に格納されている値であり、その型は T である。型アサーションが偽である場合、実行時パニックが発生する。言い換えれば、x の動的型は実行時にしかわからないが、正しいプログラムにおいて x.(T) の型は T であることが確定している。

var x interface{} = 7          // x は動的型 int と値 7 を持つ
i := x.(int)                   // i は型 int と値 7 を持つ

type I interface { m() }

func f(y I) {
	s := y.(string)        // 不正: string は I を実装しない(メソッド m が欠けている)
	r := y.(io.Reader)     // r は型 io.Reader を持ち、y の動的型は I と io.Reader の両方を実装していなければならない
	…
}

以下の特別な形式の代入文または初期化で使用される型アサーションは

v, ok = x.(T)
v, ok := x.(T)
var v, ok = x.(T)
var v, ok interface{} = x.(T) // v と ok の動的型はそれぞれ T と bool

追加の型なし真偽値を生成する。アサーションが成立する場合、ok の値は true である。そうでなければ false であり、v の値は型 T のゼロ値である。この場合、実行時パニックは発生しない。


解説

型アサーションってなに?

型アサーションは、インターフェース型の変数から、具体的な型の値を取り出す仕組みです。

Go のインターフェース型の変数は、中にどんな型の値が入っているかが外からはわかりません。型アサーションは「この中身、本当は int でしょ?」とコンパイラに伝えて、その型として取り出す操作です。

var x interface{} = "hello"

s := x.(string)   // 「中身は string のはず!」と表明して取り出す
fmt.Println(s)     // "hello"

身近な例えで言うと、贈り物の箱(インターフェース)を受け取って、「この中身はケーキだよね?」と確認して開ける作業に似ています。正しければケーキが手に入り、間違っていたらパニック(びっくり!)になります。

2つのパターン:具体型とインターフェース型

T の種類によって、型アサーションの意味が少し変わります。

T が具体型の場合:「この中身はまさにこの型だよね?」

var x interface{} = 42

i := x.(int)       // OK! 中身は int なので成功
s := x.(string)    // 実行時パニック! 中身は string ではない

T がインターフェース型の場合:「この中身はこのインターフェースを実装してるよね?」

var x interface{} = os.Stdout

r := x.(io.Reader)   // OK! *os.File は io.Reader を実装している
w := x.(io.Writer)   // OK! *os.File は io.Writer も実装している

この場合、取り出された値の型は io.Reader になります。元の具体型(*os.File)の情報は保持されますが、コンパイル時の型としては io.Reader として扱われます。

コンパイル時のチェック

型アサーションには、コンパイル時に弾かれるケースがあります。原文の y.(string) の例を見てみましょう。

type I interface { m() }

func f(y I) {
    s := y.(string)  // コンパイルエラー!
}

string 型は m() メソッドを持っていないので、I を実装していません。つまり、y の中に string が入っていることは原理的にありえないのです。コンパイラはこれを検出してエラーにしてくれます。

一方、以下の場合はコンパイルが通ります。

r := y.(io.Reader)  // コンパイルOK(成功するかは実行時に決まる)

io.Reader を実装しつつ m() メソッドも持つ型は存在しうるので、コンパイラは通します。実際に成功するかは、y の中身次第です。

パニックしない安全な書き方:v, ok パターン

型アサーションが失敗するとパニックが起きますが、2値で受け取る形式を使えばパニックを避けられます。

var x interface{} = "hello"

// 危険な書き方:失敗するとパニック
i := x.(int)  // パニック!

// 安全な書き方:失敗しても ok が false になるだけ
i, ok := x.(int)
if ok {
    fmt.Println("int の値:", i)
} else {
    fmt.Println("int ではなかった")  // こちらが実行される
}

失敗した場合、v には T のゼロ値が入り、okfalse になります。パニックは起きません。

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

よくある使い方

1. 型による分岐

func describe(x interface{}) {
    if s, ok := x.(string); ok {
        fmt.Println("文字列:", s)
    } else if i, ok := x.(int); ok {
        fmt.Println("整数:", i)
    } else {
        fmt.Println("その他の型")
    }
}

ただし、複数の型で分岐したい場合は型アサーションよりも 型スイッチ(type switch) のほうが読みやすいことが多いです。

func describe(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Println("文字列:", v)
    case int:
        fmt.Println("整数:", v)
    default:
        fmt.Println("その他の型")
    }
}

2. インターフェースの機能チェック

あるインターフェースの値が、追加の機能を持っているか確認するときにも使います。

type Closer interface {
    Close() error
}

func finish(r io.Reader) {
    // Reader が Close もできるなら閉じる
    if c, ok := r.(Closer); ok {
        c.Close()
    }
}

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

原文の冒頭に「型パラメータではないインターフェース型の式」とあります。型パラメータに対しては型アサーションを使えません。

func f[T any](x T) {
    s := x.(string)  // コンパイルエラー! T は型パラメータ
}

型パラメータの場合は、型制約で型を絞るか、any 型に変換してから型アサーションを使う必要があります。

func f[T any](x T) {
    s := any(x).(string)  // any に変換してからアサーション
}

おわりに 

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

よっしー
よっしー

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

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

コメント

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