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

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

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

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

スポンサーリンク

背景

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

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

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

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

呼び出し(Calls)

関数型 F の式 f が与えられたとき、

f(a1, a2, … an)

は引数 a1, a2, … anf を呼び出す。1つの特殊なケースを除き、引数は F のパラメータ型に代入可能な単一値の式でなければならず、関数が呼び出される前に評価される。式の型は F の結果型である。メソッド呼び出しも同様であるが、メソッド自体はメソッドのレシーバ型の値に対するセレクタとして指定される。

math.Atan2(x, y)  // 関数呼び出し
var pt *Point
pt.Scale(3.5)     // レシーバ pt によるメソッド呼び出し

f がジェネリック関数を表す場合、呼び出されるかまたは関数値として使用される前にインスタンス化されなければならない。

f の型が型パラメータである場合、その型集合のすべての型は同じ基底型を持たなければならず、その基底型は関数型でなければならない。また、関数呼び出しはその型に対して有効でなければならない。

関数呼び出しにおいて、関数値と引数は通常の順序で評価される。評価の後、関数のパラメータと結果を含む変数のために新しい記憶領域が確保される。次に、呼び出しの引数が関数に渡される。これは引数が対応する関数パラメータに代入されることを意味し、呼び出された関数が実行を開始する。関数が戻る際に、関数の戻りパラメータが呼び出し元に返される。

nil の関数値を呼び出すと実行時パニックが発生する。

特殊なケースとして、関数またはメソッド g の戻り値の数が別の関数またはメソッド f のパラメータの数と等しく、個別に代入可能である場合、呼び出し f(g(parameters_of_g))g の戻り値を順に f のパラメータに渡した上で f を呼び出す。f の呼び出しには g の呼び出し以外のパラメータを含んではならず、g は少なくとも1つの戻り値を持たなければならない。f に最後の ... パラメータがある場合、通常のパラメータの代入の後に残った g の戻り値がそれに代入される。

func Split(s string, pos int) (string, string) {
	return s[0:pos], s[pos:]
}

func Join(s, t string) string {
	return s + t
}

if Join(Split(value, len(value)/2)) != value {
	log.Panic("test fails")
}

メソッド呼び出し x.m() は、x の(型の)メソッドセットが m を含み、引数リストが m のパラメータリストに代入可能である場合に有効である。x がアドレス指定可能であり、&x のメソッドセットが m を含む場合、x.m()(&x).m() の省略形である:

var p Point
p.Scale(3.5)

独立したメソッド型は存在せず、メソッドリテラルも存在しない。


解説

関数呼び出しの基本

関数呼び出しは Go で最も頻繁に書く操作のひとつですね。基本は単純です。

result := math.Atan2(x, y)  // 関数名(引数)

引数は関数が実行される前にすべて評価されます。左から右の順に評価される点も覚えておくとよいでしょう。

func add(a, b int) int { return a + b }

// f() が先に評価され、次に g() が評価され、最後に add が呼ばれる
add(f(), g())

メソッド呼び出し

メソッド呼び出しは、レシーバに . をつけて呼びます。

var pt *Point
pt.Scale(3.5)  // pt がレシーバ、Scale がメソッド

ここで大事なのが、ポインタレシーバのメソッドを値から呼べるという省略規則です。

var p Point          // 値型の変数
p.Scale(3.5)         // Scale は *Point レシーバだけど呼べる!
// 内部的には (&p).Scale(3.5) と同じ

p がアドレス指定可能であれば、コンパイラが自動的に &p を取ってくれます。ただし、アドレス指定可能でない値からは呼べません。

Point{1, 2}.Scale(3.5)  // コンパイルエラー! 一時的な値はアドレスを取れない

関数呼び出しの裏側

原文には関数呼び出し時に内部で何が起きるかが書かれています。順を追って見てみましょう。

  1. 関数値と引数が評価される
  2. パラメータと戻り値のための新しいメモリが確保される
  3. 引数がパラメータにコピー(代入)される
  4. 関数の実行が始まる
  5. 関数が return すると、戻り値が呼び出し元に返される

ここで大事なのは、引数はコピーされるという点です。

func double(n int) {
    n = n * 2  // この n はコピーなので、呼び出し元には影響しない
}

x := 10
double(x)
fmt.Println(x)  // 10 のまま

呼び出し元の変数を変更したい場合はポインタを渡します。

func double(n *int) {
    *n = *n * 2
}

x := 10
double(&x)
fmt.Println(x)  // 20

nil の関数値を呼ぶとパニック

関数値が nil のまま呼び出すと、実行時パニックになります。

var f func(int) int  // nil の関数値
f(42)                // 実行時パニック!

コンパイル時には検出されないので注意が必要です。コールバックやハンドラを扱うときは、nil チェックをする癖をつけましょう。

if f != nil {
    f(42)  // 安全
}

特殊ケース:戻り値をそのまま引数に渡す

Go では、ある関数の複数の戻り値を、別の関数の引数にそのまま渡せます。

func Split(s string, pos int) (string, string) {
    return s[0:pos], s[pos:]
}

func Join(s, t string) string {
    return s + t
}

// Split の戻り値 (string, string) を Join の引数 (string, string) にそのまま渡せる
result := Join(Split("hello", 2))
// Split が "he", "llo" を返し、Join("he", "llo") が呼ばれて "hello" になる

ただし条件があります。

  • g の戻り値の数と型が f のパラメータにぴったり対応すること
  • f の呼び出しに g 以外の引数を含めないこと
// これはダメ! g の呼び出し以外の引数を含んでいる
result := SomeFunc(Split("hello", 2), "extra")  // コンパイルエラー!

このルールは、エラーハンドリングでよく活用されます。

func must(val int, err error) int {
    if err != nil {
        panic(err)
    }
    return val
}

// strconv.Atoi は (int, error) を返す → must にそのまま渡せる
n := must(strconv.Atoi("42"))

ジェネリック関数の呼び出し

ジェネリック関数は、呼び出す前にインスタンス化(型パラメータに具体型を当てはめる)が必要です。ただし、多くの場合はコンパイラが型を推論してくれるので、明示的に書かなくても大丈夫です。

func min[T ~int|~float64](x, y T) T {
    if x < y {
        return x
    }
    return y
}

// 型推論が効くので、型引数は省略できる
min(3, 5)         // T は int と推論される
min[int](3, 5)    // 明示的に書いてもOK

// 関数値として使うときは、先にインスタンス化が必要
f := min[int]     // インスタンス化してから変数に代入
f(3, 5)

メソッド型とメソッドリテラルは存在しない

原文の最後の一文は、Go の設計上の特徴です。

Go には「メソッド型」という独立した型も、「メソッドリテラル」(その場でメソッドを書く構文)もありません。メソッドは必ず func で宣言するものであり、関数リテラルのようにその場で作ることはできません。

// 関数リテラルはある
f := func(x int) int { return x * 2 }

// メソッドリテラルは存在しない
// p.method := func() { ... }  ← こういうことはできない

メソッドを関数値として扱いたい場合は、前回学んだメソッド式メソッド値を使うことになります。

おわりに 

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

よっしー
よっしー

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

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

コメント

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