Goでsliceの一部を抽出する時の注意点

sliceの構造

Goのsliceは 配列へのポインタ 長さ 容量 を持った構造体として定義されています。

go/slice.go at go1.18 · golang/go
The Go programming language. Contribute to golang/go development by creating an account on GitHub.
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

sliceの構造については以下の記事が分かりやすかったです。

実装して理解するスライス #golang - Qiita
はじめに この記事はGoアドベントカレンダーの1日目の記事です。 スライスの実態 runtimeのコードをみるとGoのスライスは以下のように定義されています。 type slice struct { array ...

この構造をきちんと理解していなくてバグを踏んだので、その時調べたことをまとめました。

sliceの要素を抽出  

sliceの要素の一部を抜き出したい時は以下のように書きます。

	base := []int{1, 2, 3, 4}
	newSlice := base[1:2]

この時の注意点として、元のsliceと新しく生成したsliceは同じ配列へのポインタを持っています。

例えば以下のようにsliceの一部を取り出し、別のsliceを作り、その中身を出力してみます。

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	base := []int{1, 2, 3, 4}
	from1to2 := base[1:2]
	to2 := base[:2]
	from1 := base[1:]
	from2 := base[2:]
	all := base[:]
	empty := base[:0]
	appended := []int{}
	appended = append(appended, base...)

	printSliceHeader("base    ", base)
	printSliceHeader("from1to2", from1to2)
	printSliceHeader("to2     ", to2)
	printSliceHeader("from1   ", from1)
	printSliceHeader("from2   ", from2)
	printSliceHeader("all     ", all)
	printSliceHeader("empty   ", empty)
	printSliceHeader("appended", appended)
}

func printSliceHeader(n string, ss []int) {
	ptr := unsafe.Pointer(&ss)
	s := (*reflect.SliceHeader)(ptr)
	fmt.Printf("%s : %#v\n", n, s)
}

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

base     : &reflect.SliceHeader{Data:0xc000134000, Len:4, Cap:4}
from1to2 : &reflect.SliceHeader{Data:0xc000134008, Len:1, Cap:3}
to2      : &reflect.SliceHeader{Data:0xc000134000, Len:2, Cap:4}
from1    : &reflect.SliceHeader{Data:0xc000134008, Len:3, Cap:3}
from2    : &reflect.SliceHeader{Data:0xc000134010, Len:2, Cap:2}
all      : &reflect.SliceHeader{Data:0xc000134000, Len:4, Cap:4}
empty    : &reflect.SliceHeader{Data:0xc000134000, Len:0, Cap:4}
appended : &reflect.SliceHeader{Data:0xc000134020, Len:4, Cap:4}

base, all, empty と from1to2, from1 はそれぞれ同じポインタを指しています。

要するに、最初にbaseを生成した時に配列aが作られ、all, empty は配列aの0番目を指し、from1to2, from1 は配列aの1番目を指し、from2 は配列aの2番目を指している、ということです。

appendedは別のsliceとして生成したので、全く別の配列を指しています。

そのため、一つのsliceの要素を変更すると全てのsliceに影響があります。

	base[1] = 9

	fmt.Println("base     :", base)
	fmt.Println("from1to2 :", from1to2)
	fmt.Println("to2      :", to2)
	fmt.Println("from1    :", from1)
	fmt.Println("from2    :", from2)
	fmt.Println("all      :", all)
	fmt.Println("empty    :", empty)
	fmt.Println("appended :", appended)
base     : [1 9 3 4]
from1to2 : [9]
to2      : [1 9]
from1    : [9 3 4]
from2    : [3 4]
all      : [1 9 3 4]
empty    : []
appended : [1 2 3 4]

意図せずsliceの値が変わってしまう可能性があるので気をつけたほうがよさそうです。

sliceの一部を抽出した時にできるslice

これまでの結果から、sliceを抽出した場合、長さは指定した添え字分の長さになっていそうです。

容量については、以下のように長さと容量の違うsliceを抽出してみると、

	base := make([]int, 0, 4)
	base = append(base, 1, 2, 3)
	from1to2 := base[1:2]

	printSliceHeader("base    ", base)
	printSliceHeader("from1to2", from1to2)

以下のようになります。

base     : &reflect.SliceHeader{Data:0xc00012a000, Len:3, Cap:4}
from1to2 : &reflect.SliceHeader{Data:0xc00012a008, Len:1, Cap:3}

容量は元のsliceから開始地点の添え字分を引いたものになってそうです。

以上より、長さlen 容量cap 配列へのポインタptr のsliceに対してslice[from:to] を実行すると、長さto-from 容量cap-from 配列へのポインタptr+from のsliceができるようです。

より詳しいことは以下に書いてあります。

The Go Programming Language Specification - The Go Programming Language
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

appendして配列を更新した場合

sliceのcapを超えてappendするとsliceの指す配列自体が生成し直されるので、他のsliceに影響しなくなります。

	printSliceHeader("base    ", base)
	base = append(base, 5)
	base[1] = 9

	printSliceHeader("base    ", base)
	printSliceHeader("from1to2", from1to2)
	printSliceHeader("to2     ", to2)
	printSliceHeader("from1   ", from1)
	printSliceHeader("from2   ", from2)
	printSliceHeader("all     ", all)
	printSliceHeader("empty   ", empty)
	printSliceHeader("appended", appended)
	fmt.Println("base     :", base)
	fmt.Println("from1to2 :", from1to2)
	fmt.Println("to2      :", to2)
	fmt.Println("from1    :", from1)
	fmt.Println("from2    :", from2)
	fmt.Println("all      :", all)
	fmt.Println("empty    :", empty)
	fmt.Println("appended :", appended)
base     : &reflect.SliceHeader{Data:0xc0000ba000, Len:4, Cap:4}
base     : &reflect.SliceHeader{Data:0xc0000c2040, Len:5, Cap:8}
from1to2 : &reflect.SliceHeader{Data:0xc0000ba008, Len:1, Cap:3}
to2      : &reflect.SliceHeader{Data:0xc0000ba000, Len:2, Cap:4}
from1    : &reflect.SliceHeader{Data:0xc0000ba008, Len:3, Cap:3}
from2    : &reflect.SliceHeader{Data:0xc0000ba010, Len:2, Cap:2}
all      : &reflect.SliceHeader{Data:0xc0000ba000, Len:4, Cap:4}
empty    : &reflect.SliceHeader{Data:0xc0000ba000, Len:0, Cap:4}
appended : &reflect.SliceHeader{Data:0xc0000ba020, Len:4, Cap:4}
base     : [1 9 3 4 5]
from1to2 : [2]
to2      : [1 2]
from1    : [2 3 4]
from2    : [3 4]
all      : [1 2 3 4]
empty    : []
appended : [1 2 3 4]