どうも、Go言語を勉強中のとがみんです。
Goを勉強する中で「配列」と「スライス」という似たような型があり、固定長配列か可変長配列かという違いがありますが、メモリの使い方が全然違いそうで、パフォーマンスやメモリ効率の良いコードを書く上ではちゃんと理解しといた方が良さそうだなと思ったので調べてみることにしました。
この記事ではGo言語における「Array(配列)」と「slice(スライス)」について理解を深めていこうかと思います。
Arrays(配列)型とは?
配列の概要
配列とは、同じ型を持つ要素を並べたもので、GoのArrays(配列)は固定長です。
最初に宣言した配列のサイズを変更することはできません。
上記は、aという変数が、5つの要素を持つ、int型を格納する配列であることが宣言されています。
配列の構造
配列は、下記のような構造でメモリに値が格納されます。
slice(スライス型)とは?
スライスの概要
sliceは「可変長配列」を表現する型です。
sliceを表現するときは下記のようにコードを記載します。
上記は、sという変数が、int型を格納するスライスであることが宣言されています。
スライスは可変長なため、プログラムの実行時に、動的に要素数が変化します。
スライスの構造
sliceは、全ての型のポインターを表せるunsafe.Pointer型のarrayという変数と、長さlenと、容量capという要素を持ちます。
unsafe.Pointer型のarrayという変数は、配列に対するポインタを格納し、長さlenは、そのスライスに含まれる要素の数で、容量capは、スライスの最初の要素から数えて、元となる配列の要素数です。
array unsafe.Pointer
len int
cap int
}
スライスは下記のように配列へのポインタと長さと容量を持ったものとして表現されています。
スライスが配列を持っているような振る舞いをするのは、スライスが持っているポインタを元に配列を参照しているためです。長さが2であれば、参照先の値から2つ分の赤い部分を参照します。
容量を超えた要素の追加の仕組み
スライスは「可変長配列」ですが、実態はポインタにより固定長の配列を参照しています。
容量、すなわち確保した固定長の配列の長さを超える場合の要素を追加する場合は、さらに容量が2倍のメモリ領域を確保した上で、元のスライスが格納していたデータを新しい領域にコピーします。
拡張されるのは、一定の閾値に応じて、変わり必ずしも2倍というわけではないです。
1 2 3 4 5 6 7 8 | func main() { slice := []int{10,20,30,40,50} fmt.Println("長さ:",len(slice),"容量:",cap(slice),"0番目の要素のアドレス:",&slice[0]) slice = append(slice,60) fmt.Println("長さ:",len(slice),"容量:",cap(slice),"0番目の要素のアドレス:",&slice[0]) } //長さ: 5 容量: 5 0番目の要素のアドレス: 0xc000016180 //長さ: 6 容量: 10 0番目の要素のアドレス: 0xc00001c050 |
上記のように容量を超えるような値を追加した場合、アドレスが変わっています。
また、容量が5から10に変わっていることがわかります。
スライスが容量を拡張するためには、配列をコピーする処理が走るため、実行性能を落とさず良質なパフォーマンスを実現し続けるには、あらかじめスライスが保持し得る要素数が推定できる場合は、できるだけ容量の指定にも気を配る必要がありそうです。
まとめ
Go言語における配列とスライスのについて整理していきました。