スライスとは
スライス([]T)は、同じ型の要素を順序付きで並べた可変長の列です。配列[n]Tと違い、長さは実行中に変えられます。
スライスの内部構造
スライス値は、要素が並ぶ元配列への参照、現在見えている要素数(len)、参照先配列の容量(cap)の3つから成るヘッダです。capは再割り当てなしに要素を追加できる上限を表します。
スライスを代入したり関数に渡したりすると、ヘッダがコピーされます。元配列は共有されるため、一方で要素を書き換えると他方からも見えます。s[low:high]の部分スライスも、同じ元配列の一部を指します。
スライスの作成
nums := []int{1, 2, 3}
buf := make([]byte, 0, 64) // len=0, cap=64リテラルやmakeで作ります。要素の追加はappendで行います。
スライシングと共有
s[low:high]は元配列の一部を指すビューです。ヘッダは別ですが、要素は同じ元配列を共有します。
all[low:high]はlow以上high未満の要素を切り出します。
all := []int{0, 1, 2, 3, 4}
mid := all[1:4] // midは[1, 2, 3]、len=3
mid[0] = 99
fmt.Println(all) // [0 99 2 3 4]midとallは別のヘッダですが、要素は同じ元配列を指します。mid側を書き換えるとallからも見えます。
2要素式の容量はcap(元) - lowです。allのcapは5、lowは1なので、midのcapは4です。
fmt.Println(len(mid), cap(mid)) // 3, 43インデックス形式s[low:high:max]は、capをmax - lowに制限します。見えている範囲の末尾までcapを合わせたいときに使います。
fullCap := all[1:4:4] // len=3, cap=3(max-low = 4-1)
fmt.Println(len(fullCap), cap(fullCap)) // 3, 3部分スライスへappendしたとき、元のスライスからも追記が見えるかどうかは、そのスライスのcapに空きがあるかで決まります。
all := []int{0, 1, 2, 3, 4}
wide := all[1:4] // len=3, cap=4(あと1要素分の空きあり)
wide = append(wide, 9)
fmt.Println(all) // [0 1 2 3 9](元配列に書き込まれた)
all = []int{0, 1, 2, 3, 4}
tight := all[1:4:4] // len=3, cap=3(空きなし)
tight = append(tight, 9) // 別配列に再割り当て
fmt.Println(all) // [0 1 2 3 4](allは変わらない)共有を断ちたいときは、makeで新しい元配列を確保し、copyで要素だけをコピーします。dstとmidは別の元配列を指すため、dstを書き換えてもmidやallは変わりません。
all := []int{0, 1, 2, 3, 4}
mid := all[1:4] // [1, 2, 3]
dst := make([]int, len(mid)) // 新しい元配列を確保
copy(dst, mid) // 要素だけコピー
dst[0] = 0
fmt.Println(dst[0], mid[0]) // 0, 1(dstを変えてもmidは変わらない)nilスライスと空スライス
| 式 | len | cap | == nil |
|---|---|---|---|
var s []int | 0 | 0 | true |
s := []int{} | 0 | 0 | false |
make([]int, 0) | 0 | 0 | false |
いずれもappendは可能です。
appendのルール
appendは次のように動きます。
- 現在のcapに余裕があれば、元配列に書き込み、lenを増やした新しいヘッダを返す
- capが足りなければ、より大きな新配列を確保し、要素をコピーしてから追記する
appendはlenが増えた新しいヘッダを返します。戻り値を変数へ代入しないと、その変数のlenは変わりません。append(s, x)と書いただけではsは更新されません。
nums := []int{1, 2, 3}
nums = append(nums, 4, 5) // 末尾に要素4と5を追加 → [1, 2, 3, 4, 5]
s := []int{1}
append(s, 2) // 戻り値を捨てる → len(s)は1のまま
s = append(s, 2) // 代入するとlen=2
v := []int{1}
t := v
t = append(t, 99) // tだけ更新(len=2)
fmt.Println(len(v), len(t)) // 1, 2(vは変わらない)
v = append(v, 99) // 戻り値をvへ代入すると反映
fmt.Println(len(v)) // 2関数に渡るのはヘッダのコピーです。関数内でs = append(s, x)しても、呼び出し元の変数へ戻ったヘッダは更新されないため、呼び出し元から見たlenは変わりません。ただしヘッダには元配列への参照も含まれます。capに余裕があり再割り当てが起きない場合は、元配列へ書き込まれることがあります。
func grow(s []int) {
s = append(s, 99) // 関数内のヘッダだけlen=2になる
}
data := make([]int, 1, 2) // len=1, cap=2
grow(data)
fmt.Println(len(data)) // 1のまま
fmt.Println(data[1]) // 99(元配列に書き込まれている)
narrow := []int{1} // len=1, cap=1
grow(narrow)
fmt.Println(len(narrow), narrow) // 1, [1](cap不足で別配列に再割り当て)二次元スライス
スライスのスライスで行列を表現できます。
rows, cols := 2, 3
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}マップとは
マップ(map[K]V)はキーから値を引く連想配列です。
比較可能なキーだけ
キー型Kは比較演算==と!=が定義されている型に限られます。スライス、マップ、関数は比較できないためキーに使えません。構造体はすべてのフィールドが比較可能なときだけキーにできます。
マップの作成と操作
makeで空のマップを作り、キーへ値を代入します。
scores := make(map[string]int) // map[string]int{"go": 90} でも初期化できる
scores["go"] = 90
scores["rust"] = 85存在するキーはscores[k]で読み取れます。キーがあるかどうかを区別したいときは、2値受け取りでokを使います。okがfalseならキーは無く、vは値型のゼロ値です。
fmt.Println(scores["go"]) // 90
v, ok := scores["java"]
fmt.Println(v, ok) // 0, false(キーが無い)不要なキーはdeleteで削除します。
delete(scores, "go")
fmt.Println(scores["go"], scores["rust"]) // 0, 85(goはゼロ値)var m map[string]intのままではnilマップです。書き込みにはmakeかリテラルでの初期化が必要です。
var m map[string]int
// m["k"] = 1 // panic: nilマップへの書き込み
v, ok := m["k"] // v=0, ok=false(読み取りは可)
delete(m, "k") // キーが無くても安全走査
マップのrange走査順序は言語仕様で定められていません。実装は走査のたびに順序をランダム化し、プログラムが「キーが常にこの順で出る」と依存するのを防ぎます。ハッシュ表の内部構造に依存したバグを減らすための意図的な仕様です。
scores := map[string]int{"go": 90, "rust": 85, "java": 80}
for lang, score := range scores {
fmt.Println(lang, score)
}
// 走査するたびにキーの出力順は変わることがある(順序に意味を持たせない)キーの昇順など、決まった順で処理したいときは、キーをスライスに集めてソートしてから値を参照します。
keys := make([]string, 0, len(scores))
for lang := range scores {
keys = append(keys, lang)
}
sort.Strings(keys)
for _, lang := range keys {
fmt.Println(lang, scores[lang]) // go, java, rustの順(キー名で昇順)
}スライスをrangeするとき、走査中にappendなどでスライス自体を変更すると、意図しないループ回数や要素の見落としが起きます。次のように書かないでください。
// 悪い例: 走査中にスライスを変更する
s := []int{1, 2, 3}
for i, v := range s {
s = append(s, v) // ループ回数や処理対象が不定になる
}