2019-12-04

全然わからない。俺たちは雰囲気でSliceを使っている

この記事は Go7 Advent Calendar 2019 4日目の記事です。 https://qiita.com/advent-calendar/2019/go7

Arrayは固定長で扱いづらいから可変長のSliceを使う。 そのくらいにしか考えていませんでしたが、Effective go とかいろいろ読むうちに整理されたのでそれぞれの違いと操作について書き残しておきます。

SliceとArrayの関係

最初に結論を言うと SliceArray のポインタをフィールドに持つ構造体です。

具体的には 以下のように定義されています (slice.go)

type slice struct { array unsafe.Pointer len int cap int }

Slice は内部に持ったArrayを len フィールドの長さに切り出すことで 可変であるように振る舞います。

では capacity フィールドは何なんでしょうか。

たとえば、こんな感じにキャパシティを超えて追加してみると、エラーになるわけでもなく普通にできてしまいます。

capacityover.go
package main

import (
	"fmt"
)

func main() {
	// 初期値: [0], キャパ: 2の数値型スライスを作る
	l := make([]int, 1, 2)
	fmt.Println(l) // [0]
	// 2つ目の値(1)を追加
	l = append(l, 1)
	fmt.Println(l) // [0 1]
	// 3つ目の値(2)を追加
	l = append(l, 2)
	fmt.Println(l) // [0 1 2]
}

キャパシティとは一体..

neko1.jpg

続いて Slice length and capacity - A Tour of Go を見てみましょう。

The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice.

(スライスの容量とは根底となる配列をスライスの先頭から数えた要素数である)

neko2.jpg

なるほど、 len はスライスの長さ(論理的な長さ)で、 capacity は 配列の長さ(物理的な長さ)ということのようです。 ただしスライスが配列の途中から切り出されている場合、スライスを切り出したインデックスが起点となります。(スライス操作については後述)

スライスの要素数が配列を下回っている場合は配列をそのまま利用できますが、スライスの要素数が配列を超える場合は配列自体を作り直す必要があるというわけです。 これをアロケーションといいます。

アロケーションの発生を抑えてパフォーマンスを向上させるためには、スライスの初期化時にキャパシティをある程度確保することも検討しましょう。 とはいえふつうのWebアプリを作るくらいではほとんど影響ないレベルです。DBのIOのほうが圧倒的に遅いしね。

また、スライスは参照型なので関数に渡してもコピーが発生しません。一方、配列は値型なので参照渡ししなければコピーが作られてしまい非効率です。通常はスライスを利用することが推奨されており、 配列を直接操作することはあまり多くなさそうです。

スライス操作

作る

スライスを作るにはいくつか方法があります。

初期化

初期値を与えつつ、スライスを作る場合は以下のように [] 内に長さを指定せずに要素を指定します。 この場合、キャパシティは指定した要素数と同じになります。

slice.go
package main

import (
	"fmt"
)

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

make 関数

make 関数を使ってスライスを作ることができます。

make 関数は「型」「初期サイズ」「キャパシティ」を与えてスライスを作れます。

初期サイズに 1以上 を指定した場合、すべての要素がその型のゼロ値で初期化されます。

make.go
package main

import (
	"fmt"
)

func main() {
	s := make([]int, 2, 3)
	fmt.Println(s, cap(s)) // [0 0] 3
}

配列を切り出す

配列を切り出すことで、スライスを作ることができます。

info
  • この切り出し処理自体も スライス と言います。 この場合のスライスは動詞で使われていることに注意してください。
  • ここでは区別するために、切り出し処理についてはスライシングと言います。

要素を指定した配列を作成したあと、スライシングすることで任意の要素とキャパシティを持つスライスを作ることができます。(他のより1ステップ多いですが..)

arrayslicing.go
package main

import (
	"fmt"
)

func main() {
	a := [3]int{1, 2, 0}
	s := a[:2]
	fmt.Println(s, cap(s)) // [1 2] 3
}

append 関数

スライスに要素を追加するには append 関数を使います。

第2引数以降にスライスの要素に合った型の値を指定します。

キャパシティを超えるとアロケーションが発生し別のスライスが返ってくるため、 左辺もスライスで受ける必要があります。

append.go
package main

import (
	"fmt"
)

