いろいろ備忘録日記

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

Goメモ-23 (スライス, Slices, Tour of Go)

概要

Tour of Go の - Slices についてのサンプル。

tour.golang.org

Goのスライスは、他の言語でいう 可変長リスト のようなイメージです。

配列の一部をスライスしたものから生成することも出来るし、ゼロから生成することも出来ます。

サイズの拡張は自動で行われます。

スライスは []型名 で表します。

つまり、intのスライスは []int とします。

配列の場合は、[2]int となります。要素数が書かれているかどうかが違いですね。

スライスを作る場合、大きく分けて以下の2つのやり方があります。

  • 組み込み関数 make() を使う
  • 直接書いて定義

組み込み関数作る場合は、以下のようにします。

// make(type, len, cap) の順となっている
// 以下の場合は、intのスライスを初期長さが0で、容量5で生成
var arr = make([]int, 0, 5)

直接記述する場合は、以下のようにします。

// 以下は、intスライスの宣言のみしている。値は nil (ゼロ値)
var arr1 []int
// 以下は、宣言と初期値を一気に設定
var arr2 = []int{1, 2, 3, 4, 5}

配列の長さや容量は、組み込み関数 len()cap() を使って取得できます。

要素の追加は、組み込み関数 append() を使います。

この append() 、ちょっとクセがあるので最初は戸惑いました・・。

var sli1 = []int{1, 2, 3}
sli1 = append(sli1, 4, 5)

のようにして使います。戻り値を受け取っていないと値が変わらないときがあります。要注意。

pythonやっていると

arr = []
arr.append(100)

のように書いたりするので、よく間違えそうになります。

ちなみに、要素の削除も append() を使います。

細かいところは、以下のサンプル見ていただいたほうが早いです。

サンプル

package tutorial

import "fmt"

// Slice は、 Tour of Go - Slices (https://tour.golang.org/moretypes/7) の サンプルです。
func Slice() error {
    // ------------------------------------------------------------
    // Go言語のスライス
    // Go言語のスライスは、他の言語でいう 可変長リスト みたいな感じ.
    // 配列の一部分をスライスしたものから生成することも出来るし、ゼロから生成することも
    // 出来る. サイズの拡張は、自動で行われる.
    //
    // スライスは []型名 で表す.
    //
    // 配列からスライスを取り出す場合は、pythonの様に a[1:4] のように部分を指定する.
    //
    // スライスは、長さ(length) と 容量 (capacity) を持つ
    //   - 長さは、スライスに含まれる要素数
    //     - 長さは len() で取得できる
    //   - 容量は、スライスの最初の要素から数えて、元となる配列の要素数
    //     - 容量は、cap() で取得できる
    // 要素の追加は、 組み込み関数 append() を使用して行う
    // 要素の削除も、 組み込み関数 append() を使用して行う
    //   - ちょっとトリッキー
    //   - append(slice[:i], slice[i+1:]...)
    // スライスのゼロ値は、nil
    //   - nil な スライスは、長さが0で容量も0で、元となる配列の参照もなし
    // スライスを動的サイズで生成するには、組み込み関数 make() を使用して行う
    //
    // スライスは、配列への参照のようなものなので、スライスの要素を変更すると
    // 元の配列にも影響する.
    // ------------------------------------------------------------
    var (
        arr  = [5]int{1, 2, 3, 4, 5}
        sli1 = arr[1:4]
        sli2 = []int{1, 2, 3, 4, 5}
        sli3 []int
    )

    fmt.Printf("%#v\t%#v\t%#v\t%#v\n", arr, sli1, sli2, sli3)
    fmt.Printf("%v\t%v\n", len(sli1), cap(sli1))

    // 組み込み関数 make() を使用して、新たにスライスを生成して割当
    sli3 = make([]int, 5) // 長さが5のスライスを割り当て
    fmt.Printf("%#v\tlen=%d\tcap=%d\n", sli3, len(sli3), cap(sli3))

    sli3 = make([]int, 0, 5) // 長さは0で、容量が5のスライスを割り当て
    fmt.Printf("%#v\tlen=%d\tcap=%d\n", sli3, len(sli3), cap(sli3))

    // 組み込み関数 append() を使用して、スライスに要素を追加する
    // append() は、第一引数に指定したスライスに要素を追加するのではなく、新たなスライスを戻り値で
    // 返すことに注意。以下は、わざと別の変数で受けているが、よく利用するのは以下のようにすること
    //   sli3 = append(sli3, 100, 200)
    sli4 := append(sli3, 100, 200, 300)
    fmt.Printf("%#v\t%#v\n", sli3, sli4)

    // スライスのループは、配列と同様に行う
    for i, v := range sli4 {
        fmt.Print(i, v, "\t")
    }

    fmt.Println("")

    // スライスから要素を削除するのも append() を使用する
    // 要は、削除したい要素のインデックスを覗いてスライスを再構築するという形になる
    // 以下は、インデックス[1](値は200) を削除している
    //
    // sli4[2:]... となっている 「...」はこのシーケンスを展開するという意味
    // python の *list と同じ感じ
    //
    // 可変長引数の場合は、 「...」 が 前に付く
    //   func A(numbers ...int) {}
    //
    // 参考: https://golang.org/ref/spec#Passing_arguments_to_..._parameters
    sli4 = append(sli4[:1], sli4[2:]...)
    fmt.Println(sli4)

    return nil
}

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

