Goブログ

Goのスライス:使用方法と内部構造

Andrew Gerrand
2011年1月5日

はじめに

Goのスライス型は、型付きデータのシーケンスを扱うための便利で効率的な手段を提供します。スライスは他の言語の配列に似ていますが、いくつかの独特の特性を持っています。この記事では、スライスとは何か、そしてどのように使用されるかを見ていきます。

配列

スライス型はGoの配列型の上に構築された抽象化であり、スライスを理解するにはまず配列を理解する必要があります。

配列型の定義は、長さおよび要素型を指定します。例えば、型`[4]int`は4つの整数の配列を表します。配列のサイズは固定されており、その長さは型の構成要素です(`[4]int`と`[5]int`は区別され、互換性のない型です)。配列は通常の方法でインデックス付けできるため、式`s[n]`は0から始まるn番目の要素にアクセスします。

var a [4]int
a[0] = 1
i := a[0]
// i == 1

配列は明示的に初期化する必要はありません。配列のゼロ値は、その要素がゼロに設定された、すぐに使用できる配列です。

// a[2] == 0, the zero value of the int type

`[4]int`のメモリ内表現は、単に4つの整数の値が連続して配置されているだけです。

Goの配列は値です。配列変数は配列全体を表し、最初の配列要素へのポインタではありません(Cの場合のように)。つまり、配列の値を代入または渡す場合、その内容のコピーが作成されます。(コピーを回避するために、配列へのポインタを渡すこともできますが、それは配列へのポインタであり、配列ではありません。)配列を考えると、インデックス付きのフィールドではなく名前付きフィールドを持つstructのようなものと考えることができます。固定サイズの複合値です。

配列リテラルは次のように指定できます。

b := [2]string{"Penn", "Teller"}

または、コンパイラに配列要素を数えさせることもできます。

b := [...]string{"Penn", "Teller"}

どちらの場合も、`b`の型は`[2]string`です。

スライス

配列には用途がありますが、やや柔軟性に欠けるため、Goコードではあまり見かけません。しかし、スライスはどこにでもあります。スライスは配列を基に構築され、大きなパワーと利便性を提供します。

スライスの型指定は`[]T`であり、`T`はスライスの要素の型です。配列型とは異なり、スライス型には長さは指定されていません。

スライスリテラルは、要素数を除いて配列リテラルと同じように宣言されます。

letters := []string{"a", "b", "c", "d"}

スライスは、`make`という組み込み関数を使用して作成できます。この関数のシグネチャは次のとおりです。

func make([]T, len, cap) []T

ここで、Tは作成するスライスの要素型を表します。`make`関数は型、長さ、およびオプションの容量を取ります。呼び出されると、`make`は配列を割り当て、その配列を参照するスライスを返します。

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

容量引数を省略すると、指定された長さにデフォルト設定されます。同じコードのより簡潔なバージョンを次に示します。

s := make([]byte, 5)

スライスの長さおよび容量は、組み込みの`len`関数および`cap`関数を使用して検査できます。

len(s) == 5
cap(s) == 5

次の2つのセクションでは、長さおよび容量の関係について説明します。

スライスのゼロ値は`nil`です。`nil`スライスでは、`len`関数と`cap`関数の両方が0を返します。

スライスは、既存のスライスまたは配列を「スライス」することによっても形成できます。スライスは、コロンで区切られた2つのインデックスを使用して半開区間を指定することによって行われます。たとえば、式`b[1:4]`は`b`の要素1〜3を含むスライスを作成します(結果のスライスのインデックスは0〜2になります)。

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b

スライス式の開始インデックスと終了インデックスはオプションです。それぞれ0とスライスの長さにデフォルト設定されます。

// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b

これは、配列を指定してスライスを作成するための構文でもあります。

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x

スライスの内部構造

スライスは、配列セグメントの記述子です。配列へのポインタ、セグメントの長さ、およびその容量(セグメントの最大長)で構成されます。

前に`make([]byte, 5)`で作成した変数`s`は、次のような構造になっています。

長さは、スライスによって参照される要素の数です。容量は、基礎となる配列内の要素数です(スライスポインタによって参照される要素から始まります)。長さおよび容量の違いは、次のいくつかの例を説明する際に明らかになります。

`s`をスライスする際に、スライスデータ構造の変化とその基礎となる配列との関係を観察します。

