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

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

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

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

スポンサーリンク

背景

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

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

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

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

単純スライス式(Simple slice expressions)

文字列、配列、配列へのポインタ、またはスライス a に対して、一次式

a[low : high]

は部分文字列またはスライスを構築する。インデックス lowhigh は、オペランド a のどの要素が結果に含まれるかを選択する。結果のインデックスは0から始まり、長さは highlow である。配列 a をスライスした後

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]

スライス s の型は []int、長さは3、容量は4、要素は以下のようになる

s[0] == 2
s[1] == 3
s[2] == 4

利便性のため、インデックスはいずれか省略することができる。low を省略するとデフォルトで0、high を省略するとデフォルトでスライスされるオペランドの長さになる:

a[2:]  // a[2 : len(a)] と同じ
a[:3]  // a[0 : 3] と同じ
a[:]   // a[0 : len(a)] と同じ

a が配列へのポインタである場合、a[low : high](*a)[low : high] の省略形である。

配列または文字列の場合、インデックスは 0 <= low <= high <= len(a) であれば範囲内であり、そうでなければ範囲外である。スライスの場合、上限インデックスは長さではなくスライスの容量 cap(a) である。定数インデックスは非負で、int 型の値で表現可能でなければならない。配列または定数文字列の場合、定数インデックスは範囲内でもなければならない。両方のインデックスが定数である場合、low <= high を満たさなければならない。実行時にインデックスが範囲外である場合、実行時パニックが発生する。

型なし文字列を除いて、スライスされるオペランドが文字列またはスライスの場合、スライス操作の結果はオペランドと同じ型の非定数値である。型なし文字列のオペランドの場合、結果は string 型の非定数値である。スライスされるオペランドが配列の場合、それはアドレス指定可能でなければならず、スライス操作の結果は配列と同じ要素型を持つスライスである。

有効なスライス式のスライスされるオペランドが nil スライスである場合、結果は nil スライスである。そうでない場合、結果がスライスであれば、オペランドと基底となる配列を共有する。

var a [10]int
s1 := a[3:7]   // s1 の基底配列は配列 a である; &s1[2] == &a[5]
s2 := s1[1:4]  // s2 の基底配列は s1 の基底配列であり、配列 a である; &s2[1] == &a[5]
s2[1] = 42     // s2[1] == s1[2] == a[5] == 42; すべて同じ基底配列要素を参照する

var s []int
s3 := s[:0]    // s3 == nil

解説

スライス式の基本

a[low:high] は、alow 番目から high-1 番目までを取り出します。high は含まれない点に注意してください。

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]
// s = [2, 3, 4]  ← インデックス 1, 2, 3 の要素

結果の長さは high - low になります。この例では 4 - 1 = 3 なので長さ3のスライスが得られます。

インデックスの省略

lowhigh を省略できます。

a[2:]   // a[2:len(a)] と同じ → インデックス2以降すべて
a[:3]   // a[0:3] と同じ → 先頭から3つ
a[:]    // a[0:len(a)] と同じ → 全体

a[:] は「配列をスライスに変換する」イディオムとしてよく使われます。

arr := [3]int{1, 2, 3}
s := arr[:]  // [3]int → []int に変換

スライスの容量(cap)とは

スライス式で重要な概念が**容量(capacity)**です。

スライスの正体は「配列の一部を見ている窓」です。その窓には以下の3つの情報が入っています。

  1. どこから見始めるか(ポインタ)
  2. いくつ見えているか(長さ、len
  3. 最大どこまで伸ばせるか(容量、cap
a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]
//     ^       ^
//   low=1   high=4

fmt.Println(len(s))  // 3    (4 - 1)
fmt.Println(cap(s))  // 4    (5 - 1)

cap は「low の位置から元の配列の末尾まで」の長さです。だから s は必要なら s[:4] と伸ばせます。

s2 := s[:4]   // cap の範囲内なら伸ばせる
// s2 = [2, 3, 4, 5]  ← 元の配列 a[4] まで取れた

スライス式の範囲ルール

配列・文字列とスライスでルールが少し違います。

対象インデックスの許容範囲
配列・文字列0 <= low <= high <= len(a)
スライス0 <= low <= high <= cap(a)

スライスに限っては、len を超えて cap まで指定できるのがポイントです。これは、スライスの裏にある配列がもう少し長いことを活用するためです。

a := [5]int{1, 2, 3, 4, 5}
s := a[1:3]       // len=2, cap=4
s2 := s[:4]        // len を超えて cap まで取れる!
// s2 = [2, 3, 4, 5]

ここが一番重要:基底配列は共有される

スライスを切り出しても、データがコピーされるわけではありません。 元の配列と同じメモリを共有します。

var a [10]int
s1 := a[3:7]    // s1 は a の一部を見ている窓
s2 := s1[1:4]   // s2 は s1 の一部 = 結局 a の一部

s2[1] = 42      // s2[1] を変更すると...
// s1[2] も a[5] も 42 に変わる! 全部同じ場所を指しているから

この挙動は便利な反面、バグの温床にもなります。

// よくある落とし穴
original := []int{1, 2, 3, 4, 5}
part := original[1:3]    // [2, 3]
part[0] = 999
fmt.Println(original)    // [1, 999, 3, 4, 5]  ← 元のデータも変わる!

独立したコピーが欲しい場合は、copy を使うか、append で新しいスライスを作ります。

part := make([]int, 2)
copy(part, original[1:3])  // 値をコピー
part[0] = 999
fmt.Println(original)      // [1, 2, 3, 4, 5]  ← 元は変わらない

スライスの長さと容量を視覚化

先ほどの例をもう少しじっくり見てみましょう。

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]

メモリ上のイメージはこんな感じです。

a (配列):  [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ]
                ↑              ↑
              s の始点        a の末尾
              (low=1)

s (スライス):   [ 2 ][ 3 ][ 4 ]  ← len=3
                [ 2 ][ 3 ][ 4 ][ 5 ]  ← cap=4 まで伸ばせる

len は「今見えている範囲」、cap は「最大まで伸ばしたときの範囲」です。

nilスライスをスライスするとnil

ちょっとした特殊ケースですが、nil スライスを [:0] でスライスすると結果も nil です。

var s []int
s3 := s[:0]
fmt.Println(s3 == nil)  // true

普通は [:0] すると空のスライス(nil ではない)になりそうですが、元が nil の場合は nil のまま、というのが Go の仕様です。実用上ほとんど意識する必要はありませんが、nil と空スライスを厳密に区別するコードを書くときに覚えておくと役立ちます。

おわりに 

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

よっしー
よっしー

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

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

コメント

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