実行すると以下な感じ。

[Name] "tutorial_gotour_slice"
[5]int{1, 2, 3, 4, 5}   []int{2, 3, 4}  []int{1, 2, 3, 4, 5}    []int(nil)
3   4
[]int{0, 0, 0, 0, 0}    len=5   cap=5
[]int{} len=0   cap=5
[]int{} []int{100, 200, 300}
0 100   1 200   2 300   
[100 300]

スライスを関数に引数として渡す場合の注意点

スライスを自関数内で作って、値を設定して利用している場合は特に問題はおこなりませんが

それを別の関数に引数として、渡して処理してもらう場合に、注意点があります。

append 関数は、追加を行う際に元の配列に要素が足りない場合は
要素を増やした別の配列を用意して、そちらに移し替えたものを戻り値として返す

私は、今でもよく間違えますww

忘れないうちに以下にソースごとメモです。

package slice_

import "fmt"

// SliceAppend は、スライスの append 利用時についてのサンプルです.
func SliceAppend() error {
    // ----------------------------------------------------------------
    // Go の スライス に対しての append の利用
    //
    // スライスに append 関数を使って要素を追加していく際に
    // 以下の注意点がある。
    //
    // - append 関数は、追加を行う際に元の配列に要素が足りない場合は要素を増やした
    //   別の配列を用意して、そちらに移し替えたものを戻り値として返す.
    //
    // なので、よく別の言語でやるように、関数の引数にリストを渡して関数内の処理で
    // そのリストに要素を追加していくような形の処理をGoでそのまま書くと、元のリストには
    // 何の要素も追加されないという事態が発生するので注意が必要。
    //
    // 回避としては、元々のサイズをしっかり確保したスライスを作っておくか
    // 関数に渡すのではなく、関数内でスライスを作って戻り値で返して、呼び元で統合するなどがある.
    //
    // その他にも、関数内で引数で指定されたスライスに対して append すると、その関数内では
    // 値は更新されているが、呼び出し元で見てみると、更新されていない現象というのもある。
    // これは、同じ配列を示しているが、呼び出し元の slice の len が更新されていないから。
    // (https://christina04.hatenablog.com/entry/2017/09/26/190000)
    //
    // 呼び出し元のlenを更新するには
    //   ints = ints[:cap(ints)]
    // という呼び出しを入れる。これで、このスライスのlenが更新されることになる。
    //
    // 以下の例では、capがゼロなスライスと余裕も持っているスライスの2つを定義して
    // 関数の引数として渡して、その中で10個の要素をappendしている。
    // capがゼロのスライスは、その際に必ず拡張が発生するので、別のアドレスを指し示すように
    // 変わってしまう。
    // ----------------------------------------------------------------
    var (
        zeroCapSlice = make([]int, 0, 0)
        manyCapSlice = make([]int, 0, 15)
    )

    fmt.Printf("[zeroCap] len:%d\tcap:%d\taddr:[%p]\n", len(zeroCapSlice), cap(zeroCapSlice), zeroCapSlice)
    appendItems(zeroCapSlice)

    fmt.Printf("[manyCap] len:%d\tcap:%d\taddr:[%p]\n", len(manyCapSlice), cap(manyCapSlice), manyCapSlice)
    appendItems(manyCapSlice)

    // 別の関数内で要素を追加していても、呼び出し元のスライスから見るとlenが変わっていないため
    // 値が表示されない。以下のように 再度スライスをすることによって len が更新される。
    // (https://christina04.hatenablog.com/entry/2017/09/26/190000)
    zeroCapSlice = zeroCapSlice[:cap(zeroCapSlice)]
    manyCapSlice = manyCapSlice[:cap(manyCapSlice)]

    fmt.Println("----------------------------------------------")
    fmt.Printf("[zeroCap] len:%d\tcap:%d\taddr:[%p]\n", len(zeroCapSlice), cap(zeroCapSlice), zeroCapSlice)
    fmt.Printf("[manyCap] len:%d\tcap:%d\taddr:[%p]\n", len(manyCapSlice), cap(manyCapSlice), manyCapSlice)
    fmt.Println("zeroCapSlice", zeroCapSlice)
    fmt.Println("manyCapSlice", manyCapSlice)
    fmt.Println("----------------------------------------------")

    // 安全なのが、関数に引数で渡して更新するのではなく、関数内で別途スライスを作って戻り値として返すようにすること.
    // 呼び元は、受け取ったデータを自分で管理しているスライスに追加する.
    var (
        zeroCapSlice2 = make([]int, 0, 0)
        manyCapSlice2 = make([]int, 0, 15)
    )

    fmt.Printf("[zeroCap2] len:%d\tcap:%d\taddr:[%p]\n", len(zeroCapSlice2), cap(zeroCapSlice2), zeroCapSlice2)
    fmt.Printf("[manyCap2] len:%d\tcap:%d\taddr:[%p]\n", len(manyCapSlice2), cap(manyCapSlice2), manyCapSlice2)
    fmt.Println("zeroCapSlice2", zeroCapSlice2)
    fmt.Println("manyCapSlice2", manyCapSlice2)

    items := retrieveItems(10)
    for _, v := range items {
        zeroCapSlice2 = append(zeroCapSlice2, v)
        manyCapSlice2 = append(manyCapSlice2, v)
    }

    fmt.Printf("[zeroCap2] len:%d\tcap:%d\taddr:[%p]\n", len(zeroCapSlice2), cap(zeroCapSlice2), zeroCapSlice2)
    fmt.Printf("[manyCap2] len:%d\tcap:%d\taddr:[%p]\n", len(manyCapSlice2), cap(manyCapSlice2), manyCapSlice2)
    fmt.Println("zeroCapSlice2", zeroCapSlice2)
    fmt.Println("manyCapSlice2", manyCapSlice2)
    fmt.Println("----------------------------------------------")

    return nil
}

