7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go言語】スライスとポインタ

Last updated at Posted at 2024-05-20

はじめに

Goのスライスを使う上での注意点をよく忘れるので、自分でまとめました。appendやcopyなどを何も考えずに行うと、メモリリークが起きたり、意図しない動作になる可能性があります。また、v1.21以降では、slicesパッケージが追加されたので、そちらについても少し触れております。

スライドも作成したので、載せておきます。

例題

さて、突然ですが以下のコードを実行するとどうなると思いますか?

sample1
func main() {
	slice1 := make([]int, 0, 5)
	slice2 := slice1
	for i := 0; i < 10; i++ {
		slice1 = append(slice1, i)
		slice2 = append(slice2, i+100)
	}
	fmt.Println("slice1 =", slice1)
	fmt.Println("slice2 =", slice2)
}

Go Playground

実行すると、以下のような結果になります。

sample1の実行結果
slice1 = [100 101 102 103 104 5 6 7 8 9]
slice2 = [100 101 102 103 104 105 106 107 108 109]

普通だったら、slice1 = [0 1 2 3 4 5 6 7 8 9]となるのを期待します。

スライスの構造

スライスを作成すると、Goはまず配列を作成します。この配列にはアクセスできません。次に、配列をスライスすると、Goはその既存の配列へのポインターを取得します。

Goのスライスは以下のような構造をしています。

image.png

  • 配列へのポインタ : 元となる配列へのポインタ。スライスの元の配列の最初の要素を指す
  • スライスの長さ
  • 容量 : 元となる配列のサイズ

値渡しとポインタ渡し

まず、Goに参照渡しは存在しません。

  • 値渡し : コピーして、中身が等しい新しいものを作成する
  • ポインタ渡し : ポインタを渡す。ポインタが参照している値が変化すれば、値が変わる

以下の例では、関数の引数s は値渡しとなるので、アドレスは変更される。しかし、スライスの要素s[0]はポインタ渡しになるので、アドレスは変更されない。

func main() {
	s := []int{1, 2, 3}
	fmt.Printf("%p\n", &s)    // 0xc000010018
	fmt.Printf("%p\n", &s[0]) // 0xc00001a018

	someFunc(s)
}

func someFunc(s []int) {
	fmt.Printf("%p\n", &s)    // 0xc000010030
	fmt.Printf("%p\n", &s[0]) // 0xc00001a018
}

Capacityによるスライスの動作の違い

capacity(容量)によって、スライスの動作が少し異なります。

  1. capacityが足りないとき
  2. capacityが足りないとき(関数)
  3. capacityが足りるとき
  4. capacityが足りるとき(関数)

Capacityが足りない時

Capacityを超えると新しい配列ができ、ポインタの場所が変更される。
ちなみに、新しい配列のCapacityは2倍になります。(要素数が1024を超えると、25%ずつ増える)

Go Playground

func main() {
	s := make([]int, 0, 3)
	s = append(s, 1, 2)
	fmt.Printf("%p\n", &s)    // 0xc000010018
	fmt.Printf("%p\n", &s[0]) // 0xc00001a018

	s = append(s, 3)
	fmt.Printf("%p\n", &s)    // 0xc000010018
	fmt.Printf("%p\n", &s[0]) // 0xc00001a018

	s = append(s, 4) // 容量を超える
	fmt.Printf("%p\n", &s)    // 0xc000010018
	fmt.Printf("%p\n", &s[0]) // 0xc00007a000
}

Capacityが足りない時(関数に渡す時)

関数内では新しい基底配列が作られ、関数内で容量を超えた際にさらに新しい配列が作成される。

Go Playground

func main() {
	s := []int{1, 2, 3}
	fmt.Printf("%p\n", &s[0]) // 0xc00001a018

	add(s)
	fmt.Println(s)            // [1 2 3]
	fmt.Printf("%p\n", &s[0]) // 0xc00001a018
}

func add(s []int) {
	fmt.Printf("before: %p\n", &s[0]) // before: 0xc00001a018
	s = append(s, 4)
	fmt.Println(s)                   // [1 2 3 4]
	fmt.Printf("after: %p\n", &s[0]) // after: 0xc00007e000
}

