いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-36 (スライスのポインタについて)

概要

よく忘れるので、自分用にメモです。

Goのスライスは、内部にデータが格納されている配列のポインタを持っています。

なので、通常スライスをポインタで利用する必要は無いと記載されていることが多いです。

ですが、C#やPythonなどでよくやるように、メソッドにリストを渡して、メソッド内部で

そのリストに対して、要素を追加したりする場合、スライスをポインタで渡して処理した方が良い場合があります。

スライスは以下のような構造になっています。

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

https://golang.org/pkg/reflect/#SliceHeader

Dataフィールドが件の配列への参照ですね。その他に長さと容量を表すフィールドを持っています。

スライスを関数に対して、値渡しする場合、この構造体が値コピーされて渡ることになります。

なので、関数内でパラメータで渡されたスライスを編集すると、以下のような事が発生します。

  • capが足りている場合であっても、呼び出し元が持っているスライスと関数が持っているスライスのCapが異なっている
  • capが足りていない場合、Goが自動的に新しい配列を用意して設定するので、呼び出し元が持っているスライスと関数が持っているスライスでDataの参照先が異なっている

1つ目 の件は、以下のようなことですね。

f := func(s []int) {
    s = append(s, 1)
    fmt.Printf("\t%v\n", s)
}

s := make([]int, 0, 1)

fmt.Println(s)

// ここで要素が一つ追加されるが、capはまだ大丈夫
f(s)

// だけど、呼び元が持っているスライスのCapは更新できていないので値は見えない
fmt.Println(s)

// capを更新
s = s[:cap(s)]

// これで見える
fmt.Println(s)

こんな感じになります。

[]
        [1]
[]
[1]

capに到達していない場合、新しい配列が割り当てられないので、capを更新してしまえばオッケイですが

上の状態から更に要素を追加すると、capを超えます。この場合、新しい配列が用意されて設定されるので

呼び元が持っている参照先と関数内部が持っているスライスの参照先が異なる状態になります。

スライスを値渡しで関数に渡している場合、データの参照先を示すポインタの値も値コピーされているので、違う配列を見ている状態となります。

こうなったら、もう cap 更新しても見えません。違う配列を見ていることになるので。

// さっきの状態の続き

// これでcap超えるので、別の配列が割り当てられる
f(s)

fmt.Println(s)

s = s[:cap(s)]

// capを更新しても、配列自体が変わっているので見えない
fmt.Println(s)
        [1 1]
[1]
[1]

こんなときに、スライスをポインタで渡すとすんなりうまくいきます。

capの更新も必要なしです、同じスライスを呼び元も関数も見ているので。

package main

import "fmt"

func main() {
    f := func(s *[]int) {
        *s = append(*s, 1)
        fmt.Printf("\t%v\n", *s)
    }

    s := make([]int, 0, 1)

    fmt.Println(s)

    f(&s)

    fmt.Println(s)

    f(&s)

    fmt.Println(s)
}
[]
        [1]
[1]
        [1 1]
[1 1]

サンプル

package slice_

import "fmt"

