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

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

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

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

スポンサーリンク

背景

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

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

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

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

セレクタ(Selectors)

パッケージ名ではない一次式 x に対して、セレクタ式

x.f

は値 x(場合によっては *x。以下を参照)のフィールドまたはメソッド f を表す。識別子 f は(フィールドまたはメソッドの)セレクタと呼ばれ、ブランク識別子であってはならない。セレクタ式の型は f の型である。x がパッケージ名である場合は、修飾識別子の節を参照のこと。

セレクタ f は型 T のフィールドまたはメソッド f を表すか、あるいは T のネストされた埋め込みフィールドのフィールドまたはメソッド f を参照することができる。f に到達するために辿る埋め込みフィールドの数を、T における f の深さと呼ぶ。T で直接宣言されたフィールドまたはメソッド f の深さはゼロである。T の埋め込みフィールド A で宣言されたフィールドまたはメソッド f の深さは、A における f の深さに1を加えたものである。

セレクタには以下のルールが適用される:

  1. T がポインタ型でもインターフェース型でもない型 T または *T の値 x に対して、x.f はそのような f が存在する T の中で最も浅い深さにあるフィールドまたはメソッドを表す。最も浅い深さの f がちょうど1つでない場合、そのセレクタ式は不正である。
  2. インターフェース型 I の値 x に対して、x.fx の動的な値が実際に持つ名前 f のメソッドを表す。I のメソッドセットに名前 f のメソッドが存在しない場合、そのセレクタ式は不正である。
  3. 例外として、x の型が定義されたポインタ型であり、(*x).f がフィールド(メソッドではない)を表す有効なセレクタ式である場合、x.f(*x).f の省略形である。
  4. その他のすべての場合において、x.f は不正である。
  5. x がポインタ型で値が nil であり、x.f が構造体フィールドを表す場合、x.f への代入または評価は実行時パニックを引き起こす。
  6. x がインターフェース型で値が nil である場合、メソッド x.f の呼び出しまたは評価は実行時パニックを引き起こす。

たとえば、以下の宣言が与えられたとき:

type T0 struct {
	x int
}

func (*T0) M0()

type T1 struct {
	y int
}

func (T1) M1()

type T2 struct {
	z int
	T1
	*T0
}

func (*T2) M2()

type Q *T2

var t T2     // t.T0 != nil とする
var p *T2    // p != nil かつ (*p).T0 != nil とする
var q Q = p

次のように書くことができる:

t.z          // t.z
t.y          // t.T1.y
t.x          // (*t.T0).x

p.z          // (*p).z
p.y          // (*p).T1.y
p.x          // (*(*p).T0).x

q.x          // (*(*q).T0).x        (*q).x は有効なフィールドセレクタ

p.M0()       // ((*p).T0).M0()      M0 は *T0 レシーバを期待する
p.M1()       // ((*p).T1).M1()      M1 は T1 レシーバを期待する
p.M2()       // p.M2()              M2 は *T2 レシーバを期待する
t.M2()       // (&t).M2()           M2 は *T2 レシーバを期待する。呼び出しの節を参照

ただし以下は不正である:

q.M0()       // (*q).M0 は有効だがフィールドセレクタではない

解説

セレクタとは

セレクタは、Go で毎日のように使う .(ドット)のことです。x.f と書いて、x のフィールドやメソッドにアクセスします。

type User struct {
    Name string
    Age  int
}

u := User{Name: "Alice", Age: 30}
fmt.Println(u.Name)  // セレクタで Name フィールドにアクセス

シンプルに見えますが、埋め込みフィールドやポインタが絡むと挙動が少し複雑になります。順番に見ていきましょう。

埋め込みフィールドと「深さ」

Go の構造体では、フィールド名を省略して型だけを書く「埋め込み」ができます。埋め込まれた型のフィールドやメソッドは、あたかも自分自身のもののようにアクセスできます。

type Animal struct {
    Name string
}

