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

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

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

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

スポンサーリンク

背景

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

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

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

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

インデックス式(Index expressions)

以下の形式の一次式

a[x]

は、x でインデックス付けされた配列、配列へのポインタ、スライス、文字列、またはmap a の要素を表す。値 x はそれぞれインデックスまたはmapキーと呼ばれる。以下のルールが適用される。

a がmapでも型パラメータでもない場合:

  • インデックス x は型なし定数であるか、その型が整数型、または整数型のみを含む型集合を持つ型パラメータでなければならない
  • 定数インデックスは非負で、int 型の値で表現可能でなければならない
  • 型なしの定数インデックスには int 型が与えられる
  • 0 <= x < len(a) の場合、インデックス x は範囲内である。そうでなければ範囲外である

a が配列型 A の場合:

  • 定数インデックスは範囲内でなければならない
  • x が実行時に範囲外である場合、実行時パニックが発生する
  • a[x] はインデックス x の配列要素であり、a[x] の型は A の要素型である

a が配列へのポインタ型の場合:

  • a[x](*a)[x] の省略形である

a がスライス型 S の場合:

  • x が実行時に範囲外である場合、実行時パニックが発生する
  • a[x] はインデックス x のスライス要素であり、a[x] の型は S の要素型である

a が文字列型の場合:

  • 文字列 a も定数である場合、定数インデックスは範囲内でなければならない
  • x が実行時に範囲外である場合、実行時パニックが発生する
  • a[x] はインデックス x の非定数のバイト値であり、a[x] の型は byte である
  • a[x] に代入することはできない

a がmap型 M の場合:

  • x の型は M のキー型に代入可能でなければならない
  • mapにキー x のエントリが含まれている場合、a[x] はキー x のmap要素であり、a[x] の型は M の要素型である
  • mapが nil であるか、そのようなエントリが含まれていない場合、a[x]M の要素型のゼロ値である

a が型パラメータ型 P の場合:

  • インデックス式 a[x] は、P の型集合に含まれるすべての型の値に対して有効でなければならない
  • P の型集合に含まれるすべての型の要素型は同一でなければならない。この文脈では、文字列型の要素型は byte である
  • P の型集合にmap型が含まれる場合、その型集合のすべての型がmap型でなければならず、それぞれのキー型もすべて同一でなければならない
  • a[x] は、P がインスタンス化される型引数のインデックス x の配列、スライス、または文字列の要素、もしくはキー x のmap要素であり、a[x] の型は(同一の)要素型である
  • P の型集合に文字列型が含まれる場合、a[x] に代入することはできない

それ以外の場合、a[x] は不正である。

map a(型 map[K]V)に対するインデックス式が、以下の特別な形式の代入文または初期化で使用される場合

v, ok = a[x]
v, ok := a[x]
var v, ok = a[x]

追加の型なし真偽値を生成する。キー x がmapに存在する場合、ok の値は true であり、そうでなければ false である。

nil マップの要素への代入は、実行時パニックを引き起こす。


解説

インデックス式ってなに?

インデックス式は a[x] の形で書く、おなじみの要素アクセス構文です。配列・スライス・文字列・map、そのすべてで使えます。

arr := [3]int{10, 20, 30}
s := []string{"a", "b", "c"}
str := "hello"
m := map[string]int{"one": 1}

arr[0]      // 10
s[1]        // "b"
str[0]      // 104 ('h' のバイト値)
m["one"]    // 1

見た目は同じ a[x] でも、対象の型によって挙動が少しずつ違います。

インデックスは整数

インデックス x は整数型でなければなりません。そして、配列・スライス・文字列では 0 から len(a) - 1 までの範囲である必要があります。

範囲外のインデックスを使うとどうなるかは、定数か実行時かで変わります。

arr := [3]int{10, 20, 30}

arr[5]   // 定数インデックスが範囲外 → コンパイルエラー

i := 5
arr[i]   // 実行時に範囲外 → 実行時パニック

コンパイル時にバグを見つけられるなら、早い段階でエラーになってくれるわけですね。

配列ポインタは自動デリファレンス

配列へのポインタでも [] でそのままアクセスできます。

arr := [3]int{10, 20, 30}
p := &arr

p[0]       // (*p)[0] の省略形。10 が得られる

わざわざ (*p)[0] と書かなくていい、という便利な糖衣構文です。

文字列のインデックスは「バイト」

ここは注意が必要なポイントです。文字列を [] でアクセスすると、文字ではなくバイトが返ってきます。

s := "hello"
fmt.Println(s[0])  // 104 ('h' のバイト値)

ASCII だけならこれで問題ないのですが、日本語のようなマルチバイト文字を扱うときは落とし穴になります。

s := "こんにちは"
fmt.Println(s[0])   // 227 (「こ」の最初のバイト。文字そのものではない!)
fmt.Println(len(s)) // 15  (5文字だが、バイト数は15)

文字単位で扱いたい場合は、rune に変換するか range を使います。

s := "こんにちは"
runes := []rune(s)
fmt.Println(runes[0])        // 12371 (「こ」の Unicode コードポイント)
fmt.Println(string(runes[0])) // "こ"

for i, r := range s {
    fmt.Printf("%d: %c\n", i, r)  // 文字単位で取り出せる
}

また、文字列の要素は読み取り専用です。s[0] = 'H' のような代入はできません。文字列は不変(イミュータブル)だからです。

mapの特殊な挙動

mapのインデックス式には、他とは違う2つの特徴があります。

1. 存在しないキーでもパニックしない

m := map[string]int{"one": 1}

v := m["zero"]       // パニックしない! int のゼロ値 0 が返る

これは便利な反面、「本当にキーが存在するのか、それともゼロ値が入っているのか」を区別できないという問題があります。

2. v, ok の2値受け取り

そこで登場するのが、原文の最後で説明されている 2値受け取りの特別な形式です。

v, ok := m["zero"]
if ok {
    fmt.Println("キーが存在する:", v)
} else {
    fmt.Println("キーが存在しない")
}

ok は真偽値で、キーが存在すれば true、なければ false になります。これは map アクセスの定番パターンなので、ぜひ覚えておいてください。

nilマップへの代入はパニック

mapを nil のまま使うと、読み取りはできますが代入はパニックします。

var m map[string]int  // nil マップ

v := m["foo"]     // OK! ゼロ値 0 が返る
m["foo"] = 1      // 実行時パニック!

nil のままでは値を入れる箱がないので、必ず make で初期化してから使います。

m := make(map[string]int)  // 初期化
m["foo"] = 1               // OK!

または、リテラルで作ってもよいです。

m := map[string]int{}  // 空のmapを作る
m["foo"] = 1           // OK!

型パラメータに対するインデックス式

ジェネリクスと組み合わせる場合のルールは少し複雑です。型パラメータ P に対して a[x] を書くには、P の型集合に含まれるすべての型で a[x] が意味をなす必要があります。

// OK:スライスも配列も [] でアクセス可能
func First[T any, S ~[]T | ~[3]T](s S) T {
    return s[0]
}

ただし、スライスと文字列を混ぜるような制約は工夫が必要です。

// 両方とも [] でアクセスできるが、要素型が byte で一致するならOK
func First[S ~[]byte | ~string](s S) byte {
    return s[0]  // 要素型はどちらも byte
}

さらに、型集合に文字列型が含まれる場合、a[x] への代入は禁止されます(文字列は不変だから)。普段の開発で気にすることは少ないですが、ライブラリを作るときの制約として覚えておくとよいでしょう。

おわりに 

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

よっしー
よっしー

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

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

コメント

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