スポンサーリンク
この記事は約7分で読めます。
よっしー
よっしー

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

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

スポンサーリンク

背景

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

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

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

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

return文(Return statements)

関数 F 内の return 文は F の実行を終了し、任意で1つ以上の結果値を提供する。F によって遅延された関数は、F が呼び出し元に戻る前に実行される。

ReturnStmt = "return" [ ExpressionList ] .

結果型を持たない関数では、return 文は結果値を指定してはならない。

func noResult() {
	return
}

結果型を持つ関数から値を返すには3つの方法がある:

  1. 戻り値(または複数の戻り値)を return 文に明示的にリストすることができる。各式は単一値であり、関数の結果型の対応する要素に代入可能でなければならない。
func simpleF() int {
	return 2
}

func complexF1() (re float64, im float64) {
	return -7.0, -4.0
}
  1. return 文の式リストは、複数値を返す関数への単一の呼び出しであってもよい。効果は、その関数から返された各値がそれぞれの値の型を持つ一時変数に代入され、それらの変数をリストする return 文が続くかのようになり、その時点で前のケースのルールが適用される。
func complexF2() (re float64, im float64) {
	return complexF1()
}
  1. 関数の結果型がその結果パラメータに名前を指定している場合、式リストは空であってよい。結果パラメータは通常のローカル変数として動作し、関数は必要に応じてそれらに値を代入することができる。return 文はこれらの変数の値を返す。
func complexF3() (re float64, im float64) {
	re = 7.0
	im = 4.0
	return
}

func (devnull) Write(p []byte) (n int, _ error) {
	n = len(p)
	return
}

宣言方法に関わらず、すべての結果値は関数への入口でその型のゼロ値に初期化される。結果を指定する return 文は、遅延された関数が実行される前に結果パラメータを設定する。

実装上の制限:結果パラメータと同じ名前の別のエンティティ(定数、型、または変数)が return の場所でスコープ内にある場合、コンパイラは return 文の空の式リストを禁止することがある。

func f(n int) (res int, err error) {
	if _, err := f(n-1); err != nil {
		return  // 無効な return 文: err がシャドーイングされている
	}
	return
}

解説

return文の基本

return 文は関数の実行を終了し、呼び出し元に値を返します。

func add(a, b int) int {
    return a + b  // 値を返して関数を終了
}

戻り値がない関数では return だけ書くか、省略できます。

func greet(name string) {
    fmt.Println("Hello,", name)
    return  // 省略可能。関数の末尾に自動的に return がある
}

3つの戻し方

方法1:値を明示的に返す(最も一般的)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

方法2:複数値を返す関数の結果をそのまま返す

func readConfig() (string, error) {
    return os.ReadFile("config.txt")  // ReadFile の戻り値をそのまま転送
}

この書き方はラッパー関数を書くときに便利です。

方法3:名前付き戻り値(naked return)

func divide(a, b float64) (result float64, err error) {
    if b == 0 {
        err = fmt.Errorf("division by zero")
        return  // result=0.0(ゼロ値), err=エラー値 を返す
    }
    result = a / b
    return  // result=計算結果, err=nil を返す
}

名前付き戻り値は通常のローカル変数のように使え、return に何も書かなくてもその時点の値が返されます。

名前付き戻り値の注意点

名前付き戻り値は便利ですが、短い関数でのみ使うのが推奨されます。長い関数で使うと、どの時点でどの値が設定されているのかが追いにくくなります。

// 短い関数なら読みやすい
func (d devnull) Write(p []byte) (n int, _ error) {
    n = len(p)
    return
}

// 長い関数では明示的に返すほうが読みやすい
func processData(data []byte) (result []byte, err error) {
    // ... 50行の処理 ...
    return result, nil  // 明示的に書いたほうがわかりやすい
}

戻り値はゼロ値で初期化される

名前付きかどうかに関わらず、すべての戻り値は関数の開始時にゼロ値で初期化されます。

func f() (x int, s string, p *int) {
    return  // x=0, s="", p=nil が返される
}

これは「何も設定しなくても安全な値が返る」ことを保証しています。

defer との関係

return 文は結果パラメータを設定した後にdefer された関数が実行されます。これを利用して、defer で戻り値を変更できます。

func readFile(name string) (data []byte, err error) {
    f, err := os.Open(name)
    if err != nil {
        return
    }
    defer func() {
        if closeErr := f.Close(); closeErr != nil && err == nil {
            err = closeErr  // 名前付き戻り値 err を defer 内で変更!
        }
    }()

    data, err = io.ReadAll(f)
    return
}

defer の中で名前付き戻り値 err を変更できるのは、return が結果を設定してから defer が実行される、という順序が保証されているからです。このパターンはリソースのクリーンアップとエラーハンドリングを組み合わせるときに非常に役立ちます。

シャドーイングの罠

原文の最後の例は、名前付き戻り値とシャドーイングが組み合わさった落とし穴です。

func f(n int) (res int, err error) {
    if _, err := f(n-1); err != nil {
        //     ^^^^ この err は新しい変数(:= で宣言)
        return  // コンパイルエラー! 戻り値の err ではなく、ローカルの err が見えている
    }
    return
}

if の初期化文で := を使って err を宣言すると、外側の名前付き戻り値 errシャドーイング(隠蔽)されます。空の return は名前付き戻り値を返そうとしますが、同じ名前の別の変数がスコープにあるため、コンパイラはどちらの err を返すべきか曖昧になります。

修正方法はいくつかあります。

// 方法1:= で代入する(新しい変数を作らない)
if _, err = f(n-1); err != nil {
    return
}

// 方法2:明示的に戻り値を書く
if _, err := f(n-1); err != nil {
    return 0, err
}

おわりに 

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

よっしー
よっしー

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

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

コメント

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