
こんにちは。よっしーです(^^)
本日は、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を加えたものである。
セレクタには以下のルールが適用される:
Tがポインタ型でもインターフェース型でもない型Tまたは*Tの値xに対して、x.fはそのようなfが存在するTの中で最も浅い深さにあるフィールドまたはメソッドを表す。最も浅い深さのfがちょうど1つでない場合、そのセレクタ式は不正である。- インターフェース型
Iの値xに対して、x.fはxの動的な値が実際に持つ名前fのメソッドを表す。Iのメソッドセットに名前fのメソッドが存在しない場合、そのセレクタ式は不正である。 - 例外として、
xの型が定義されたポインタ型であり、(*x).fがフィールド(メソッドではない)を表す有効なセレクタ式である場合、x.fは(*x).fの省略形である。 - その他のすべての場合において、
x.fは不正である。 xがポインタ型で値がnilであり、x.fが構造体フィールドを表す場合、x.fへの代入または評価は実行時パニックを引き起こす。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 と書くと、コンパイラが「Dog に Name はないけど、埋め込まれた 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 と書くことはほぼありません。
原文の例を読み解く
原文の T0、T1、T2 の例は、埋め込みと自動デリファレンスが組み合わさったケースです。構造を整理してみましょう。
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言語の言語仕様について解説しました。

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

コメント