Capacityが足りてる時

capacityが足りていると、新しい基底配列は作成されず、ポインタは変わらない。

Go Playground

func main() {
	s := make([]int, 3, 4)
	s[0], s[1], s[2] = 1, 2, 3
	fmt.Printf("%p\n", &s)    // 0xc0000a8000
	fmt.Printf("%p\n", &s[0]) //0xc0000aa000
	fmt.Println(len(s))       // 3

	s = append(s, 4)
	fmt.Printf("%p\n", &s)    // 0xc0000a8000
	fmt.Printf("%p\n", &s[0]) //0xc0000aa000
	fmt.Println(len(s))       // 4
}

Capacityが足りてる時(関数に渡す時)

capacityが足りていると、新しい基底配列は作成されず、ポインタは変わらない。

func main() {
	s := make([]int, 3, 4)
	s[0], s[1], s[2] = 1, 2, 3
	fmt.Printf("%p\n", &s[0]) // 0xc000112000
	fmt.Println(len(s))       // 3

	add(s)
	fmt.Println(s)            // [1 2 3]
	fmt.Printf("%p\n", &s[0]) //0xc000112000
	fmt.Println(len(s))       // 3
}

func add(s []int) {
	fmt.Printf("before: %p\n", &s[0]) // before: 0xc000112000
	fmt.Println(len(s))               // 3

	s = append(s, 4)
	fmt.Println(s)                   // [1 2 3 4]
	fmt.Printf("after: %p\n", &s[0]) // after: 0xc000112000
	fmt.Println(len(s))              // 4
}

Go Playground

スライス化

配列、もしくはスライスからスライスを作成することをスライス化といいます。
例えば、slice2 := slice1[start:end]とすると、slice2はslice1のstart番目end-1番目で構成されます。

func main() {
	slice1 := make([]int, 3, 6)
	slice2 := slice1[1:3]
}

slice1とslice2は、下記の図のように同じ配列を参照します。

image.png

なので、slice1を変更すると、slice2にも変更が反映されます。

func main() {
	slice1 := make([]int, 3, 6)
	slice2 := slice1[1:3]
    slice1[1] = 1
	slice1[2] = 2
	fmt.Println("slice1 =", slice1) // slice1 = [0 1 2]
	fmt.Println("slice2 =", slice2) // slice2 = [1 2]
}

図にすると、slice1[2] = 3によって元の配列の要素が更新され、同じ配列を参照しているslice2にも反映されるということ。

image.png

次に、slice2に要素を追加します。
以下のようにCapacityを超えない場合は特に問題ないです。

func main() {
	slice1 := make([]int, 3, 6)
    slice2 := slice1[1:3]
	slice1[1] = 1
	slice1[2] = 2
	slice2 = append(slice2, 3)
	slice2 = append(slice2, 4)
	slice2 = append(slice2, 5)
	fmt.Println("slice1 =", slice1) // slice1 = [0 1 2]
	fmt.Println("slice2 =", slice2) // slice2 = [1 2 3 4 5]
}

図にすると以下のように、両方とも同じ配列を参照しております。ただし、appendした要素はslice1からは、長さを超えているため、見えておりません。

image.png

slice2にCapacityを超えて要素を追加すると、slice2は新しい配列を参照するようになります。

func main() {
	slice1 := make([]int, 3, 6)
	slice2 := slice1[1:3]
	slice1[1] = 1
	slice1[2] = 2
	slice2 = append(slice2, 3)
	slice2 = append(slice2, 4)
	slice2 = append(slice2, 5)
	fmt.Printf("slice1 : %p\n", &slice1[0]) // 0xc00011c000
	fmt.Printf("slice2 : %p\n", &slice2[0]) // 0xc00011c008
	slice2 = append(slice2, 6)
	fmt.Printf("slice1 : %p\n", &slice1[0]) // 0xc00011c000
	fmt.Printf("slice2 : %p\n", &slice2[0]) // 0xc000122000 新しい配列を参照
	fmt.Printf("%d\n", cap(slice2)) // 10 容量が2倍になる
}