type Dog struct {
    Animal        // 埋め込み
    Breed string
}

d := Dog{Animal: Animal{Name: "Pochi"}, Breed: "Shiba"}
fmt.Println(d.Name)   // d.Animal.Name の省略形。深さ1
fmt.Println(d.Breed)  // Dog 自身のフィールド。深さ0

d.Name と書くと、コンパイラが「DogName はないけど、埋め込まれた Animal にあるな」と自動的に辿ってくれます。このとき辿った埋め込みフィールドの数が「深さ」です。

深さが同じだと曖昧でエラーになる

もし同じ深さに同じ名前のフィールドが2つあったら、コンパイラはどちらを選べばいいかわかりません。

type A struct { Name string }
type B struct { Name string }

type C struct {
    A  // A.Name が深さ1
    B  // B.Name も深さ1
}

c := C{}
fmt.Println(c.Name)  // コンパイルエラー! A.Name か B.Name か曖昧

この場合は明示的に c.A.Name または c.B.Name と書く必要があります。

ポインタの自動デリファレンス

Go では、ポインタを通じてフィールドにアクセスするとき、* を書かなくても自動的にデリファレンス(ポインタの先を見る)してくれます。

p := &User{Name: "Alice", Age: 30}

// この2つは同じ意味
fmt.Println((*p).Name)  // 明示的にデリファレンス
fmt.Println(p.Name)     // 自動デリファレンス(省略形)

これはとても便利な機能で、Go のコードでは (*p).Name と書くことはほぼありません。

原文の例を読み解く

原文の T0T1T2 の例は、埋め込みと自動デリファレンスが組み合わさったケースです。構造を整理してみましょう。

type T2 struct {
    z int    // 直接のフィールド(深さ0)
    T1       // 埋め込み。T1 は y を持つ
    *T0      // ポインタで埋め込み。T0 は x を持つ
}

この構造で t.y と書くと、実際には以下のように解釈されます。

t.y    →  t.T1.y        // T1 を辿って y にアクセス
t.x    →  (*t.T0).x     // T0 のポインタをデリファレンスして x にアクセス

p*T2 型のポインタ)の場合はさらに一段デリファレンスが加わります。

p.z    →  (*p).z
p.y    →  (*p).T1.y
p.x    →  (*(*p).T0).x  // p のデリファレンス + T0 のデリファレンス

手で書くと大変ですが、普段は p.x だけでOKです。コンパイラがすべて自動で補ってくれます。

定義されたポインタ型(Q)の制限

原文の最後にある Q の例は少し特殊です。

type Q *T2  // Q は *T2 を基底型とする定義されたポインタ型
var q Q = p

q.x はフィールドアクセスなので許可されます(ルール3の例外が適用される)。しかし q.M0() はメソッド呼び出しなので許可されません。

なぜでしょうか? ルール3はフィールドに限って省略を許可しており、メソッドには適用されないからです。定義されたポインタ型は、元の型のメソッドセットを引き継がない、という Go の設計上の判断があります。

q.x      // OK! フィールドアクセスはルール3で許可
q.M0()   // エラー! メソッド呼び出しはルール3の対象外
(*q).M0() // これなら OK(明示的にデリファレンスすれば通る)

このケースに遭遇することはめったにありませんが、「定義されたポインタ型はちょっと特別」と覚えておくとよいでしょう。

nil のときはパニック

ポインタが nil のときにフィールドにアクセスしたり、インターフェースが nil のときにメソッドを呼び出したりすると、実行時パニックが起きます。

var p *User = nil
fmt.Println(p.Name)  // 実行時パニック!

var r io.Reader = nil
r.Read(buf)          // 実行時パニック!

コンパイル時にはエラーにならないので、ポインタやインターフェースを使うときは nil チェックを忘れないようにしましょう。

if p != nil {
    fmt.Println(p.Name)  // 安全
}

おわりに 

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

よっしー
よっしー

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

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

コメント

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