func main() {
	s := make([]int, 1, 2)
	fmt.Printf("%d (%p)\n", s, s) // [0] (0x40e020)
	s = append(s, 1)
	fmt.Printf("%d (%p)\n", s, s) // [0 1] (0x40e020)
	s = append(s, 2)
	fmt.Printf("%d (%p)\n", s, s) // [0 1 2] (0x40e050)
}

第2引数以降は可変長なので、複数の値を追加できます。

スライスの後ろに ... をつけることでスライスを引数に展開できるため、スライス同士を結合することもできます。

appendmultiple.go
package main

import (
	"fmt"
)

func main() {
	s := make([]int, 1, 3)
	fmt.Printf("%d (%p)\n", s, s) // [0] (0x40e020)
	s = append(s, 1, 2)
	fmt.Printf("%d (%p)\n", s, s) // [0 1 2] (0x40e020)
	s = append(s, []int{3, 4}...)
	fmt.Printf("%d (%p)\n", s, s) // [0 1 2 3 4] (0x45e020)
}

スライシング

先程も少し使いましたが、配列やスライスを切り出す処理も スライス と言います。

スライスのインデックスに : 区切りで 始端のインデックス, 終端のインデックス, キャパシティ が指定することで新たなスライスが切り出されます。

なお、それぞれの値は省略できます。

  • 始端を省略した場合: 最初から
  • 終端を省略した場合: 最後まで
  • キャパシティを省略した場合: 現在のキャパシティをそのまま使う。省略する場合、最後の : も省略できる。

スライシングすることで、スライスの縮小を行うことができます。 キャパシティを指定しない場合、スライスの長さだけを変えキャパシティはそのまま引き継ぎます。

slicing.go
package main

import (
	"fmt"
)

func main() {
	a := [5]int{0, 0, 0, 0, 0}
	s := a[:3]                                        // 配列をスライシングするとスライスになる
	fmt.Printf("%d (%p) cap: %d \n", s, s, cap(s))    // [0 0 0] (0x45e000) cap: 5
	s[1] = 2                                          // 2番目の要素に2を入れる
	fmt.Printf("%d (%p) cap: %d \n", s, s, cap(s))    // [0 2 0] (0x45e000) cap: 5
	s1 := s[:2]                                       // 2番目までの要素を切り出してスライスのサイズを縮小
	s1[0] = 1                                         // 切り出したスライス(1)の1番目に1を入れる
	fmt.Printf("%d (%p) cap: %d \n", s1, s1, cap(s1)) // [1 2] (0x45e000) cap: 5
	s2 := s[1:]                                       // 2番目以降の要素を切り出してスライスのサイズを縮小
	s2[1] = 3                                         // 切り出したスライス(2)の2番目に3を入れる
	fmt.Printf("%d (%p) cap: %d \n", s2, s2, cap(s2)) // [2 3] (0x45e004) cap: 4
	fmt.Printf("%d (%p) cap: %d \n", a, &a, cap(a))   // [1 2 3 0 0] (0x45e000) cap: 5
}

Python ではスライスすると別のリストが得られますが、 Golang ではスライスしても同じスライス(配列)を参照するため、切り出したそれぞれのスライスを編集すると元のスライスにも変更が伝わります。

存在チェック

スライスの要素の存在チェックをするための関数がデフォルトで存在しないので、 必要に応じて自分で定義する必要があります。

例えば、JSのように該当インデックスを返す関数は以下のように定義できます。

indexof.go
package main

import (
	"fmt"
)

func indexOf(l []int, n int) int {
	for i, v := range l {
		if v == n {
			return i
		}
	}
	return -1
}

func main() {
	l := []int{1, 2, 3, 4, 5}
	fmt.Println(indexOf(l, 1), indexOf(l, 5), indexOf(l, 100)) // 0 4 -1
}

もしジェネリクスがあったらビルトインで作られてたのかな。

間違いがあったら指摘ください(定期)

なんか今(12/4)みたらアドベントカレンダーの枠、全部埋まってました。すごい。 空いてたら別の日にも入れようと思ってたので少し残念。

参考

https://github.com/golang/go/wiki/SliceTricks https://blog.golang.org/go-slices-usage-and-internals https://hackernoon.com/golang-slice-and-append-e18498cf924a https://www.golangprograms.com/go-language/slices-in-golang-programming.html