// SlicePointer は、スライスの ポインタ 利用時についてのサンプルです.
func SlicePointer() error {
    // ----------------------------------------------------------------
    // スライスのポインタ利用について
    //
    // スライスは内部構造で参照先となっている配列のポインタを持っているため
    // 通常ポインタを利用する必要はないと記載されている場合があるが
    // C#やJavaなどでよくやるように、メソッドにリストを渡して、メソッド内部で
    // そのリストに対して、要素を追加したりする場合、スライスをポインタで渡して
    // 処理したほうが良い場合がある。
    //
    // スライスは以下のような構造となっている。
    // (https://golang.org/pkg/reflect/#SliceHeader)
    //
    // type SliceHeader struct {
    //     Data uintptr
    //     Len  int
    //     Cap  int
    // }
    //
    // 関数に、スライスをそのまま渡すと、この構造体が値コピーされて渡る。
    // 内部にデータへのポインタを持っているので、そのまま渡しても参照先の配列
    // は同じポインタを示すので、通常は問題ない。
    //
    // しかし、渡された関数側でスライスの要素を追加したりする場合は
    // 元々、スライス作成時に指定した cap を超える場合がある。
    // その場合、Goは自動で新しい配列を用意してDataのポインタを変更してくれる。
    // この挙動により、関数内部でスライスをいじった場合に呼び出し元と関数側の
    // Dataフィールドのポインタが異なってしまうため、追加された要素が反映されない。
    //
    // また、cap を超えない場合でも、スライス自身を値渡ししているため
    // Capフィールドが更新されないので、呼び元で値を見ても反映されていないように
    // 見える。この場合は以下のようにすることで、最新のcapを適用することができる。
    //
    //     sli := sli[:cap(sli)]
    //
    // 上記の問題は、どちらも スライス を ポインタ で渡すことで解決はできる。
    // ----------------------------------------------------------------
    // スライスを値渡しで受け取って、要素を追加
    dump := func(sli []int, prefix string) {
        fmt.Printf("[%-25s] len:%d\tcap:%d\tvalues:%v\n", prefix, len(sli), cap(sli), sli)
    }

    byVal := func(sli []int, count int) {
        for i := 0; i < count; i++ {
            sli = append(sli, i)
        }

        dump(sli, "byVal call")
    }

    byRef := func(sli *[]int, count int) {
        for i := 0; i < count; i++ {
            *sli = append(*sli, i)
        }

        dump(*sli, "byRef call")
    }

    // ---------------------------------------
    // スライスを値渡しの場合
    //   int の スライスを作成( cap は 1 )
    // ---------------------------------------
    sliByVal := make([]int, 0, 1)
    dump(sliByVal, "sliByVal init")

    // 要素を一つ追加
    //   内部で要素が追加されるがcapが更新されていないため
    //   呼び元でそのまま見ると、追加された要素が見えない。
    //   capに到達していないので、capを更新すると追加された要素が見える.
    byVal(sliByVal, 1)
    dump(sliByVal, "sliByVal - byVal(1) - 1")

    // cap を更新
    sliByVal = sliByVal[:cap(sliByVal)]
    dump(sliByVal, "sliByVal - update cap")

    // 要素をさらに一つ追加
    //   内部で要素が追加されるタイミングでcapに到達するため
    //   goは自動的に新しい配列を用意してスライス内部の配列の
    //   参照を書き換える. 呼び元のスライスが元々参照していた
    //   データ配列の参照とは異なる状態になっているため
    //   あとで、capを更新しても追加された要素は見ることができない。
    //   (別の配列に変わってしまっているため)
    byVal(sliByVal, 1)
    dump(sliByVal, "sliByVal - byVal(1) - 2")

    // cap を更新
    sliByVal = sliByVal[:cap(sliByVal)]
    dump(sliByVal, "sliByVal - update cap")

    fmt.Println("-----------------------------")

    // ---------------------------------------
    // スライスをポインタ渡しの場合
    //   int の スライスを作成 ( cap は 1 )
    // ---------------------------------------
    sliByRef := make([]int, 0, 1)
    dump(sliByRef, "sliByRef init")

    // 要素を一つ追加
    //   内部で要素が追加されるが、スライス自身をポインタで
    //   渡しているため、capの更新もそのまま見えている。
    //   なので、 cap の更新は必要なく、追加された要素も見える。
    byRef(&sliByRef, 1)
    dump(sliByRef, "sliByRef - byRef(1) - 1")

    // 要素をさらに一つ追加
    //   内部で要素が追加されるタイミングでcapに到達するため
    //   goは自動的に新しい配列を用意してスライス内部の配列の
    //   参照を書き換える. 呼び元のスライスが元々参照していた
    //   データ配列の参照とは異なる状態になっているが
    //   スライス自身をポインタで渡しているため、その変更は
    //   呼び元のスライスにもそのまま適用されている。
    //   なので、データは何もせずとも更新後がちゃんと見える。
    byRef(&sliByRef, 1)
    dump(sliByRef, "sliByRef - byRef(1) - 2")

    return nil
}

try-golang/slice_pointer.go at master · devlights/try-golang · GitHub

実行結果は以下のような感じ。

[Name] "slice_pointer"
[sliByVal init            ] len:0       cap:1   values:[]
[byVal call               ] len:1       cap:1   values:[0]
[sliByVal - byVal(1) - 1  ] len:0       cap:1   values:[]
[sliByVal - update cap    ] len:1       cap:1   values:[0]
[byVal call               ] len:2       cap:2   values:[0 0]
[sliByVal - byVal(1) - 2  ] len:1       cap:1   values:[0]
[sliByVal - update cap    ] len:1       cap:1   values:[0]
-----------------------------
[sliByRef init            ] len:0       cap:1   values:[]
[byRef call               ] len:1       cap:1   values:[0]
[sliByRef - byRef(1) - 1  ] len:1       cap:1   values:[0]
[byRef call               ] len:2       cap:2   values:[0 0]
[sliByRef - byRef(1) - 2  ] len:2       cap:2   values:[0 0]

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com