読者です 読者をやめる 読者になる 読者になる

kakts-log

programming について調べたことを整理していきます

Goのスライスについて

Goのスライスについて

Go Slices: usage and internals - The Go Blog
Go言語において固定長のサイズの配列とは別に、要素の追加に応じて自由にサイズを拡張できるスライスという型があります。

スライスのデータ構造

スライスのデータ構造としては、ソースコードを読んでみるのが早くて、以下のurlにまとまっています。 https://github.com/golang/go/blob/master/src/runtime/slice.go#L11-L15

type slice struct {
    array unsafe.Pointer // 配列のポインタ
    len   int   // スライスが現在もっている要素数
    cap int    // スライスのサイズ
}

スライスの内部的には、配列のポインタを持っているため、従来の配列と同様の操作が行えます。

さらには、現在スライスが現在持っている要素数lenをもっていて、要素を追加するたびにこの値が増加していきます。 lenとは別に、スライスの宣言時に初期化した要素に応じてcapも決められます。 それと同時に、lenとcapをチェックし、capの値を超えたら、古いcapサイズの2倍のサイズのメモリを確保(malloc)し、同じ要素でサイズが2倍のスライスを新たに作ります。

このあたりの処理はsliceの内部実装で使われているgrowsliceという関数で実現されています。 go/slice.go at master · golang/go · GitHub
読んでみると、スライス拡張の際に新たにメモリサイズを計算し、mallocして新たにメモリが割り当てられていることが理解できると思います。

スライスの宣言

スライスを使う際に、宣言する方法は配列のものと非常に似ていて、下記のように書くことができます。 makeを使ってcapとlenを指定した上で宣言する事もできます。

// 配列の宣言
// 宣言時に[]にサイズを指定する。固定長のため、サイズが増えることはない
arr := [4]int{1, 2, 3, 4}


// スライスの宣言
// サイズは指定せず、4つの要素を指定して初期化している。
sl := []int{1, 2, 3, 4}

// makeを使って、サイズを決めた上で宣言も行える
sl := make([]int, 4, 8)

スライスの要素の操作

スライスは宣言時に自由に要素を指定することができ、指定した要素に応じて長さが決まることを説明しました。

このスライスに対して、様々な操作を行うことができます。

sl := []int{1, 2, 3, 4}
// 元のスライスの1番目から(4-1)番目の要素をもつスライスを取得
fmt.Println(sl[1:4]) //  [2 3 4]と表示
// 元のスライスの0番目から(2-1)番目の要素をもつスライスを取得
fmt.Println(sl[:2]) // [1 2]と表示

そして、もちろんスライスの特定の要素に対して値を代入することもできます。

sl := []int{1, 2, 3, 4}
sl[2] = 100
fmt.Println(sl) // [1 2 100 4]と表示

また、別の変数に対して、既に宣言したスライスの参照を渡すことができます。

sl := []int{1, 2, 3, 4}

// sl2にslの参照を渡す
sl2 := sl

fmt.Println(sl) // [1 2 3 4]と表示
fmt.Println(sl2) // [1 2 3 4]と表示

// slの特定の要素を変更すると、同じ参照を持つsl2の方でも値が変わっているのを確認できる。
sl[2] = 100
fmt.Println(sl) // [1 2 100 4]と表示
fmt.Println(sl2) // [1 2 100 4]と表示

上記のように、別の変数に参照を渡すことで、同じ実体を複数の変数から見ることができます。

appendを用いたスライスの要素の追加

既に宣言済みのスライスに対して、要素を追加する際にはappend()関数を使って行います。 ここでは、len, capが4のスライスに対して、appendを行ってみます。

sl := []int{1, 2, 3, 4}

// sl: len 4  cap 4   slは [1 2 3 4]
fmt.Println("sl: len, %d cap %d", len(sl), cap(sl), sl)

// len,capが4で要素が満杯の状態でappendを行う
sl3 = append(sl, 5)
// sl: len 5  cap 8       sl は [1 2 3 4 5]
fmt.Printf("sl: len, %d cap %d", len(sl), cap(sl))

既に要素が満杯のスライスに対して appendを行うので、前述したとおり、新たにcapサイズを2倍の8にした上で、5を追加したスライスを返しています。
内部的にはmallocをしているので、append前と後では、変数slが指すポインタアドレスは別のものになっています。
スライスを使って実装をする上で、スライスのデータ構造、内部実装を理解していないと思わぬバグになりそうなので、ここは非常に注意が必要です。