いろいろ備忘録日記

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

Goメモ-44 (スライスのクリア、及び、nilスライスと空スライス)

概要

Goで、スライスはめっちゃ使うと思いますが、スライスのクリアについてメモメモ。

あとついでに、nilスライスと空スライスについてもメモメモ。

スライスのクリア

以下の記事がとてもわかり易かったです。

yourbasic.org

要約すると、スライスをクリアするために nil を設定すると綺麗に len() も cap() もクリアされる。データも削除される。

s = nil

別の方法として

s = s[:0]

という風に、スライスが参照する範囲だけを変更した場合は len() は 0 になるけど cap() はそのまま。データもそのまま。

なので

s[:2]

とかすると、元のデータが見える。

基本的に、クリアしたい場合はデータ毎クリアしたいときが多いので

s = nil

s = []int{}

のようにするのがベターかなって思っています。

nilスライスと空スライス

スライスにて、以下のスライスは機能的には同じです。

  • nilスライス
  • non-nilだけど、空のスライス

nilスライスは以下のことです。

var s1 []int

non-nilだけど、空のスライスは以下のことです。

s2 := []int{}

どちらも len(), cap() は 0 です。

どっち使っても良いみたいなのですが

github.com

Go Wikiによると Go チームとしては

var s1 []int

を推奨しているみたいですね。私は、他の言語のクセが残っててどうしても

s2 := []int{}

ってしちゃうんですよねー。nullなシーケンスって状態が嫌。。

Goやってる限りはGo流でいきたいところ。

あと、余談ですがGoLand使っている場合は nil スライス に対してイリーガルな範囲操作しようとするとインスペクションで警告が出て気づきに便利です。空スライスの場合、インスペクションで警告となりません。

また、JSONを扱う場合、nilスライスだとnullって変換されます。

空のスライスだと [] って変換されるので、JSONの場合は non-nilな空スライスの方がいいですね。

サンプル

package slice_

import "github.com/devlights/try-golang/lib/output"

// SliceClear は、スライスのクリア、及び、nilスライスと空のスライスについてのサンプルです.
func SliceClear() error {
    // ----------------------------------------------------------------
    // スライスのクリア、及び、nilスライスと空のスライスについて
    //
    // スライスで以下の2つは機能的に同じ
    // (1) nilスライス
    //     var s []int
    // (2) non-nilだけど長さが0のスライス(空のスライス)
    //     var s []int{}
    //
    // 上記のどちらも len(s), cap(s) は 0 となる。
    // Go Wiki では、(1) の nilスライスを推奨している。
    // ただし、JSONを扱う場合、nilスライスはnullとなってしまい
    // 空スライスは [] と表現されるので、JSONの場合は空スライスの方が良い。
    //
    // 個人的には、他の言語のクセで []int{} の方が好きなので、こっちをよく使ってしまっている..
    // Goらしく、nilスライスを使うようにしようと矯正中。
    // 余談であるが、nilスライスにしておくと、範囲外のアクセスをしているコードがある場合に
    // GoLandだとインスペクションで警告してくれるので、こっちの方が良さそう。
    // (空スライスだとインスペクションで引っかからない)
    //
    // スライスのクリアには、大きく2つある。
    // (1) nilを代入
    //     nilスライスになるので、データがクリアされる。
    //     スライスに設定されていたデータは、GCによって開放される対象となる
    // (2) スライスが参照している場所を変更する
    //     s[:0]と代入することで、長さが0の範囲を参照するスライスにしてしまう。
    //     この場合、参照している範囲を変えただけなのでメモリはクリアされていない
    //     見た目上、長さは0になるが、範囲を広げると元のデータが見える。
    //     capはそのまま。
    //
    // REF:
    //   https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices
    //   https://yourbasic.org/golang/clear-slice/
    // ----------------------------------------------------------------
    s1 := []int{1, 2, 3, 4, 5}
    s2 := []int{1, 2, 3, 4, 5}
    s3 := []int{1, 2, 3, 4, 5}

    output.Stdoutl("s1", s1, len(s1), cap(s1))
    output.Stdoutl("s2", s2, len(s2), cap(s2))
    output.Stdoutl("s3", s3, len(s3), cap(s3))

    // nilを設定
    // len(s1), cap(s1) も 0 になる
    s1 = nil
    output.Stdoutl("s1", s1, len(s1), cap(s1))

    // 範囲を0にする
    // len(s2) は 0 になるが cap は残る
    s2 = s2[:0]
    output.Stdoutl("s2", s2, len(s2), cap(s2))

    // 空スライスを設定
    // len(), cap() ともに 0 となる
    s3 = []int{}
    output.Stdoutl("s3", s3, len(s3), cap(s3))

    // メモリ上にデータは残っているので範囲を広げると
    // 元のデータは見える
    output.Stdoutl("s2[:2]", s2[:2])

    // nil スライスの場合は、メモリからデータを消しているので
    // s1[:2]とかすると存在しない範囲に対してのアクセスとなる
    // つまり panic する
    defer func() {
        err := recover()
        output.Stdoutl("s1[:2]", err)
    }()

    //noinspection GoNilness
    output.Stdoutl("s1[:2]", s1[:2])
    // nil スライスと機能的には同じなので、以下は同じく panic する
    // nil スライスだとGoLandのインスペクションで警告されるが
    // 空スライスだと警告されない。
    output.Stdoutl("s3[:2]", s3[:2])

    return nil
}

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

実行すると以下のようになります.

[Name] "slice_clear"
s1 [1 2 3 4 5] 5 5
s2 [1 2 3 4 5] 5 5
s3 [1 2 3 4 5] 5 5
s1 [] 0 0
s2 [] 0 5
s3 [] 0 0
s2[:2] [1 2]
s1[:2] runtime error: slice bounds out of range [:2] with capacity 0

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

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

devlights.github.io

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

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

github.com

github.com

github.com