GoのArrayとSliceについて深掘りしてみた

株式会社Schooの [@hiroto_0411](https://x.com/hiroto_0411)です！
Go研修を受けた際に、「開発だとSliceを使うことが多いです。」と言われており、ArrayとSliceの違いに興味を持ったので深掘りしてみました！

## 簡単にまとめると

-  Arrayの全てまたは一部を参照しているのがSlice

- 柔軟性の高いSliceがよく使われる

- Sliceの作成方法は使い分けられると良い

## Array
```go
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)を持っている

**参考**

https://go.dev/blog/slices-intro

### 色々なSliceの作成方法
#### 空のSliceを作成
```go
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
```go
func main(){
	var s []int = []int{1, 2}
	fmt.Println(s) //[1 2]
}
```

- 初期値を設定してSliceを作りたい場合はSlice literalが有効

#### makeを使う方法
```go
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
```go
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に含まれる要素の数
```go
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になる
```go
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へ要素を追加する
```go
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)
```go
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近くまでは容量を倍にして新しい配列を確保し、それ以降は増加率が低くなると書かれている。

**参考**

https://zenn.dev/sryoya/articles/6a8ae7daa20aa7

## 参考文献

https://go.dev/tour/moretypes/6

https://qiita.com/imoty/items/bb18fb50d526474d2d10

https://uqichi.hatenablog.com/entry/go-slice-allocate-capacity/

---
**Schooでは一緒に働く仲間を募集しています！**

https://corp.schoo.jp/careers/graduates

https://corp.schoo.jp/careers