🖥

Go言語 — slice と array とポインタ

2023/08/26に公開

まとめ

  • スライス は 配列 のラッパーのようなもの。
  • スライス への操作をおこなうと、内部ではいい感じで 配列を操作してくれる。 スライス はその 配列 を参照する。
  • スライス という名前には 「配列の断片を使う」というニュアンスがあるように感じられる。
    • この動作を便宜的に「要素数を気にしないタイプの配列」として扱えるイメージ。
  • Gopher の間では「スライスだけで良いじゃん」という説もあるようだ。

検証

1. 配列をスライスする

配列から一部の要素だけをスライスしてみる。
(スライスっていう名前通りの使い方)

array := [3]string {"Alice", "Bob", "Carol"}
slice := array[:2]

一部の要素だけをスライスしたので、得られる要素数は、スライスのほうが一個少ない。
配列にはCarorlがいるが、スライスにはいない。

fmt.Println(array) // [Alice Bob Carol]
fmt.Println(slice) // [Alice Bob]

2. スライスを変更する

Alice を Dave に変えてみる。

slice[0] = "Dave"

配列とスライスの両方で、Alice が Dave に変わった。
スライスは実体ではなく、配列を参照しているからだ。

fmt.Println(array) // [Dave Bob Carol]
fmt.Println(slice) // [Dave Bob]

「スライスの基底となる配列」のポインタを見ることも出来る。

underlying := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
fmt.Println(underlying) // 例: &{842350559520 2 3}

3. 配列を変更する

今度はスライスではなく、配列側の値を変更してみる。

array[1] = "Eric"

配列とスライスの両方で、Bob が Eric に変わった。

fmt.Println(array) // [Dave Eric Carol]
fmt.Println(slice) // [Dave Eric]

引き続きスライスは同じポインタで配列を参照している。

fmt.Println(underlying) // 例: &{842350559520 2 3}

4. スライスに要素を追加する

2個の要素が得られているスライスに、3個目の要素を追加してみる。

slice = append(slice, "Frank")

配列の3個目の要素が変わった。 Carol は Frank になった。

fmt.Println(array) // [Dave Bob Frank]
fmt.Println(slice) // [Dave Bob Frank]

スライスの cap の値が 2 から 3 に増えているが、ポインタは変わっていない。

fmt.Println(underlying) // 例: &{842350559520 3 3}

5. 配列の要素数を越える

Go での配列は、要素数の決まった型だ。
つまりこの例でいうと、配列は3個の string しか持っていない。 ( [3]string という型 )

スライスの末尾にさらに要素を追加してみる。

slice = append(slice, "Greg") 
  • 配列の要素数は 3個
  • スライスはその配列を参照している

と考えると、スライスに4個目の要素は追加できないはずだが?

結果を見てみると、今度は配列とスライスで得られる値が変わっている。
スライスにだけ Greg が追加されている。

fmt.Println(array) // [Dave Eric Frank]
fmt.Println(slice) // [Dave Eric Frank Greg]

試しに、スライスの1個目の要素を変更してみる。

slice[0] = "Henry"

そうすると、また配列は変更されていない。
スライスでだけ Dave が Henry に変わっている。

fmt.Println(array) // [Dave Eric Frank]
fmt.Println(slice) // [Henry Eric Frank Greg]

ポインタの状態を見てみると、ポインタの参照先が変わっている。

fmt.Println(underlying) // 例: &{842350895200 4 6}

配列とスライスの「リンク」が切れた状態になったようだ。

goのスライスは、今回のように【配列の要素数を越えるスライス】を作った場合、内部的に新しい配列を作り、今度はそいつを参照するようになるようだ。

pointer / len / cap

ちなみに、スライスは ポインタ / 長さ(len) / キャパシティ(cap) という構造になっているが、

最初のポインタが

  • どの配列を参照するか
  • 配列の何番目から何番目までを参照するか

を覚えていてくれている。

image

( Go Slices: usage and internals - The Go Blog より )

  • len はスライスで得られる要素数
  • cap は基底となる配列の要素数

っぽい。

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

( A Tour of Go より )

ただコードの最後の例で cap が 3 から 6 に増えているのは謎なので、今後調べたい。

コード全体


package main

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

func main() {
	array := [3]string {"Alice", "Bob", "Carol"} // 配列を作る
	slice := array[:2] // 配列から二個の要素をスライスする
	fmt.Println(array) // [Alice Bob Carol]
	fmt.Println(slice) // [Alice Bob]

	slice[0] = "Dave"  // スライスの先頭を変更する
	fmt.Println(array) // [Dave Bob Carol] // 配列の値が変更される
	fmt.Println(slice) // [Dave Bob] // スライスは配列を参照している

	underlying := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) // スライスのポインタを見てみる
	fmt.Println(underlying) // 例: &{842350559520 2 3}

	array[1] = "Eric"  // スライスではなく、配列側の中身を変更してみる
	fmt.Println(array) // [Dave Eric Carol] // Bob が Eric に変わった
	fmt.Println(slice) // [Dave Eric] // スライスは配列を参照しているので、スライスで得られる値も変わる

	fmt.Println(underlying) // 例: &{842350559520 2 3} // ポインタが同じ

	slice = append(slice, "Frank") // スライスの末尾に要素を追加する // 得られる要素数は三個になる
	fmt.Println(array) // [Dave Bob Frank] // 配列の三個目の要素が変更された
	fmt.Println(slice) // [Dave Bob Frank] // スライスは配列の要素全てを参照するようになった 

	fmt.Println(underlying) // 例: &{842350559520 3 3} 

	slice = append(slice, "Greg") // スライスの末尾にさらに要素を追加する // 参照先の配列の要素数を越えて4個になりそうだが?
	fmt.Println(array) // [Dave Eric Frank] // 今度は配列の状態は変わっていない
	fmt.Println(slice) // [Dave Eric Frank Greg] // スライスで得られる要素数は四個になった // スライスは配列を参照していたはずでは?

        fmt.Println(underlying) // 例: &{842350895200 4 6} // ポインタの参照先が変わっている

	slice[0] = "Henry" // スライスの最初の要素を変更する
	fmt.Println(array) // [Dave Eric Frank] // 今度も配列の状態は変わっていない
	fmt.Println(slice) // [Henry Eric Frank Greg] // スライスで得られる値は期待通りに変わっている

	fmt.Println(underlying) // 例: &{842350895200 4 6} // ポインタの参照先が変わっている
}

環境

  • go version go1.8 darwin/amd64

感想

  • 低級言語の仕組みを知れて良かった。
  • 当たり前だが高級言語でも内部的には、こういったことがおこなわれているんだろうなと思った。
  • 配列の扱いひとつでも、メモリ使用量とかを気にする気持ちが少し分かった気がする。

参考

チャットメンバー募集

何か質問、悩み事、相談などあればLINEオープンチャットもご利用ください。

https://line.me/ti/g2/eEPltQ6Tzh3pYAZV8JXKZqc7PJ6L0rpm573dcQ

Twitter

https://twitter.com/YumaInaura

公開日時

2017-03-27

Discussion