Go言語のスライスについて

こんにちは、M.Uです。

今回はGo言語のスライスについての記事です。
スライスと配列についての記事は数多くありますが、スライスが配列への参照であることと、組み込み関数えあるappend関数についての学習も兼ねて記事にしました。

問題

突然ですが、ここで問題です。以下のコードを実行すると標準出力はどのような内容になるでしょうか?

package main

import "fmt"

func main() {
    s1 := make([]int, 0, 100)
    s2 := s1
    s1 = append(s1, 1)
    s2 = append(s2, 2)
    s1 = append(s1, 3)
    s2 = append(s2, 4)
    s1 = append(s1, 5)
    s2 = append(s2, 6)
    fmt.Println(s1, s2)
}

答えは…こちら。




$ go run main.go
[2 4 6] [2 4 6]

いかがでしょうか?
スライスが配列への参照という点と、append関数の仕組みを把握してなんとなく想像がつくでしょうか。

スライスは配列へのポインタの値を持っている

スライスが持っているのは固定長配列へのポインタ(参照先)やその配列のサイズです。

参考URL: https://blog.golang.org/go-slices-usage-and-internals

スライスの割り当てはmake関数で行います。第二引数0を指定することで要素0個の配列への参照として宣言できます。第三引数は容量を表します。
また、スライスが参照している配列のことを基底配列(underlying array)と呼びます。
参照URL: https://golang.org/ref/spec#Slice_expressions

次にスライスs2へs1を代入していることから、2つのスライスは共通の配列を参照していることになります。

試しに以下のようなコードを書いてみます。 それぞれのスライスの配列0番目のポインタは同じアドレスを指していることがわかります。

package main

import "fmt"

func main() {
    s1 := make([]int, 10)
    s2 := s1
    fmt.Printf("s1[0]: %p\ns2[0]: %p\n", &s1[0], &s2[0])
}

結果

s1[0]: 0xc0000160f0
s2[0]: 0xc0000160f0

スライスそれぞれで異なるサイズ情報を保持している

次に重要なポイントは、スライスの変数s1とs2それぞれに異なるサイズ情報が保持されている点です。
以下のようにサイズを求める len関数を追加したコードを実行してみます。

package main

import "fmt"

func main() {
    s1 := make([]int, 0, 100)
    s2 := s1
    s1 = append(s1, 1)
    fmt.Printf("s1:%v  len: %d\n", s1, len(s1))
    fmt.Printf("s2:%v  len: %d\n", s2, len(s2))
    fmt.Println("-------------------")
    s2 = append(s2, 2)
    fmt.Printf("s1:%v  len: %d\n", s1, len(s1))
    fmt.Printf("s2:%v  len: %d\n", s2, len(s2))
    fmt.Println("-------------------")
    s1 = append(s1, 3)
    fmt.Printf("s1:%v  len: %d\n", s1, len(s1))
    fmt.Printf("s2:%v  len: %d\n", s2, len(s2))
    fmt.Println("-------------------")
    s2 = append(s2, 4)
    fmt.Printf("s1:%v  len: %d\n", s1, len(s1))
    fmt.Printf("s2:%v  len: %d\n", s2, len(s2))
    fmt.Println("-------------------")
    s1 = append(s1, 5)
    fmt.Printf("s1:%v  len: %d\n", s1, len(s1))
    fmt.Printf("s2:%v  len: %d\n", s2, len(s2))
    fmt.Println("-------------------")
    s2 = append(s2, 6)
    fmt.Printf("s1:%v  len: %d\n", s1, len(s1))
    fmt.Printf("s2:%v  len: %d\n", s2, len(s2))
    fmt.Println("-------------------")
    fmt.Println(s1, s2)
}

結果

s1:[1]  len: 1
s2:[]  len: 0
-------------------
s1:[2]  len: 1
s2:[2]  len: 1
-------------------
s1:[2 3]  len: 2
s2:[2]  len: 1
-------------------
s1:[2 4]  len: 2
s2:[2 4]  len: 2
-------------------
s1:[2 4 5]  len: 3
s2:[2 4]  len: 2
-------------------
s1:[2 4 6]  len: 3
s2:[2 4 6]  len: 3
-------------------
[2 4 6] [2 4 6]

上記の結果からスライス変数s1s2が基底配列を共有しているにもかかわらず異なるサイズ情報を持っていることがわかります。

s1:[2 3]  len: 2
s2:[2]  len: 1

上記のように変数s2の場合はサイズ1となるため、1番目の要素の値までしか見れません。
一方、変数s1の場合はサイズ2となっているため、2番目の要素まで見られるようになっています。

appned関数で引数の値が配列の末尾に追加されますが、この末尾に追加というのが引数のスライスの長さ(len関数の値)で決まります。
例えば、s2 = append(s2, 4) の処理では、s2のサイズは1なので追加する場所は2番目の要素となります。
結果、変数s1でも参照している2番目の値3が4に上書きされる事になります。

f:id:toranoana-lab:20190424153240p:plain

最後に

Go言語ではポインタ変数のインクリメントは許されていません。なので、C言語のように配列の参照がポインタ操作で参照することはできません。代わりにスライスを通じて配列の要素を柔軟に参照している役割を担っているかのような印象を受けました。 今回の問題のコードは実験的ではありますが、スライスと配列に密接な関係があることがわかりました。

P.S.

虎の穴では一緒に働く仲間を絶賛募集中です! この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。

yumenosora.co.jp