はじめに
Goのスライスタイプは、型付きデータのシーケンスを扱うための便利で効率的な手段を提供します。スライスは他の言語の配列に類似していますが、いくつかの特異な特性があります。この記事では、スライスが何であるか、そしてどのように使用されるかを見ていきます。
配列
スライスタイプはGoの配列タイプの上に構築された抽象化であり、スライスを理解するためにはまず配列を理解する必要があります。
配列タイプの定義は、長さと要素タイプを指定します。例えば、タイプ[4]int
は4つの整数の配列を表します。配列のサイズは固定されており、その長さはタイプの一部です([4]int
と[5]int
は異なる互換性のないタイプです)。配列は通常の方法でインデックス指定できるため、式s[n]
はゼロから始まるn番目の要素にアクセスします。
var a [4]int
a[0] = 1
i := a[0]
// i == 1
配列は明示的に初期化する必要はありません。配列のゼロ値は、要素がすべてゼロに初期化された使用可能な配列です:
// a[2] == 0, the zero value of the int type
![](https://cdn.hedaai.com/projects/go-latest/fda1ec30ff4bbdaedb28dd540e7910f2.png_big1500.jpeg)
Goの配列は値です。配列変数は全体の配列を示し、最初の配列要素へのポインタではありません(Cの場合のように)。これは、配列値を割り当てたり渡したりすると、その内容のコピーが作成されることを意味します。(コピーを避けるために配列への*ポインタ*を渡すこともできますが、それは配列へのポインタであり、配列ではありません。)配列を考える一つの方法は、インデックス付きのフィールドを持つ構造体の一種として考えることです:固定サイズの複合値です。
配列リテラルは次のように指定できます:
``````bash
b := [2]string{"Penn", "Teller"}
`
または、コンパイラに配列要素をカウントさせることもできます:
b := [...]string{"Penn", "Teller"}
スライス
配列にはその役割がありますが、少し柔軟性に欠けるため、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
です。len
およびcap
関数は、nilスライスに対して両方とも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
スライス式の開始インデックスと終了インデックスはオプションであり、それぞれデフォルトでゼロとスライスの長さになります:
// 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
は、次のように構造化されています:
長さはスライスが参照する要素の数です。容量は基になる配列の要素数(スライスポインタが参照する要素から始まります)です。長さと容量の違いは、次のいくつかの例を通じて明確になります。
``````bash
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[:cap(s)]
スライスはその容量を超えて成長することはできません。そうしようとすると、スライスや配列の境界外にインデックス指定する場合と同様に、ランタイムパニックが発生します。同様に、スライスはゼロ未満に再スライスして配列の以前の要素にアクセスすることはできません。
スライスの成長(コピーと追加関数)
スライスの容量を増やすには、新しい大きなスライスを作成し、元のスライスの内容をそれにコピーする必要があります。この技術は、他の言語の動的配列実装が裏でどのように機能するかを示しています。次の例では、s
の容量を新しいスライスt
を作成し、s
の内容をt
にコピーし、その後スライス値t
を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`````を使用すると、上記のコードスニペットを簡素化できます:
``````bash
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
}
``````bash
p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
`
しかし、ほとんどのプログラムは完全な制御を必要としないため、Goはほとんどの目的に適した組み込みの`````append`````関数を提供しています。この関数のシグネチャは
``````bash
func append(s []T, x ...T) []T
`
``````bash
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の言語仕様はスライスとその関連するヘルパー関数を定義しています。