
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
スライスへの追加とコピー(Appending to and copying slices)
組み込み関数 append と copy は、一般的なスライス操作を支援する。両方の関数において、引数が参照するメモリが重複しているかどうかに関わらず、結果は独立している。
可変長引数関数 append は、型 S のスライス s にゼロ個以上の値 x を追加し、結果のスライス(これも型 S)を返す。値 x は型 ...E のパラメータに渡される。ここで E は S の要素型であり、対応するパラメータ渡しのルールが適用される。特殊なケースとして、append は []byte 型に代入可能な型のスライスに対して、string 型の第2引数に ... を続ける形も受け付ける。この形式は文字列のバイトを追加する。
append(s S, x ...E) S // E は S の要素型
S が型パラメータの場合、その型集合のすべての型は同じ基底スライス型 []E を持たなければならない。
s の容量が追加の値を収めるのに十分でない場合、append は既存のスライス要素と追加の値の両方を収める、十分に大きい新しい基底配列を割り当てる。そうでなければ、append は基底配列を再利用する。
s0 := []int{0, 0}
s1 := append(s0, 2) // 単一要素を追加 s1 は []int{0, 0, 2}
s2 := append(s1, 3, 5, 7) // 複数要素を追加 s2 は []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...) // スライスを追加 s3 は []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...) // 重複するスライスを追加 s4 は []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
var t []interface{}
t = append(t, 42, 3.1415, "foo") // t は []interface{}{42, 3.1415, "foo"}
var b []byte
b = append(b, "bar"...) // 文字列の内容を追加 b は []byte{'b', 'a', 'r' }
関数 copy はソース src からデスティネーション dst にスライス要素をコピーし、コピーされた要素数を返す。両方の引数は同一の要素型 E を持ち、型 []E のスライスに代入可能でなければならない。コピーされる要素数は len(src) と len(dst) の最小値である。特殊なケースとして、copy は []byte 型に代入可能なデスティネーション引数と string 型のソース引数も受け付ける。この形式は文字列からバイトスライスにバイトをコピーする。
copy(dst, src []T) int
copy(dst []byte, src string) int
一方または両方の引数の型が型パラメータの場合、それぞれの型集合のすべての型は同じ基底スライス型 []E を持たなければならない。
例:
var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:]) // n1 == 6, s は []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:]) // n2 == 4, s は []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!") // n3 == 5, b は []byte("Hello")
解説
append の基本
append は、スライスに要素を追加して新しいスライスを返す関数です。Go のスライス操作で最もよく使う組み込み関数ですね。
s := []int{1, 2, 3}
s = append(s, 4) // 1つ追加
s = append(s, 5, 6, 7) // 複数追加
fmt.Println(s) // [1 2 3 4 5 6 7]
必ず結果を代入する
以前の式文の節で学んだとおり、append は式文として使えません。結果を変数に代入しないと何も起きません。
append(s, 4) // コンパイルエラー! 結果を使っていない
s = append(s, 4) // ✅ 結果を s に代入
なぜ s = append(s, 4) と自分自身に代入し直す必要があるのでしょうか? それは append が元のスライスを変更するのではなく、新しいスライスを返すからです。容量が足りない場合、新しい基底配列が確保され、ポインタが変わります。
容量が足りるかどうかで動作が変わる
ここが append で最も理解すべきポイントです。
容量に余裕がある場合:基底配列を再利用
s := make([]int, 2, 5) // len=2, cap=5
s[0], s[1] = 10, 20
s2 := append(s, 30)
// s と s2 は同じ基底配列を共有!
// s2[0] を変更すると s[0] も変わる
容量が足りない場合:新しい基底配列を確保
s := []int{10, 20} // len=2, cap=2
s2 := append(s, 30)
// 容量が足りないので、新しい基底配列が確保される
// s と s2 は別々の基底配列を持つ
この挙動を意識しないと、基底配列の共有によるバグが起きることがあります。以前学んだ完全スライス式 s[low:high:max] で容量を制限するのが、この問題への対策でしたね。
スライス同士の結合
スライスを別のスライスに追加するには ... を使います。
a := []int{1, 2, 3}
b := []int{4, 5, 6}
a = append(a, b...) // b の全要素を a に追加
// a = [1 2 3 4 5 6]
... がないとコンパイルエラーになります。append の第2引数以降は ...E 型(個別の要素)として渡す必要があるからです。
文字列を []byte に追加する
特殊なケースとして、[]byte に文字列を追加できます。
var b []byte
b = append(b, "hello"...) // 文字列のバイトを追加
// b = [104 101 108 108 111]
文字列をバイトスライスに変換して結合したいときに便利です。
copy の基本
copy は、スライスの要素を別のスライスにコピーする関数です。
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)
fmt.Println(n) // 3(コピーされた要素数)
fmt.Println(dst) // [1 2 3]
コピーされる要素数は len(src) と len(dst) の小さいほうです。はみ出す分は無視されます。
copy の安全性:重複があっても大丈夫
copy は、ソースとデスティネーションが同じ基底配列を共有していても正しく動きます。
s := []int{0, 1, 2, 3, 4, 5}
copy(s, s[2:]) // 自分自身の一部を自分にコピー
fmt.Println(s) // [2 3 4 5 4 5]
内部的にメモリの重複を正しく処理してくれるので、安心して使えます。
copy で文字列からバイトスライスに
append と同様に、copy も文字列から []byte へのコピーを特別にサポートしています。
b := make([]byte, 5)
n := copy(b, "Hello, World!")
fmt.Println(n) // 5(dst の長さ分だけコピー)
fmt.Println(string(b)) // "Hello"
よくある使い方
1. スライスの独立したコピーを作る
original := []int{1, 2, 3, 4, 5}
clone := make([]int, len(original))
copy(clone, original)
// clone を変更しても original に影響しない
Go 1.21 以降なら slices.Clone も使えます。
2. スライスの先頭に要素を挿入する
s := []int{2, 3, 4}
s = append([]int{1}, s...) // 先頭に 1 を追加
// s = [1 2 3 4]
3. スライスから要素を削除する
s := []int{1, 2, 3, 4, 5}
i := 2 // インデックス 2 の要素を削除
s = append(s[:i], s[i+1:]...)
// s = [1 2 4 5]
4. nil スライスへの append
nil スライスにも append できます。make で事前に作っておく必要はありません。
var s []int // nil スライス
s = append(s, 1, 2) // OK! 自動的に基底配列が確保される
fmt.Println(s) // [1 2]
これは Go の便利な特徴です。「最初は空で、必要に応じて追加していく」というパターンが自然に書けます。
おわりに
本日は、Go言語の言語仕様について解説しました。

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

コメント