func retrieveItems(count int) []int {
    r := make([]int, 0, count)
    for i := 0; i < count; i++ {
        r = append(r, i)
    }

    return r
}

func appendItems(ints []int) {
    fmt.Printf("\t[append][before] len:%d\tcap:%d\taddr:[%p]\n", len(ints), cap(ints), ints)

    for i := 0; i < 10; i++ {
        ints = append(ints, i)
    }

    fmt.Printf("\t[append][after]  len:%d\tcap:%d\taddr:[%p]\n", len(ints), cap(ints), ints)
}

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

実行すると以下な感じ。

[Name] "slice_append"
[zeroCap] len:0 cap:0   addr:[0x67a5e0]
    [append][before] len:0  cap:0   addr:[0x67a5e0]
    [append][after]  len:10 cap:16  addr:[0xc0000a6080]
[manyCap] len:0 cap:15  addr:[0xc0000a6000]
    [append][before] len:0  cap:15  addr:[0xc0000a6000]
    [append][after]  len:10 cap:15  addr:[0xc0000a6000]
----------------------------------------------
[zeroCap] len:0 cap:0   addr:[0x67a5e0]
[manyCap] len:15    cap:15  addr:[0xc0000a6000]
zeroCapSlice []
manyCapSlice [0 1 2 3 4 5 6 7 8 9 0 0 0 0 0]
----------------------------------------------
[zeroCap2] len:0    cap:0   addr:[0x67a5e0]
[manyCap2] len:0    cap:15  addr:[0xc0000a6100]
zeroCapSlice2 []
manyCapSlice2 []
[zeroCap2] len:10   cap:16  addr:[0xc0000a6180]
[manyCap2] len:10   cap:15  addr:[0xc0000a6100]
zeroCapSlice2 [0 1 2 3 4 5 6 7 8 9]
manyCapSlice2 [0 1 2 3 4 5 6 7 8 9]
----------------------------------------------

スライスのポインタ

スライスのポインタについては、別記事で追記しました。以下参照ください。

devlights.hatenablog.com


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

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

devlights.github.io

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

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

github.com

github.com

github.com