株式会社Schooの @hiroto_0411です!
Go研修を受けた際に、「開発だとSliceを使うことが多いです。」と言われており、ArrayとSliceの違いに興味を持ったので深掘りしてみました!
簡単にまとめると
-
Arrayの全てまたは一部を参照しているのがSlice
-
柔軟性の高いSliceがよく使われる
-
Sliceの作成方法は使い分けられると良い
Array
func main(){
var a [10]int
fmt.Println(a) //[0 0 0 0 0 0 0 0 0 0]
a[0] = 12
a[2] = 17
fmt.Println(a) //[12 0 17 0 0 0 0 0 0 0]
b := [6]int{1, 2, 3, 4, 5, 6}
fmt.Println(b) //[1 2 3 4 5 6]
}
-
Arrayの長さ(要素数)まで型の一部である(
[10]int
で一つの型) -
要素数は変更できない
Slice
Sliceの特徴
- Sliceは要素数が可変である
- Sliceを作成したら、裏では配列が作成されている
- Sliceは作成された配列のポインタ、セグメントの長さ(length)、セグメントの容量(capacity)を持っている
参考
色々なSliceの作成方法
空のSliceを作成
func main(){
var s []int
fmt.Printf("len=%v cap=%v %v\n", len(s), cap(s), s)
//len=0 cap=0 []
}
-
長さと容量はどちらも0
-
nilとなる
Slice literal
func main(){
var s []int = []int{1, 2}
fmt.Println(s) //[1 2]
}
- 初期値を設定してSliceを作りたい場合はSlice literalが有効
makeを使う方法
func main() {
s := make([]int, 5, 10)
fmt.Printf("len=%v cap=%v %v\n", len(s), cap(s), s) //len=5 cap=10 [0 0 0 0 0]
}
- 長さと容量を指定してスライスを初期化できる
- make関数はゼロの配列を確保し、その配列を参照するスライスを返している
- 初期値を設定することはできない
- 容量を事前に確保することができるので、リアルタイム処理や、パフォーマンスが重要視される処理など無駄なallocateを防ぎたいときはmakeが有効
※allocateについては下で説明しています
Arrayの全部または一部を参照しているのがSlice
func main() {
b := [6]int{1, 2, 3, 4, 5, 6}
fmt.Println(b) //[1 2 3 4 5 6]
//スライスの作成(配列からSliceを作る方法)
c := b[0:3]
fmt.Println(c) //[1 2 3]
//要素に別の値を入れる
c[0] = 100
//b配列の1つ目の要素のaddressとc配列の1つ目の要素のaddressは同じになっている
fmt.Printf("Address of b[0]: %v\n", unsafe.Pointer(&b[0])) //Address of b[0]: 0x14000018150
fmt.Printf("Address of c[0]: %v\n", unsafe.Pointer(&c[0])) //Address of c[0]: 0x14000018150
//スライスcはbの配列を参照しているため、どちらの値も100になっている
fmt.Println(c) //[100 2 3]
fmt.Println(b) //[100 2 3 4 5 6]
}
- Sliceはデータを保持しておらず、配列の一部または全部を参照している
そのため、スライスの要素を変更すると配列の対応する箇所の要素も変更される
Sliceのlengthとcapacity
length
長さ
Sliceに含まれる要素の数
func main() {
b := [6]int{1, 2, 3, 4, 5, 6}
fmt.Printf("len=%v %v\n", len(b), b) //len=6 [1 2 3 4 5 6]
c := b[0:3]
fmt.Printf("len=%v %v\n", len(c), c) //len=3 [1 2 3]
}
capacity
容量
スライスの開始位置から参照先の配列の末尾までの要素数がcapacityになる
func main() {
b := [6]int{1, 2, 3, 4, 5, 6}
fmt.Printf("cap=%v %v\n", cap(b), b) //cap=6 [1 2 3 4 5 6]
c := b[0:1]
fmt.Printf("cap=%v %v\n", cap(c), c) //cap=6 [1]
d := b[3:5]
fmt.Printf("cap=%v %v\n", cap(d), d) //cap=3 [4 5]
}
Sliceへ要素を追加する
func main() {
var s []int
fmt.Printf("cap=%v %v\n", cap(s), s) //cap=0 []
//追加
s = append(s, 1)
fmt.Printf("cap=%v %v\n", cap(s), s) //cap=1 [1]
}
Sliceの容量を超えると新しい配列が確保される(allocation)
func main() {
b := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("cap=%d %v\n", cap(b), b) //cap=6 [1 2 3 4 5 6]
fmt.Printf("Address of b[0]: %v\n", unsafe.Pointer(&b[0])) //Address of b[0]: 0x140000b0000
c := b[0:6]
fmt.Printf("cap=%d %v\n", cap(c), c) //cap=6 [1 2 3 4 5 6]
fmt.Printf("Address of c[0]: %v\n", unsafe.Pointer(&c[0])) //Address of c[0]: 0x140000b0000
// 要素を追加して、Slice c の容量を超えるとaddressが変わっているのがわかる
c = append(c, 7)
fmt.Printf("cap=%d %v\n", cap(c), c) //cap=12 [1 2 3 4 5 6 7]
fmt.Printf("Address of c[0]: %v\n", unsafe.Pointer(&c[0])) //Address of c[0]: 0x1400008c060
}
スライスに対して append
を使用して元の配列の容量を超えると、Goランタイムは容量を増やした新しい配列を確保し、元の配列の内容と追加された要素をその新しい配列にコピーする。その結果、スライスは元の配列に影響を与えることなく拡張される。この動作により、元の配列 b
は変更されず、スライス c
は新しい配列を指すようになる。
allocationの動作については以下の記事を見つけた。
容量が1000近くまでは容量を倍にして新しい配列を確保し、それ以降は増加率が低くなると書かれている。
参考
参考文献
Schooでは一緒に働く仲間を募集しています!