🚕

【Go言語学習#2】~配列とスライス~

2023/10/21に公開

はじめに

最近Go言語を学習し始めたので、基本文法の定着を図るために記事にまとめていきます。今回は配列スライスに焦点を当てて学習していきます。

参考書
https://www.amazon.co.jp/dp/4814400047?tag=baldandersinf-22&linkCode=ogi&th=1&psc=1

配列

Go言語の配列は固定長なので後から配列の大きさを変更することはできません。宣言方法はvar:=を使う二つの方法があります。値を指定しない場合は、指定した型のゼロ値が各要素に入ります。

var arr1 [5]int // ゼロ値
var arr2 = [5]int{1, 2, 3, 4, 5}
arr3 := [5]int{1, 2, 3, 4, 5}

fmt.Printf("arr1 --> %d\n", arr1)
fmt.Printf("arr2 --> %d\n", arr2)
fmt.Printf("arr3 --> %d\n", arr3)
output
arr1 --> [0 0 0 0 0]
arr2 --> [1 2 3 4 5]
arr3 --> [1 2 3 4 5]

配列の型

Go言語では配列の型は、配列の大きさと指定した型によって決まります。以下の二つの配列はintで宣言されていますが、、配列の大きさが異なるので型は異なります。

arr1 := [5]int{1, 2, 3, 4, 5}
arr2 := [2]int{100, 200}

fmt.Printf("arr1 --> Type: %T, Value: %d\n",arr1, arr1)
fmt.Printf("arr2 --> Type: %T, Value: %d\n",arr2, arr2)
output
arr1 --> Type: [5]int, Value: [1 2 3 4 5]
arr2 --> Type: [2]int, Value: [100 200]

要素数の省略

要素数を省略したい場合には、要素数ではなく...を使用することで指定した要素数で配列を宣言することができます。

arr1 := [...]int{1, 2, 3, 4, 5}
arr2 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}

fmt.Printf("arr1 --> Type: %T, Value: %d\n", arr1, arr1)
fmt.Printf("arr2 --> Type: %T, Value: %d\n", arr2, arr2)
output
arr1 --> Type: [5]int, Value: [1 2 3 4 5]
arr2 --> Type: [9]int, Value: [1 2 3 4 5 6 7 8 9]

配列の代入

配列を変数に代入する際には配列のコピーが変数に代入されます。Go Slices: usage and internalsでは以下のような記述があります。

Go’s arrays are values. An array variable denotes the entire array; it is not a pointer to the first array element (as would be the case in C). This means that when you assign or pass around an array value you will make a copy of its contents.
Goの配列は値です。配列変数は配列全体を表します。(C言語の場合のように)配列の最初の要素へのポインタではありません。つまり、配列の値を代入したり渡したりすると、その内容のコピーが作成されます。

arr1 := [5]int{1, 2, 3, 4, 5}
arr2 := arr1

// arr1への変更はarr2に影響しない
arr1[0] = 100

fmt.Printf("arr1 --> Pointer: %p, Value: %d\n", &arr1, arr1)
fmt.Printf("arr2 --> Pointer: %p, Value: %d\n", &arr2, arr2)
output
arr1 --> Pointer: 0x140000162a0, Value: [100 2 3 4 5]
arr2 --> Pointer: 0x140000162d0, Value: [1 2 3 4 5]

スライス

Go言語の「可変長の配列」がスライスです。スライスは以下のように宣言します。配列の宣言と違い、[]内の大きさの指定をしません。

var slice1 []int // ゼロ値
var slice2 = []int{1, 2, 3, 4, 5}
slice3 := []int{1, 2, 3, 4, 5}

fmt.Printf("slice1 --> %d\n", slice1)
fmt.Printf("slice2 --> %d\n", slice2)
fmt.Printf("slice3 --> %d\n", slice3)
output
slice1 --> []
slice2 --> [1 2 3 4 5]
slice3 --> [1 2 3 4 5]

スライスのゼロ値

スライスのゼロ値はnilという値になります。

var slice1 []int

if slice1 == nil {
	fmt.Printf("slice1 is nil\n")
}
output
slice1 is nil

要素の追加

スライスの要素を追加するにはappendを使います。append関数の第一引数に対象のスライスを、第二引数以降に対象のスライスに追加したい要素を指定します。

slice := []int{1}
slice = append(slice, 2, 3, 4, 5)

fmt.Printf("slice --> %d\n", slice)
output
slice --> [1 2 3 4 5]

...を使用することで対象のスライスの最後に別スライスの全要素を追加できるようになります。

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{6, 7, 8, 9}
slice1 = append(slice1, ...slice2)

fmt.Printf("slice1 --> %d\n", slice1)
output
slice1 --> [1 2 3 4 5 6 7 8 9]

長さ(len)と容量(cap)

スライスは長さと容量を持っています。(長さは配列も持っている)スライスの長さと容量が同じ大きさで、appendによってさらに要素を追加しようとすると、より大きな容量を持つスライスの領域を確保し、そこに新しいスライスのコピーとappendに指定された要素が最後に追加されたスライスが返されます。

容量の変化を追ってみる

