この記事は、 Arrays, slices (and strings): The mechanics of 'append' を翻訳、加筆したものです。
Introduction
手続き型プログラミング言語の最も一般的な機能の1つは、配列の概念です。
配列は単純なもののように見えますが、言語に配列を追加するときのメカニズムには多くの疑問が残ります。
- 固定長 or 可変長?
- 要素の型は?
- 多次元配列はどうなっているか?
- 空の配列の意味は?
これらの疑問への回答は、配列が言語の単なる機能であるか、その設計のコア部分であるかにまで影響してきます。
Goの初期の開発ではこれらの疑問に対する答えを持った正しい設計を考えるのに約1年かかりました。
一番苦労したのは、スライスの導入でした。
スライスは、固定サイズの配列に基づいて構築され、柔軟で拡張可能なデータ構造を提供します。
しかし、Goを初めて使用するプログラマーは、スライスの動作に関してつまずくことがよくあります。 これは、おそらく他の言語での経験が影響しています。
この投稿では、混乱を解消しようとします。
これを行うには、組み込み関数append
がどのように機能するのか、そしてなぜそれがそのように機能するのかを説明するために必要なコンポーネントを一つ一つ解説していきます。
配列(Arrays)
配列はGoの重要な構成要素ですが、スライスの背後に隠れた重要なコンポーネントです。
スライスが持つ、興味深く、強力で、目立つアイデアに移る前に、配列についての簡単な説明が必要になってきます。
配列は固定長なのであまりGoのプログラム中で見かけることはありません。
宣言する際には
var buffer [256]byte
この宣言では 256バイトを保持するbuffer
という変数を宣言しています。
buffer
は型が[256]byte
とサイズを含んだ型になっています。 512バイトを保持する場合は[512]byte
になります。
配列と結び付けられているデータは、文字通り要素の配列です。 概略的には、buffer
はメモリ内で次のようになります。
buffer: byte byte byte ... 256 times ... byte byte byte
つまりこの変数は256バイトのデータ以外は何も持っていません。
各要素にはよく見慣れたインデックスを使ってアクセスできます。(buffer[0], buffer[1], ..., buffer[255])
配列外にインデックスアクセスしようとしたときにはプログラムはクラッシュします。
配列、スライスや他のいくつかのデータ型は、要素数を返すlen
と呼ばれる組み込み関数があります。
配列の場合、lenが何を返すかは明らかです。 この例では、len(buffer)
は固定値256を返します。
配列の使い所としては変換行列を適切に表現する場合などが挙げられますが、Goでの最も一般的な目的はスライスのストレージを保持することです。
Slices: スライスヘッダについて
スライスはよく使用されるデータ構造ですが、うまく使用するには、スライスが何であり何をするのかを正確に理解する必要があります。
スライスはその内部で、配列のある連続な区間を記述しているデータ構造で、スライス自体は配列ではありません。スライスは配列の一部を表しています。
前のセクションの配列を表す変数buffer
を前提としてこの配列をスライスすることにより、要素100から150(正確には、100から149を含む)を記述するスライスを作成できます。
var slice []byte = buffer[100:150]
上の例では、明示的な変数宣言を使用しました。
変数slice
は[]byte
型で、「byte型のスライス」と発音され、要素100から150をスライスすることで、配列buffer
から初期化しています。
より慣用的な構文では、設定される型の記述が省略できます。
var slice = buffer[100:150]
関数内部では次のような短い宣言も可能です。
slice := buffer[100:150]
このスライス変数は正確にはどういう構造をしているのでしょうか?
全てをここでは話しませんが、今のところスライスは長さと配列の要素へのポインタという2つの要素を持つ小さなデータ構造と考えてください。
つまりスライスは裏でこのように構築されていると考えることができます:
type sliceHeader struct {
Length int
ZerothElement *byte
}
slice := sliceHeader{
Length: 50,
ZerothElement: &buffer[100],
}
もちろん、これは単なる例で実際の構造ではありません。
このコード例では、sliceHeader構造体はプログラマには表示されず、配列へのポインタの型は配列の要素の型によって異なりますが、これにより、メカニズムの一般的な考え方がわかります。
これまで、配列に対してスライス操作を使用してきましたが、次のようにスライスをスライスすることもできます。
slice2 := slice[5:10]
前と同じように、この操作は新しいスライスを作成します。
この場合、元のスライスの要素5から9([5,9])を使用します。
これは、元の配列の要素105から109を表しています。
変数slice2
の基になるsliceHeader
構造体は次のようになります。
slice2 := sliceHeader{
Length: 5,
ZerothElement: &buffer[105],
}
このヘッダは配列の参照先として、もとになったslice
と同じbuffer
を参照していることに注意してください。
再度スライスすることもできます。つまり、スライスを切り取って、次のように結果を元のスライス構造に保存します。
slice = slice[5:10]
変数slice
のsliceHeader
構造は、変数slice2
の場合と同じように見えます。
スライスの一部を切り捨てる場合にこのような方法がよく取られます。次の例はスライスの最初と最後の要素を切り捨てています。
slice = slice[1:len(slice)-1]
経験豊富なGoプログラマーが「スライスヘッダ」について話すのをよく耳にします。
なぜならスライスヘッダこそが、実際にはスライス変数に格納されているものだからです。
たとえば、bytes.IndexRune
など、引数としてスライスを受け取る関数を呼び出すと、そのスライスヘッダが関数に渡されます。
slashPos := bytes.IndexRune(slice, '/')
例えば、この関数呼び出しでは実際に関数に渡されているのはスライスヘッダです。
スライスヘッダにはもう1つのデータ項目があります。これについては以降で説明しますが、最初にスライスを使用したプログラムを書く場合にスライスヘッダの存在がどのような意味を持ってくるのかを見てみましょう。
スライスを引数としたとき
スライスにポインタが含まれていても、スライス自体は値であるという事実を理解することが重要です。
スライスの構造は、ポインタと長さを保持する構造体の値です。 構造体へのポインタではありません。
これは重要です。前の例でIndexRune
を呼び出したとき、スライスヘッダのコピーが渡されました。 その動作には重要な影響があります。
この単純な関数を考えてみましょう。
func AddOneToEachElement(slice []byte) {
for i := range slice {
slice[i]++
}
}
名前通り、スライスをループして各イテレーションでスライスの要素をインクリメントする関数です。
次のプログラムを試してみましょう。
func main() {
slice := buffer[10:20]
for i := 0; i < len(slice); i++ {
slice[i] = byte(i)
}
fmt.Println("before", slice)
AddOneToEachElement(slice)
fmt.Println("after", slice)
}
before [0 1 2 3 4 5 6 7 8 9]
after [1 2 3 4 5 6 7 8 9 10]
Program exited.
スライスヘッダ自体は値として渡されました(つまりコピーして渡された)が、ヘッダには配列の要素へのポインタが含まれているため、元のスライスヘッダーと関数に渡されるヘッダのコピーの両方が同じ配列を参照します。
したがって関数が戻ると、変更された要素は元のスライス変数を通して見ることができます。
この例が示すように、関数の引数は実際にはコピーです。
func SubtractOneFromLength(slice []byte) []byte {
slice = slice[0 : len(slice)-1]
return slice
}
func main() {
fmt.Println("Before: len(slice) =", len(slice))
newSlice := SubtractOneFromLength(slice)
fmt.Println("After: len(slice) =", len(slice))
fmt.Println("After: len(newSlice) =", len(newSlice))
}
Before: len(slice) = 50
After: len(slice) = 50
After: len(newSlice) = 49
Program exited.
この例では、スライス引数の内容は関数で変更できますが、ヘッダそのものは変更できないことがわかります。
関数には元のヘッダではなくスライスヘッダのコピーが渡されるため、スライス変数に格納されている長さ(元のヘッダのLengthプロパティ)は関数の呼び出しによって変更されません。
したがって、ヘッダを変更する関数を作成する場合は、この例で行ったように、変更したスライスを戻り値として返す必要があります。
元のスライス変数は変更されていませんが、戻り値で返されるスライスは新しい長さになり、newSliceに格納されます。
メソッドレシーバとしてのスライスのポインタ
関数がスライスヘッダに干渉するもう一つの方法は、スライスのポインタを渡すことだ
func PtrSubtractOneFromLength(slicePtr *[]byte) {
slice := *slicePtr
*slicePtr = slice[0 : len(slice)-1]
}
func main() {
fmt.Println("Before: len(slice) =", len(slice))
PtrSubtractOneFromLength(&slice)
fmt.Println("After: len(slice) =", len(slice))
}
Before: len(slice) = 50
After: len(slice) = 49
Program exited.
上の例は、あまりスマートな例には見えません。
スライスへのポインタが表示される一般的なケースが1つあります。
例えば、スライスの変更を行うメソッドにレシーバとしてポインタを使用するのはよくある手法です。
ファイルのパスから最後の/
でスライスを切り捨てるメソッドが必要だったとしましょう。
例えば dir1/dir2/dir3
なら dir1/dir2
となる必要があります。
これを満たすメソッドは以下のように書くことができます:
type path []byte
func (p *path) TruncateAtFinalSlash() {
i := bytes.LastIndex(*p, []byte("/"))
if i >= 0 {
*p = (*p)[0:i]
}
}
func main() {
pathName := path("/usr/bin/tso") // Conversion from string to path.
pathName.TruncateAtFinalSlash()
fmt.Printf("%s\n", pathName)
}
実行してみれば適切に動作することがわかるはずです。今回は呼び出し側のメソッドでスライスが変更されています。
一方、パス内のASCII文字を大文字にする(英語以外は無視する)メソッドを記述したい場合、配列の参照先は同じまま、その中身だけを変更するのでメソッドのレシーバは値でも大丈夫です。
type path []byte
func (p path) ToUpper() {
for i, b := range p {
if 'a' <= b && b <= 'z' {
p[i] = b + 'A' - 'a'
}
}
}
func main() {
pathName := path("/usr/bin/tso")
pathName.ToUpper()
fmt.Printf("%s\n", pathName)
}
ここではToUpper
メソッドは配列のインデックスとそのインデックスに対応する要素を持った2つの変数をfor range
構文の中で使っています。
こうすることでp[i]
といちいち記述する回数を減らすことができます。
Capacity: スライスの容量
引数で渡したint型のスライスslice
にelement
を付け足して拡張する以下の関数をみてみよう。
func Extend(slice []int, element int) []int {
n := len(slice)
slice = slice[0 : n+1]
slice[n] = element
return slice
}
実行してみましょう
func main() {
var iBuffer [10]int
slice := iBuffer[0:0]
for i := 0; i < 20; i++ {
slice = Extend(slice, i)
fmt.Println(slice)
}
}
[0]
[0 1]
[0 1 2]
[0 1 2 3]
[0 1 2 3 4]
[0 1 2 3 4 5]
[0 1 2 3 4 5 6]
[0 1 2 3 4 5 6 7]
[0 1 2 3 4 5 6 7 8]
[0 1 2 3 4 5 6 7 8 9]
panic: runtime error: slice bounds out of range [:11] with capacity 10
goroutine 1 [running]:
main.Extend(...)
/tmp/sandbox021597325/prog.go:16
main.main()
/tmp/sandbox021597325/prog.go:25 +0x105
Program exited: status 2.
スライスはどんどん拡張されていきますが、途中で止まってしまいました。
スライスヘッダの3番目のコンポーネントであるcapacity
について説明します。
配列ポインタと長さに加えて、スライスヘッダにはその容量(capacity
)も格納されます。
type sliceHeader struct {
Length int
Capacity int
ZerothElement *byte
}
capacity
フィールドには、基になる配列が実際持っているスペースの量が入ります。
これは、Length
が到達できる最大値です。 スライスをその容量を超えて拡大しようとすると、配列の制限を超える、つまり配列外アクセスをすることになりパニックが発生します。
slice := iBuffer[0:0]
で作成されたスライスのヘッダは
slice := sliceHeader{
Length: 0,
Capacity: 10,
ZerothElement: &iBuffer[0],
}
capacity
フィールドは、基になる配列の長さから、スライスの最初の要素が対応する配列のインデックス(この場合は0)を引いたものに等しくなります。
スライスの容量を確認する場合は、組み込み関数cap
を使用します。
if cap(slice) == len(slice) {
fmt.Println("slice is full!")
}
Make
スライスをその容量を超えて拡張したい場合はどうすればいいでしょうか?
できません! 定義上、capacity
は拡張の限界です。
ただし、新しい配列を割り当て、データをコピーし、スライスの内容を変更して新しい配列を参照することで、同等の結果を得ることができます。
割り当てから始めましょう。 新しい組み込み関数を使用して、より大きな配列を割り当て、結果をスライスすることもできますが、代わりに組み込み関数make
を使用する方が簡単です。
新しい配列を割り当て、それに参照するスライスヘッダを一発で作成します。
make
関数は、スライスの型、その初期の長さ、およびそのcapacity
(makeがスライスデータを保持するために割り当てる配列の長さ)の3つの引数を取ります。
以下の例を実行するとわかるように、この呼び出しは長さ10のスライスを作成し、さらに裏の配列にはサイズ5つ分(15-10)の余裕があります。
slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
len: 10, cap: 15
Program exited.
slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
newSlice := make([]int, len(slice), 2*cap(slice))
for i := range slice {
newSlice[i] = slice[i]
}
slice = newSlice
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
len: 10, cap: 15
len: 10, cap: 30
Program exited.
このコードを実行した後のスライスは、拡張するためのスペースが遥かに増えています (5 -> 20)
スライスを作成するとき、長さと容量が同じになることはよくあることです。 組み込み関数make
には、この一般的なケースの省略形があります。
gophers := make([]Gopher, 10) // = make([]Gopher, 10, 10)
容量を指定しなかった場合、そのデフォルト値は長さと同じになるため、上のように省略して両方を同じ値に設定できます。
Copy
前のセクションでスライスの容量を2倍にしたとき、古いデータを新しいスライスにコピーするループを作成しました。
Goには、これを簡単にするための組み込み関数copy
があります。 その引数は2つのスライスであり、データを右側の引数から左側の引数にコピーします。
copy
を使用するように書き直した例を次に示します。
newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)
関数copy
は賢くコピーを行ってくれます。 両方の引数の長さに注意を払いながら、可能な分だけをコピーします。
つまり、コピーする要素の数は、2つのスライスの長さの最小値です。
これにより、記述量を少し節約できます。 また、copyは、コピーした要素の数である整数値を返しますが、常にチェックする価値があるとは限りません。
関数copy
は、コピー元とコピー先が重複している場合にも適切に処理されます。
つまり、単一のスライスで中身を移動するために使用できます。 コピーを使用してスライスの中央に値を挿入する方法は次のとおりです。
// Insert inserts the value into the slice at the specified index,
// which must be in range.
// The slice must have room for the new element.
func Insert(slice []int, index, value int) []int {
// Grow the slice by one element.
slice = slice[0 : len(slice)+1]
// Use copy to move the upper part of the slice out of the way and open a hole.
copy(slice[index+1:], slice[index:])
// Store the new value.
slice[index] = value
// Return the result.
return slice
}
上の例ではいくつか気を付けるべき点としては、長さが変更されているので変更したスライスを必ず返り値として返す必要があるところでしょう。
また要素を追加するので capacity > length である必要もあります。
Append
数セクション前に、スライスを1つの要素だけ拡張する関数Extend
を作成しました。
ただし、スライスの容量が小さすぎると関数がクラッシュするため、バグがありました。 (Insert
の例にも同じ問題があります。)
これを修正するための要素が揃ったので、整数スライスを拡張するExtend
のしっかりした実装を作成しましょう。
func Extend(slice []int, element int) []int {
n := len(slice)
if n == cap(slice) {
// スライスが満タンなので拡張の必要あり
// 新しい容量は 2x+1 とする (+1しているのはx=0のときのため)
newSlice := make([]int, len(slice), 2*len(slice)+1)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : n+1]
slice[n] = element
return slice
}
この場合も、参照先の配列がまるっきり変わるので、スライスを返り値として返す必要があります。
Extend
関数を使って拡張を行ってスライスが満タンになったときの挙動を確認してみましょう。
slice := make([]int, 0, 5)
for i := 0; i < 10; i++ {
slice = Extend(slice, i)
fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
fmt.Println("address of 0th element:", &slice[0])
}
len=1 cap=5 slice=[0]
address of 0th element: 0xc000078030
len=2 cap=5 slice=[0 1]
address of 0th element: 0xc000078030
len=3 cap=5 slice=[0 1 2]
address of 0th element: 0xc000078030
len=4 cap=5 slice=[0 1 2 3]
address of 0th element: 0xc000078030
len=5 cap=5 slice=[0 1 2 3 4]
address of 0th element: 0xc000078030
len=6 cap=11 slice=[0 1 2 3 4 5]
address of 0th element: 0xc00005e060
len=7 cap=11 slice=[0 1 2 3 4 5 6]
address of 0th element: 0xc00005e060
len=8 cap=11 slice=[0 1 2 3 4 5 6 7]
address of 0th element: 0xc00005e060
len=9 cap=11 slice=[0 1 2 3 4 5 6 7 8]
address of 0th element: 0xc00005e060
len=10 cap=11 slice=[0 1 2 3 4 5 6 7 8 9]
address of 0th element: 0xc00005e060
Program exited.
サイズ5の初期配列がいっぱいになったときの再割り当てに注意してください。
新しい配列が割り当てられると、capacity
と0番目の要素のアドレスの両方が変更されます。
堅牢なExtend
関数をガイドとして使用すると、スライスを複数の要素で拡張できる、さらに優れた関数を作成できます。 これを行うには、関数が呼び出されたときに関数の引数のリストをスライスに変換として扱うGoの機能、つまり、Goの可変個引数関数を利用します。
関数Append
を呼び出しましょう。 最初のバージョンでは、Extendを繰り返し呼び出すことができるため、可変個引数関数のメカニズムが明確になります。 Append
関数のシグネチャは次のとおりです。
func Append(slice []int, items ...int) []int
このシグネチャは、一つのスライスと、それに続く0個以上のint型の値を引数としてとることを示しています。
// Append appends the items to the slice.
// First version: just loop calling Extend.
func Append(slice []int, items ...int) []int {
for _, item := range items {
slice = Extend(slice, item)
}
return slice
}
注意すべきこととしては、for range
ループは[]int
として扱われている引数items
の要素を各ループで扱っています。 また_
という識別子で、今回のケースでは不要なスライスのインデックスを捨てていることにも注意が必要です。
slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = Append(slice, 5, 6, 7, 8)
fmt.Println(slice)
この例で登場する新しい手法として、スライスの型とそれに続く中括弧内の要素で構成される複合リテラルを書き込むことによってスライスを初期化することが挙げられます。
slice := []int{0, 1, 2, 3, 4}
Append
関数のさらに面白い点は、要素を追加できるだけでなく、呼び出し側で...
表記を使用してスライスを引数に"分解"することにより、2番目のスライスそのものを引数として追加できる点です。
slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // The '...' is essential!
fmt.Println(slice1)
前回のAppend
では元のスライスの2倍の長さにしていたので、追加するelements
の数では配列の再割り当てが複数回行われる可能性がありましたが、下の例のAppend
では1回の割り当てだけで住むように効率化されています。
// Append appends the elements to the slice.
// Efficient version.
func Append(slice []int, elements ...int) []int {
n := len(slice)
total := len(slice) + len(elements)
if total > cap(slice) {
// Reallocate. Grow to 1.5 times the new size, so we can still grow.
newSize := total*3/2 + 1
newSlice := make([]int, total, newSize)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[:total]
copy(slice[n:], elements)
return slice
}
新しく割り当てたメモリへスライスのデータをコピーする際と、追加するアイテムを配列の末尾にコピーする際と、2回copy
が呼び出されていることに注意してください。
組み込み関数としてのAppend
やっと組み込み関数append
の設計の理由を説明するためのピースが整いました。
組み込み関数append
は、上記のAppend
の例とまったく同じように、同等の効率で実行されますが、どのスライス型でも機能します。
Goの弱点は、ジェネリック型の操作をランタイムで提供する必要があることです。
いつか変わるかもしれませんが、今のところ、スライスの操作を簡単にするためにGoにはジェネリックなappend
関数が組み込みで用意されています。
これは、[]int
のスライスと同じように機能しますが、どのスライス型でも機能します。
スライスヘッダはappend
の呼び出しによって常に更新されるため、呼び出し後に返されたスライスを保存する必要があることに注意してください。
実際、コンパイラでは、結果を保存せずにappendを呼び出すことはできません。
以下に、append
の使用例を掲載します。実際に実行してみたり、編集したりしてみると面白いかもしれません。
// Create a couple of starter slices.
slice := []int{1, 2, 3}
slice2 := []int{55, 66, 77}
fmt.Println("Start slice: ", slice)
fmt.Println("Start slice2:", slice2)
// Add an item to a slice.
slice = append(slice, 4)
fmt.Println("Add one item:", slice)
// Add one slice to another.
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)
// Make a copy of a slice (of int).
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)
// Copy a slice to the end of itself.
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)
Start slice: [1 2 3]
Start slice2: [55 66 77]
Add one item: [1 2 3 4]
Add one slice: [1 2 3 4 55 66 77]
Copy a slice: [1 2 3 4 55 66 77]
Before append to self: [1 2 3 4 55 66 77]
After append to self: [1 2 3 4 55 66 77 1 2 3 4 55 66 77]
Program exited.
特に、上の例の最後のappend
は、スライスの設計がなぜシンプルなのかを考えるのにいい例になるでしょう
有志のコミュニティによって作られた"Slice Tricks" Wiki pageにはappend
,'copy',その他のスライスに関する様々な関数や処理の例が紹介されています。
Nil
余談ですが、今回学んだ新たな知識により、nil
スライスが実際にどう表現されているかを知ることができます。
当然、これはスライスヘッダのゼロ値です。
sliceHeader{
Length: 0,
Capacity: 0,
ZerothElement: nil,
}
または
sliceHeader{}
鍵となるのは、要素のポインタがnil
である点です。
array[0:0]
このように作られた配列は長さも容量も0ですが、ポインタはnil
ではなく、nil
スライスとして扱われません。
自明ですが、(容量がゼロ以外であると仮定すれば)空のスライスは大きくなる可能性があります
しかしnil
スライスには値を入れる配列がなく、要素を保持できるように拡張することはできません。
とはいえ、nil
スライスは、何も指していなくても、機能的には長さ0のスライスと同等ですので、append
を使えば、配列の割り当てが行われて要素を追加できます。
Strings
ここでは、スライスの視点からGoの文字列について簡単な説明をしましょう。
実際には文字列のメカニズムは非常に単純です。文字列は読み取り専用のバイト型のスライスであり、言語からの構文サポートが少し追加されています。
それらは読み取り専用であるため、容量は必要ありません(つまり拡張できない)が、それを除けばほとんどの場合、読み取り専用のバイト型のスライスのように扱うことができます。
手始めに、個々のバイトにアクセスするためにそれらにインデックスを付けることができます。
slash := "/usr/ken"[0] // yields the byte value '/'.
文字列をスライスして部分文字列を抽出することも可能です。
usr := "/usr/ken"[0:4] // yields the string "/usr"
文字列のスライス時に何が裏で起こっているかはここまで読んだみなさんには自明のことでしょう。
またバイト型のスライスを次のように変換して文字列を生成することも可能です。
str := string(slice)
その逆も可能です。
slice := []byte(usr)
文字列の基になるbyteの配列は表には現れません。つまり、文字列を介する以外にその内容にアクセスする方法はありません。
これらの変換のいずれかを実行するときは、byte配列のコピーを作成する必要があります。
もちろん、Goがこれを処理するので、ユーザーが自身でそうする必要はありません。これらの変換のいずれかの後、バイトスライスの基になる配列への変更は、対応する文字列に影響を与えません。(例えば、str := string(slice)
のあとでslice
を変更してもstr
には影響がない)
文字列をスライスのように設計したことの結果として重要な点は、部分文字列の作成が非常に効率的であることです。
必要なのは、2ワードの文字列ヘッダを作成することだけです。文字列は読み取り専用であるため、元の文字列とスライス操作の結果の部分文字列は、同じ配列を安全に共有できます。
歴史的なメモ:文字列の最も初期の実装では、部分文字列の作成の際に、新しいbyte配列が常に割り当てられていましたが、スライスが言語に追加されたとき、それらは効率的な文字列処理のモデルを提供しました。その結果、一部のベンチマークでは大幅なスピードアップが見られました。
もちろん、文字列にはさらに多くのものがあり、別のブログ投稿で文字列についてさらに詳しく説明しています。
まとめ
スライスがどのように機能するかを理解するには、スライスがどのように実装されているかを理解することが役立ちます。
スライス変数に関連付けられたアイテムであるスライスヘッダという小さなデータ構造があり、そのヘッダは個別に割り当てられた配列の一部分を参照します。
スライス値を渡すと、ヘッダはコピーされますが、それが指す配列は常に共有されます。
それらがどのように機能するかを理解すると、スライスは使いやすいだけでなく、特にcopy
とappend
の組み込み関数の助けを借りて、強力で表現力豊かなデータ構造になります。
また他のGoに関しての記事も書いていますよかったらどうぞ!