image.png

ガベージコレクタ

スライスの容量を超えたとき、新たな配列を作成しますが、参照されなくなった配列はGC(ガベージコレクタ)によって解放される。(ヒープ上にある場合のみ)

スライスの初期化のベストプラクティス

makeであらかじめスライスを初期化するときに、長さと容量を省略するのはNG。容量が0で初期化したスライスに、for文で1000個の要素を追加していくとします。

func main() {
	slice := make([]int, 0, 0)

	for i := 0; i < 1000; i++ {
		slice = append(slice, i)
	}
}

参照元の配列の容量を超えるたびに、新しい配列(要素が2倍の配列。要素が1024を超える場合は25%ずつ増加)が作られます。
上記の場合だと、0→1→2→4→8→16→32→64→128→256→512→848→1280のように合計13個の配列が作成されます。つまり、12個の配列は不要な配列なので、GCで解放しなければなりません。

以下のように最初に容量を指定すれば、余計な配列のコピーとGCの追加処理の必要性がなくなります。

func main() {
	slice := make([]int, 0, 1000)

	for i := 0; i < 1000; i++ {
		slice = append(slice, i)
	}
}

また、長さを変更しても良い。

func main() {
	slice := make([]int, 1000)

	for i := 0; i < 1000; i++ {
		slice[i] = i
	}
}

上記の要素もしくは、長さを前もって決めておく2つの方法で、パフォーマンスは長さを決めておく方法である。しかし、appendする方法は可読性が高く、多くの場面で好まれる。

nilスライス v.s. 空スライス

  • 長さが0なら、スライスは空
  • nilと同値ならスライスはnil

空スライスとnilスライスの書き方は以下の4通り存在する。

  1. var slice1 []int
  2. slice2 := []int(nil)
  3. slice3 := []int{}
  4. slice4 := make([]int, 0)

そこで以下のコードを実行すると

func main() {
	var slice1 []int
	slice2 := []int(nil)
	slice3 := []int{}
	slice4 := make([]int, 0)
	fmt.Printf("empty=%t\tnil=%t\n", len(slice1) == 0, slice1 == nil)
	fmt.Printf("empty=%t\tnil=%t\n", len(slice2) == 0, slice2 == nil)
	fmt.Printf("empty=%t\tnil=%t\n", len(slice3) == 0, slice3 == nil)
	fmt.Printf("empty=%t\tnil=%t\n", len(slice4) == 0, slice4 == nil)
}

1~4の書き方は全て空スライスであり、1,2はnilスライスでもあるということがわかる。

empty=true	nil=true
empty=true	nil=true
empty=true	nil=false
empty=true	nil=false

nilスライスは空スライスの一部である。スライスに要素が含まれているかどうかはlen(0)で判別する。

nilスライスと違い、空スライスは割り当てが発生するので、関数がスライスを返す場合はnilスライスを返す方がよい。

シンタックスシュガーとして使う場合は、2つ目の[]int(nil)を使う方がいい。