s = s[2:4]

スライスはスライスのデータをコピーしません。元の配列を指す新しいスライス値を作成します。これにより、スライス操作は配列インデックスを操作するのと同じくらい効率的になります。したがって、(スライス自体ではなく)再スライスの要素を変更すると、元のスライスの要素が変更されます。

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

前に`s`を容量より短い長さにスライスしました。`s`を再度スライスすることで、`s`を容量まで増やすことができます。

s = s[:cap(s)]

スライスは、その容量を超えて増やすことはできません。そうしようとすると、スライスまたは配列の範囲外にインデックスを付ける場合と同様に、ランタイムパニックが発生します。同様に、スライスは0より下に再スライスして、配列内の前の要素にアクセスすることはできません。

スライスの拡張(コピー関数と追加関数)

スライスの容量を増やすには、新しくより大きなスライスを作成し、元のスライスの内容をそのスライスにコピーする必要があります。この手法は、他の言語の動的配列の実装が内部的に機能する方法です。次の例では、新しいスライス`t`を作成し、`s`の内容を`t`にコピーし、スライス値`t`を`s`に代入することで、`s`の容量を2倍にします。

t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
for i := range s {
        t[i] = s[i]
}
s = t

この一般的な操作のループ部分は、組み込みのコピー関数によって簡単になります。名前が示唆するように、copyはデータソーススライスから宛先スライスにデータをコピーします。コピーされた要素の数を返します。

func copy(dst, src []T) int

copy関数は、長さの異なるスライス間のコピーをサポートします(より小さい要素数までしかコピーしません)。さらに、copyは、基礎となる配列が同じであるソーススライスと宛先スライスを処理し、重複するスライスを正しく処理できます。

copyを使用して、上記のコードスニペットを簡素化できます。

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

一般的な操作は、データを終端のスライスに追加することです。この関数は、バイトのスライスにバイト要素を追加し、必要に応じてスライスを拡張し、更新されたスライス値を返します。

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

`AppendByte`は次のように使用できます。

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

`AppendByte`のような関数は、スライスの拡張方法を完全に制御できるため便利です。プログラムの特性によっては、より小さくまたは大きくチャンクを割り当てること、または再割り当てのサイズに上限を設定することが望ましい場合があります。

しかし、ほとんどのプログラムは完全な制御を必要としないため、Goはほとんどの目的に適した組み込みの`append`関数を提供しています。シグネチャは次のとおりです。

func append(s []T, x ...T) []T

`append`関数は、要素`x`をスライス`s`の最後に追加し、より大きな容量が必要な場合はスライスを拡張します。

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

1つのスライスを別のスライスに追加するには、`...`を使用して2番目の引数を引数のリストに展開します。

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

スライスのゼロ値(`nil`)はゼロ長のスライスのように動作するため、スライス変数を宣言してからループ内で追加できます。

// Filter returns a new slice holding only
// the elements of s that satisfy fn()
func Filter(s []int, fn func(int) bool) []int {
    var p []int // == nil
    for _, v := range s {
        if fn(v) {
            p = append(p, v)
        }
    }
    return p
}

考えられる「落とし穴」

前述のように、スライスを再スライスしても、基礎となる配列のコピーは作成されません。参照されなくなるまで、完全な配列がメモリに保持されます。場合によっては、必要なのはその小さな部分だけなのに、プログラムがすべてのデータをメモリに保持してしまう可能性があります。

たとえば、この`FindDigits`関数はファイルをメモリに読み込み、連続した数字の最初のグループを検索し、新しいスライスとして返します。

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

このコードは宣伝どおりに動作しますが、返される`[]byte`は、ファイル全体を含む配列を指します。スライスは元の配列を参照するため、スライスが保持されている限り、ガベージコレクタは配列を解放できません。ファイルのいくつかの有用なバイトによって、ファイル全体の内容がメモリに保持されます。

この問題を解決するには、興味のあるデータを新しいスライスにコピーしてから返すことができます。

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}

この関数のより簡潔なバージョンは、`append`を使用して構築できます。これは読者の課題として残されています。

さらに読む

Effective Goにはスライス配列に関する詳細な説明が含まれており、Goの言語仕様スライスとその関連するヘルパー関数を定義しています。

次の記事:JSONとGo
前の記事:Go:1年前の今日
ブログインデックス