var slice []int
slice = append(slice, 1)
fmt.Printf("Length: %d, Capacity: %d, Value: %d\n", len(slice), cap(slice), slice)
slice = append(slice, 2)
fmt.Printf("Length: %d, Capacity: %d, Value: %d\n", len(slice), cap(slice), slice)
slice = append(slice, 3)
fmt.Printf("Length: %d, Capacity: %d, Value: %d\n", len(slice), cap(slice), slice)
slice = append(slice, 4)
fmt.Printf("Length: %d, Capacity: %d, Value: %d\n", len(slice), cap(slice), slice)
slice = append(slice, 5)
fmt.Printf("Length: %d, Capacity: %d, Value: %d\n", len(slice), cap(slice), slice)
output
Length: 1, Capacity: 1, Value: [1]
Length: 2, Capacity: 2, Value: [1 2]
Length: 3, Capacity: 4, Value: [1 2 3]
Length: 4, Capacity: 4, Value: [1 2 3 4]
Length: 5, Capacity: 8, Value: [1 2 3 4 5]

配列からスライスを生成する

スライス式を使用して配列からスライスを生成することができます。

arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[:]   // 最初から最後まで
slice2 := arr[:3]  // 最初から指定した位置
slice3 := arr[1:3] // 指定した範囲(開始 - 終了)
slice4 := arr[2:]  // 指定した位置から最後まで

fmt.Printf("arr -----> Pointer: %p, Refer: %p, Value: %d\n", &arr, &arr[0], arr)
fmt.Printf("slice1 --> Pointer: %p, Refer: %p, Value: %d\n", &slice1, &slice1[0], slice1)
fmt.Printf("slice2 --> Pointer: %p, Refer: %p, Value: %d\n", &slice2, &slice2[0], slice2)
fmt.Printf("slice3 --> Pointer: %p, Refer: %p, Value: %d\n", &slice3, &slice3[0], slice3)
fmt.Printf("slice4 --> Pointer: %p, Refer: %p, Value: %d\n", &slice4, &slice4[0], slice4)
output
arr -----> Pointer: 0x140000162a0, Refer: 0x140000162a0, Value: [1 2 3 4 5]
slice1 --> Pointer: 0x1400000c2b8, Refer: 0x140000162a0, Value: [1 2 3 4 5]
slice2 --> Pointer: 0x1400000c2d0, Refer: 0x140000162a0, Value: [1 2 3]
slice3 --> Pointer: 0x1400000c2e8, Refer: 0x140000162a8, Value: [2 3]
slice4 --> Pointer: 0x1400000c300, Refer: 0x140000162b0, Value: [3 4 5]

上記のコードを図示しました。配列からスライスを生成すると配列がコピーされるのではなく、スライスは元の配列を指していることがわかります。スライスは元の配列を指しているため、配列の要素を変更すれば当然slice1~4にも影響します。

スライスからスライスを生成する

スライス式を使用して配列からスライスを生成できましたが、スライスからもスライスを生成することができます。

slice := [5]int{1, 2, 3, 4, 5}
slice1 := slice[:]

fmt.Printf("slice ---> Pointer: %p, Refer: %p, Value: %d\n", &slice, &slice[0], slice)
fmt.Printf("slice1 --> Pointer: %p, Refer: %p, Value: %d\n", &slice1, &slice[0], slice1)
output
slice ---> Pointer: 0x140000162a0, Refer: 0x140000162a0, Value: [1 2 3 4 5]
slice1 --> Pointer: 0x1400000c2b8, Refer: 0x140000162a0, Value: [1 2 3 4 5]

Referが同一なので、一方のスライスを変更するともう一方のスライスにも影響します。

slice := [5]int{1, 2, 3, 4, 5}
slice1 := slice[:]

slice[0] = 100
slice1[3] = 400

fmt.Printf("slice ---> Pointer: %p, Refer: %p, Value: %d\n", &slice, &slice[0], slice)
fmt.Printf("slice1 --> Pointer: %p, Refer: %p, Value: %d\n", &slice1, &slice[0], slice1)
output
slice ---> Pointer: 0x140000b21e0, Refer: 0x140000b21e0, Value: [100 2 3 400 5]
slice1 --> Pointer: 0x140000b02a0, Refer: 0x140000b21e0, Value: [100 2 3 400 5]

スライスを複製する

スライスを代入するとメモリを共有するため、変更が互いに影響し合います。メモリを共有せずスライスを複製したい場合は、copy関数を使用します。

slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, 5)
copy(slice2, slice1)

slice1[0] = 100
slice2[1] = 200

fmt.Printf("slice1 --> Pointer: %p, Refer: %p, Value: %d\n", &slice1, &slice1[0], slice1)
fmt.Printf("slice2 --> Pointer: %p, Refer: %p, Value: %d\n", &slice2, &slice2[0], slice2)
output
slice1 --> Pointer: 0x1400000c2b8, Refer: 0x140000162a0, Value: [100 2 3 4 5]
slice2 --> Pointer: 0x1400000c2d0, Refer: 0x140000162d0, Value: [1 200 3 4 5]

まとめ

普段触っているTypeScriptとは宣言方法や代入時のメモリ共有などが異なる部分なので、手を動かして言語使用を確認することでよりGo言語の配列とスライスについて理解が進みました。

一人前のGopher目指して頑張ります💪

参考

https://go.dev/blog/slices-intro
https://zenn.dev/spiegel/articles/20210315-array-and-slice
https://qiita.com/k-penguin-sato/items/daad9986d6c42bdcde90
https://www.amazon.co.jp/dp/4814400047?tag=baldandersinf-22&linkCode=ogi&th=1&psc=1

GitHubで編集を提案

Discussion