GoのSliceについて

Goのsliceは可変長配列として扱えるデータ構造であり、内部に配列へのポインタを持っていて、その配列についてのlengthとcapacityを保持している。 lengthは配列の有効サイズを示し、capacityはその配列がメモリ確保した最大サイズを示す。 capacity内であれば配列の要素は変更可能であり、メモリが新たに割当てられることはない。 capacity以上のサイズが必要になった場合は、配列にはcapacityの倍のサイズの新たなメモリが割当てられる。

以下のようにsliceそのもののアドレス&sと、sliceが保持する配列のポインタアドレス&s[0]を表示してみる。 sliceが新たに作られても、sliceが保持する配列のポインタアドレスは、capacityを超えない限り変わっていないことが分かる。 また、Slicingした際には元の(capacityが拡張されていない)sliceが保持する配列のポインタアドレスを参照しており、capacityを継承していることが分かる。

$ cat sliceTest.go 
package main

import (
    "fmt"
)

func justAppend(s []int) {
    s = append(s, 4)
    fmt.Println("In justAppend")
    fmt.Println(s)
    fmt.Println(s[:cap(s)])
    fmt.Printf("&s=%p,&s[0]=%p,%d,%d\n\n", &s, &s[0], len(s), cap(s))
}

func appendThenReturn(ps *[]int) []int {
    s := *ps
    s = append(s, 4)
    fmt.Println("In appendThenReturn")
    fmt.Println(s)
    fmt.Println(s[:cap(s)])
    fmt.Printf("&s=%p,&s[0]=%p,%d,%d\n\n", &s, &s[0], len(s), cap(s))

    return s
}

func appendMoreThanCapThenReturn(s []int) []int {
    s = append(s, 5)
    s = append(s, 6)
    fmt.Println("In appendMoreThanCapThenReturn")
    fmt.Println(s)
    fmt.Println(s[:cap(s)])
    fmt.Printf("&s=%p,&s[0]=%p,%d,%d\n\n", &s, &s[0], len(s), cap(s))

    return s
}

func main() {
    s := make([]int, 0, 5)
    s = append(s, 1)
    s = append(s, 2)
    s = append(s, 3)

    fmt.Println("Before justAppend")
    fmt.Println(s)
    fmt.Println(s[:cap(s)])
    fmt.Printf("&s=%p,&s[0]=%p,%d,%d\n\n", &s, &s[0], len(s), cap(s))

    justAppend(s)

    fmt.Println("After justAppend")
    fmt.Println(s)
    fmt.Println(s[:cap(s)])
    fmt.Printf("&s=%p,&s[0]=%p,%d,%d\n\n", &s, &s[0], len(s), cap(s))

    s = appendThenReturn(&s)

    fmt.Println("After appendThenReturn")
    fmt.Println(s)
    fmt.Println(s[:cap(s)])
    fmt.Printf("&s=%p,&s[0]=%p,%d,%d\n\n", &s, &s[0], len(s), cap(s))

    t := appendMoreThanCapThenReturn(s)

    fmt.Println("After appendMoreThanCapThenReturn")
    fmt.Println(t)
    fmt.Println(t[:cap(t)])
    fmt.Printf("&t=%p,&t[0]=%p,%d,%d\n\n", &t, &t[0], len(t), cap(t))

    u := s[0:3]

    fmt.Println("Show u")
    fmt.Println(u)
    fmt.Println(u[:cap(u)])
    fmt.Printf("&u=%p,&u[0]=%p,%d,%d\n\n", &u, &u[0], len(u), cap(u))

}
$ go run sliceTest.go
Before justAppend
[1 2 3]
[1 2 3 0 0]
&s=0x8201e8140,&s[0]=0x8201f21e0,3,5

In justAppend
[1 2 3 4]
[1 2 3 4 0]
&s=0x8201e81c0,&s[0]=0x8201f21e0,4,5

After justAppend
[1 2 3]
[1 2 3 4 0]
&s=0x8201e8140,&s[0]=0x8201f21e0,3,5

In appendThenReturn
[1 2 3 4]
[1 2 3 4 0]
&s=0x8201e8260,&s[0]=0x8201f21e0,4,5

After appendThenReturn
[1 2 3 4]
[1 2 3 4 0]
&s=0x8201e8140,&s[0]=0x8201f21e0,4,5

In appendMoreThanCapThenReturn
[1 2 3 4 5 6]
[1 2 3 4 5 6 0 0 0 0]
&s=0x8201e8320,&s[0]=0x8201ec190,6,10

After appendMoreThanCapThenReturn
[1 2 3 4 5 6]
[1 2 3 4 5 6 0 0 0 0]
&t=0x8201e8300,&t[0]=0x8201ec190,6,10

Show u
[1 2 3]
[1 2 3 4 5]
&u=0x8201e83c0,&u[0]=0x8201f21e0,3,5

Thanks to

Go のスライスでハマッたところ - Block Rockin’ Codes

Goでxxxのポインタを取っているプログラムはだいたい全部間違っている - Qiita

Go のスライスの内部実装 - Block Rockin’ Codes

GCのついての考察は興味深い。