- var s []int
- append(s, 2)
+ s := append([]int(nil, 2)

のように一行で書くことができる。
ちなみに3つ目slice3 := []int{}は初期要素がある場合に使われる書き方。(e.g. []int{1,2,3})

ライブラリによっては、nilスライスと空スライスを区別する。encodeing/jsonでは、nilスライスをマーシャルすると、nullになり、空スライスをマーシャルすると、[]になる。他にもreflect.DeepEqualはnilスライスと空スライスを比較するとfalseになる。

スライスのcopy

間違ったやり方

copy()でスライスをコピーしようとすると、正しく実行されません。

func main() {
	slice := []int{0, 1, 2}
	var dst []int
	copy(dst, slice)
	fmt.Println(dst) // []
}

copyは、コピー元のスライス、コピー先のスライスの要素数が少ない方に長さを合わせます。上記の場合だとdstの長さが0、sliceの長さが3なので、最終的にdstは0になります。

解決策1

コピー先のスライスの長さを前もって、コピー元のスライスと同じ長さに設定しておく。

func main() {
	slice := []int{0, 1, 2}
	dst := make([]int, len(slice))
	copy(dst, src)
	fmt.Println(dst)
}

解決策2

appendを使う。

func main() {
	slice := []int{0, 1, 2}
	dst := append([]int(nil), slice...)
}

解決策3(1.21以降)

slices.Clone()を使用する

func main() {
	slice := []int{0, 1, 2}
	dst := slices.Clone(slice)
	fmt.Println(dst) // [0 1 2]
}

スライスのappendの副作用

func main() {
	s1 := []int{0, 1, 2}
	s2 := s1[1:2]
	s3 := append(s2, 10)
	fmt.Println(s1) // [0 1 10]
	fmt.Println(s2) // [1]
	fmt.Println(s3) // [1 10]
}

Go Playground

s1にも変更が影響されてしまう。

image.png

同様に関数の場合でも同じ現象が起こります。

func main() {
	s := []int{0, 1, 2}
	f(s[:2])
	fmt.Println(s)
}

func f(s []int) {
	_ = append(s, 10) // [0 1 10]
}

解決策1

事前にコピーしておく。

func main() {
	s := []int{0, 1, 2}
	sCopy := make([]int, 2)
	copy(sCopy, s)
	f(sCopy)
	fmt.Println(s) // [0 1 2]
}

func f(s []int) {
	_ = append(s, 10)
}

しかし、このコピーによる解決策は、無駄なコピーが発生するのでよくありません。

解決策2

完全スライス式(s[low:high:max])を渡すことで、解決します。

func main() {
	s := []int{0, 1, 2}
	f(s[:2:2])       // 完全スライス式を渡す
	fmt.Println(s) // [0 1 2]
}

func f(s []int) {
	_ = append(s, 10)
}

s[:2:2]にappendするときはcapacityを超えるので、新しい配列を参照します。ゆえに元の配列に影響を起こしません。

image.png

バッファとしてスライスを用いる

スライスを活用する方法として、ファイルなどの外部リソースからデータを読み込むためのバッファとして使用することがある。

file, err := os.Open(fileNmae)
if err != nil {
    return err
}
defer file.Close()
data := make([]byte, 100)
for { 
    count, err := file.Read(data)
    if err != nil {
        return err
    }
    if count == 0 {
        return nil
    }
    process(data[:count]) // 読み込んだデータの処理

スライスとメモリリーク

既存のスライスや配列をスライス化すると、メモリリークが発生する可能性があります。特に上記のようにバッファとしてスライスを使用するときには、注意しなければなりません。以下のコードは、10バイトのメッセージを1000回受け取って、最初の5バイトを保存するコードです。

func main() {
	for i := 0; i < 1000; i++ {
		message := []byte{99, 99, 99,  ・・・・ , 99} // 1MB
		store(getMessageHead(message))
	}
}

func getMessageHead(msg []byte) []byte {
	return msg[:5]
}

このコードを実行したときに、約1GBのメモリが消費されます。

image.png

解決策

messageをスライス化するのではなく、スライスのコピーを作成する。

func getMessageHead(msg []byte)[]byte{
    msgHead := make([]byte, 5)
    copy(msgHead, msg)
    return msgHead
}

値のスライス v.s. ポインタのスライス

[]Tか[]*Tのどっちがいいのか結論をいうと、ほとんどの場合は[]Tの方が優れている。

スライス TIP

v1.21以上ではslices - Go Packages のスライスパッケージを使うことで簡単に導入できます。

削除(Delete)

1.22以降

slices.Delete()で安全にできる。

func main() {
	s1 := []int{1, 2, 3, 4}
	s2 := slices.Delete(s1, 0, 1)
	fmt.Println(s1) // [2 3 4 0]
	fmt.Println(s2) // [2 3 4]
}

1.21のとき

slices.Delete()はできるが、元のスライスで削除後に元々あった4, 5番目の要素が残ってしまう。

func main() {
	s1 := []int{1, 2, 3, 4}
	s2 := slices.Delete(s1, 0, 1)
	fmt.Println(s1) // [2 3 4 4]
	fmt.Println(s2) // [2 3 4]
}

1.21以前

appendで消せる。

s = append(s[:i], s[i+1:]...)

copyを使ってもできる

s = s[:i+copysa[i:], s[i+1:])]

appendの方はアロケーションが発生する。
また、GCでメモリを解放したい場合は以下のようにする。

if i < len(s)-1 {
    copy(s[i:], s[i+1:])
}
s[len(s)-1] = nil // ゼロ値 or nil
s = s[:len(s)-1]

複数削除

1.21以降

先ほどの例と同様slice.Delete()でできる。1.21はgcで解放されない。

1.21以前

s = append(s[:i], s[j:]...)

gcで解放する場合

copy(s[i:], s[j:])
for k, n := len(s)-j+i, len(s); k < n; k++ {
    s[k] = nil // nil or ゼロ値
}
s = s[:len(s)-j+i]

削除(順番が担保されない)

s[i] = s[len(s)-1] 
s = a[:len(s)-1]

gcで解放する場合

if i < len(s)-1 {
  copy(s[i:], s[i+1:])
}
s[len(s)-1] = nil // nil or ゼロ値
s = s[:len(s)-1]

挿入(Insert)

1.21以降

s := []int{1, 2, 3}
s = slices.Insert(s, 0, 0) // 先頭に追加
fmt.Println(s)             // [0 1 2 3]

1.21以前

s = append(s, x)

複数挿入

1.21以降

s := []int{1, 2, 3}
s = slices.Insert(s, 2, 8, 7, 6) 
fmt.Println(s)  // [1 2 8 7 6 3]

1.21以前

s = append(s[:i], append(make([]T, j), s[i:]...)...)

スライスの挿入

a = append(a[:i], append(b, a[i:]...)...)

スライス同士の結合

1.22以降

slice.Concat()を使うと安全に結合できる。

s3 = slice.Concat(s1,s2,s3)

1.22以前

以下は真っ先に思いつく書き方である。

s3 := append(s1, s2...)

しかし、以下の例ではs3を書き換えたらs1まで書き変わってしまいます。

func main() {
	s1 := make([]int, 2, 5) // s1 has capacity of 5
	s1[0], s1[1] = 2, 3
	s2 := []int{7, 8}

	s3 := append(s1, s2...)

	fmt.Println(s1, s3) // [2 3] [2 3 7 8]
	s3[0] = 5
	fmt.Println(s1, s3) // [5 3] [5 3 7 8] 
}

そこで以下のようにappendすると、安全に結合できます。

s3 := append(s1[:len(s1):len(s1)], s2...)

1.18以上であれば、ジェネリックスを使って定義しておくことができます。

func concatSlice[T any](first []T, second []T) []T {
	n := len(first)
	return append(first[:n:n], second...)
}

以下は複数スライスの結合の汎用関数です。

func concatMultipleSlices[T any](slices [][]T) []T {
	var totalLen int

	for _, s := range slices {
		totalLen += len(s)
	}

	result := make([]T, totalLen)

	var i int

	for _, s := range slices {
		i += copy(result[i:], s)
	}

	return result
}

例題

以下の挙動はどうなるでしょう?

func main() {
	slice1 := make([]int, 3, 5)
	slice2 := slice1[1:4]
	for i := 0; i < 10; i++ {
		slice1 = append(slice1, i)
		slice2 = append(slice2, i+100)
	}
	fmt.Println("slice1 =", slice1)
	fmt.Println("slice2 =", slice2)
}

Go Playground

実行結果
slice1 = [0 0 0 0 1 2 3 4 5 6 7 8 9]
slice2 = [0 0 0 1 101 102 103 104 105 106 107 108 109]

参考文献

  1. Go言語 100Tips ありがちなミスを把握し、実装を最適化する (impress top gear) : Teiva Harsanyi, 柴田 芳樹: 本
  2. O'Reilly Japan - 初めてのGo言語
  3. Go で 2 つ以上のスライスを連結する方法
  4. Effective Go
  5. Go Slice Tricks Cheat Sheet
  6. Bad Go: ポインタのスライス。この投稿もここにあります。コードは次のとおりです。フィル・パール著 |中くらい
7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?