Goブログ
配列、スライス(および文字列):「append」のメカニズム
はじめに
手続き型プログラミング言語の最も一般的な機能の1つは、配列の概念です。配列は単純なもののように見えますが、言語に追加するときには、次のような多くの質問に答える必要があります。
- 固定サイズか可変サイズか?
- サイズは型の一部ですか?
- 多次元配列はどのように見えるか?
- 空の配列は意味を持ちますか?
これらの質問への回答は、配列が言語の単なる機能なのか、設計の中核部分なのかに影響します。
Goの初期開発では、設計が適切だと感じるまでにこれらの質問への回答を決定するのに約1年かかりました。重要なステップは、柔軟で拡張可能なデータ構造を提供するために、固定サイズの *配列* を基盤として構築された *スライス* の導入でした。しかし、今日でも、Goを初めて使用するプログラマーは、他の言語での経験が考え方に影響を与えているためか、スライスの動作方法につまずくことがよくあります。
この記事では、混乱を解消しようと試みます。これを行うために、 `append` 組み込み関数がどのように機能するのか、そしてなぜそのような動作をするのかを説明するために、要素を構築していきます。
配列
配列はGoの重要な構成要素ですが、建物の基礎のように、より目立つコンポーネントの下に隠れていることがよくあります。スライスのより興味深く、強力で、重要な概念に進む前に、配列について簡単に説明する必要があります。
配列のサイズは型の一部であり、表現力が制限されるため、Goプログラムでは配列はあまり見られません。
宣言
var buffer [256]byte
256バイトを保持する変数 `buffer` を宣言します。 `buffer` の型には、そのサイズである `[256]byte` が含まれます。 512バイトの配列は、明確な型 `[512]byte` になります。
配列に関連付けられたデータはまさにそれです。要素の配列です。模式的に、メモリ内ではバッファは次のようになります。
buffer: byte byte byte ... 256 times ... byte byte byte
つまり、変数には256バイトのデータのみが保持されます。使い慣れたインデックス構文 `buffer[0]`、 `buffer[1]` などを介して `buffer[255]` まで、その要素にアクセスできます。(インデックス範囲0〜255は256個の要素をカバーします。)この範囲外の値で `buffer` のインデックスを作成しようとすると、プログラムがクラッシュします。
配列またはスライス、およびその他のいくつかのデータ型の要素数を返す `len` という組み込み関数があります。配列の場合、 `len` が返すものは明らかです。私たちの例では、 `len(buffer)` は固定値256を返します。
配列には適切な場所があります。たとえば、変換行列の適切な表現ですが、Goでの最も一般的な目的は、スライスのストレージを保持することです。
スライス:スライスヘッダー
スライスは動作の要ですが、それらをうまく使用するには、スライスが正確に何であるか、何を行うかを理解する必要があります。
スライスは、スライス変数自体とは別に格納された配列の連続したセクションを記述するデータ構造です。 *スライスは配列ではありません*。 スライスは配列の一部を *記述* します。
前のセクションの `buffer` 配列変数が与えられた場合、配列を *スライス* することで、要素100〜150(正確には100〜149、両端を含む)を記述するスライスを作成できます。
var slice []byte = buffer[100:150]
そのスニペットでは、明示的に示すために完全な変数宣言を使用しました。変数 `slice` は `[]byte` 型(「バイトのスライス」と発音)を持ち、配列 `buffer` から要素100(含む)から150(含まない)をスライスして初期化されます。よりイディオム的な構文では、初期化式によって設定される型を削除します。
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(両端を含む)で、元の配列の要素105〜109を意味します。 `slice2` 変数の基礎となる `sliceHeader` 構造体は次のようになります。
slice2 := sliceHeader{
Length: 5,
ZerothElement: &buffer[105],
}
このヘッダーが、 `buffer` 変数に格納されている同じ基になる配列を指していることに注意してください。
また、 *リ スライス* も可能です。つまり、スライスをスライスして、結果を元のスライス構造体に保存します。後
slice = slice[5:10]
`slice` 変数の `sliceHeader` 構造体は、 `slice2` 変数の場合と同じように見えます。たとえば、スライスを切り詰める場合など、リ スライスがよく使用されていることがわかります。このステートメントは、スライスの最初と最後の要素をドロップします。
slice = slice[1:len(slice)-1]
[練習:この割り当ての後、 `sliceHeader` 構造がどのように見えるかを書き出します。]
経験豊富なGoプログラマーが「スライスヘッダー」について話しているのをよく聞くでしょう。なぜなら、それがまさにスライス変数に格納されているものだからです。たとえば、 bytes.IndexRune のように、引数としてスライスを受け取る関数を呼び出すと、そのヘッダーが関数に渡されます。この呼び出しでは、
slashPos := bytes.IndexRune(slice, '/')
`IndexRune` 関数に渡される `slice` 引数は、実際には「スライスヘッダー」です。
スライスヘッダーにはもう1つのデータ項目がありますが、これについては後で説明します。最初に、スライスヘッダーの存在が、スライスでプログラミングするときに何を意味するかを見てみましょう。
関数へのスライスの引き渡し
スライスにはポインターが含まれていても、それ自体が値であることを理解することが重要です。内部的には、ポインターと長さを保持する構造体の値です。構造体へのポインターでは *ありません*。
これは重要です。
前の例で `IndexRune` を呼び出したとき、スライスヘッダーの *コピー* が渡されました。その動作には重要な影響があります。
この単純な関数を考えてみましょう。
func AddOneToEachElement(slice []byte) { for i := range slice { slice[i]++ } }
名前が示すように、スライスのインデックスを反復処理し( `for` `range` ループを使用)、その要素をインクリメントします。
試してみてください。
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) }
(探索したい場合は、これらの実行可能なスニペットを編集して再実行できます。)
スライスの *ヘッダー* は値で渡されますが、ヘッダーには配列の要素へのポインターが含まれているため、元のスライスヘッダーと関数に渡されるヘッダーのコピーは、同じ配列を記述します。したがって、関数が戻るとき、変更された要素は元のスライス変数を通して見ることができます。
この例が示すように、関数の引数は実際にはコピーです。
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)) }
ここで、関数の引数の *内容* は関数によって変更できますが、その *ヘッダー* は変更できないことがわかります。 `slice` 変数に格納されている長さは、関数に渡されるのはスライスヘッダーのコピーであり、オリジナルではないため、関数の呼び出しによって変更されません。したがって、ヘッダーを変更する関数を記述する場合は、ここで行ったように、結果パラメーターとして返す必要があります。 `slice` 変数は変更されませんが、返された値には新しい長さがあり、それは `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)) }
特に余分な間接参照のレベルを処理すると(一時変数が役立ちます)、この例では不器用に見えますが、スライスへのポインターが表示される一般的なケースが1つあります。スライスを変更するメソッドには、ポインターレシーバーを使用するのがイディオムです。
最後にスラッシュで切り詰めるメソッドをスライスに持たせたいとしましょう。次のように記述できます。
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文字を大文字にする(英語以外の名前を無視する)メソッドを `path` に対して書きたい場合、値レシーバーは同じ基になる配列を指すため、メソッドは値にすることができます。
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
メソッドが for
range
構文でインデックスとスライス要素をキャプチャするために2つの変数を使用しています。この形式のループは、本体で p[i]
を複数回記述することを避けます。
[練習: ToUpper
メソッドをポインタレシーバーを使用するように変換し、その動作が変わるかどうかを確認してください。]
[高度な練習: ToUpper
メソッドを、ASCIIだけでなくUnicode文字も処理するように変換してください。]
キャパシティ
引数の int
スライスを1要素拡張する以下の関数を見てください。
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) } }
スライスがどのように成長するか見てください...成長しなくなるまで。
スライスヘッダーの3番目のコンポーネント、つまりキャパシティについて説明する時が来ました。スライスヘッダーは、配列ポインタと長さの他に、キャパシティも格納しています。
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
フィールドは、基になる配列の長さから、スライスの最初の要素の配列内のインデックスを引いたものと等しくなります(この場合はゼロ)。スライスのキャパシティを調べたい場合は、組み込み関数 cap
を使用します。
if cap(slice) == len(slice) {
fmt.Println("slice is full!")
}
Make
スライスをキャパシティを超えて成長させたい場合はどうすればよいでしょうか?できません!定義上、キャパシティは成長の限界です。しかし、新しい配列を割り当て、データをコピーし、新しい配列を記述するようにスライスを変更することで、同等の結果を得ることができます。
まず、割り当てから始めましょう。組み込み関数 new
を使用して、より大きな配列を割り当ててから結果をスライスすることもできますが、代わりに組み込み関数 make
を使用する方が簡単です。これは、新しい配列を割り当て、それを記述するスライスヘッダーを一度に作成します。make
関数は、スライスの型、初期長、およびキャパシティの3つの引数を取ります。キャパシティは、make
がスライスデータを保持するために割り当てる配列の長さです。この呼び出しは、長さが10で、さらに5つの余裕がある(15-10)スライスを作成します。実行するとわかります。
slice := make([]int, 10, 15) fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
このスニペットは、int
スライスのキャパシティを2倍にしますが、長さは同じに保ちます。
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))
このコードを実行した後、スライスは別の再割り当てが必要になるまでに、はるかに多くの成長の余地があります。
スライスを作成する場合、長さとキャパシティが同じになることがよくあります。組み込みの make
には、この一般的なケースの短縮形があります。長さ引数はデフォルトでキャパシティになるため、両方を同じ値に設定する場合は省略できます。後で
gophers := make([]Gopher, 10)
gophers
スライスは、長さとキャパシティの両方が10に設定されます。
コピー
前のセクションでスライスのキャパシティを2倍にしたとき、古いデータを新しいスライスにコピーするためにループを記述しました。Goには、これを簡単にするための組み込み関数 copy
があります。その引数は2つのスライスであり、右側の引数のデータを左側の引数にコピーします。以下は、copy
を使用するように書き換えられた例です。
newSlice := make([]int, len(slice), 2*cap(slice)) copy(newSlice, slice)
copy
関数は賢いです。両方の引数の長さに注意して、コピーできるものだけをコピーします。言い換えれば、コピーする要素の数は、2つのスライスの長さの最小値です。これにより、少し管理が楽になります。また、copy
はコピーした要素の数である整数値を返しますが、常に確認する価値があるとは限りません。
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 }
この関数には、いくつかの注意点があります。まず、もちろん、長さが変更されているため、更新されたスライスを返す必要があります。次に、便利な短縮形を使用しています。式
slice[i:]
は、次のものとまったく同じ意味です。
slice[i:len(slice)]
また、まだトリックを使用していませんが、スライス式の最初の要素も省略できます。デフォルトではゼロになります。したがって、
slice[:]
は、スライス自体を意味するだけで、配列をスライスする場合に便利です。この式は、「配列のすべての要素を記述するスライス」と言うための最短の方法です。
array[:]
さて、その点を踏まえて、Insert
関数を実行してみましょう。
slice := make([]int, 10, 20) // Note capacity > length: room to add element. for i := range slice { slice[i] = i } fmt.Println(slice) slice = Insert(slice, 5, 99) fmt.Println(slice)
Append: 例
いくつかのセクション前に、スライスを1要素拡張する Extend
関数を記述しました。ただし、スライスのキャパシティが小さすぎると関数がクラッシュするため、バグがありました。(Insert
の例にも同じ問題があります。)さて、それを修正する準備ができたので、整数のスライス用の堅牢な Extend
の実装を記述しましょう。
func Extend(slice []int, element int) []int { n := len(slice) if n == cap(slice) { // Slice is full; must grow. // We double its size and add 1, so if the size is zero we still grow. newSlice := make([]int, len(slice), 2*len(slice)+1) copy(newSlice, slice) slice = newSlice } slice = slice[0 : n+1] slice[n] = element return slice }
この場合、特にスライスを返すことが重要です。なぜなら、再割り当てされると、結果のスライスはまったく異なる配列を記述するからです。以下は、スライスがいっぱいになると何が起こるかを示す小さなスニペットです。
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]) }
サイズ5の初期配列がいっぱいになるときの再割り当てに注目してください。新しい配列が割り当てられると、キャパシティとゼロ番目の要素のアドレスの両方が変わります。
堅牢な Extend
関数をガイドとして、複数の要素でスライスを拡張できるさらに優れた関数を記述できます。これを行うために、Goの関数の引数のリストを関数が呼び出されたときにスライスに変換する機能を使用します。つまり、Goの可変長引数関数機能を使用します。
関数を Append
と呼びましょう。最初のバージョンでは、可変長引数関数のメカニズムが明確になるように、Extend
を繰り返し呼び出すことができます。Append
のシグネチャは次のとおりです。
func Append(slice []int, items ...int) []int
これは、Append
が1つの引数(スライス)と、それに続くゼロ個以上の int
引数を取ることを示しています。これらの引数は、Append
の実装に関する限り、正確には 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 }
items
引数の要素を反復処理する for
range
ループに注目してください。items
引数は暗黙的に []int
型を持っています。また、この場合不要なため、ループのインデックスを破棄するためにブランク識別子 _
を使用していることにも注目してください。
試してみてください。
slice := []int{0, 1, 2, 3, 4} fmt.Println(slice) slice = Append(slice, 5, 6, 7, 8) fmt.Println(slice)
この例のもう1つの新しいテクニックは、複合リテラルを記述してスライスを初期化することです。複合リテラルは、スライスの型とそれに続く波括弧内の要素で構成されます。
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)
もちろん、Extend
の内部構造に基づいて、一度しか割り当てないことで、Append
をより効率的にすることができます。
// 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 }
ここで、copy
を2回使用していることに注目してください。1回はスライスデータを新しく割り当てられたメモリに移動するため、もう1回は追加するアイテムを古いデータの末尾にコピーするためです。
試してみてください。動作は以前と同じです。
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: 組み込み関数
こうして、組み込み関数 append
の設計の動機に至ります。これは、例の Append
とまったく同じことを、同等の効率で行いますが、任意のスライス型に対して機能します。
Goの弱点は、ジェネリック型の操作はすべてランタイムによって提供されなければならないことです。いつか変更されるかもしれませんが、今のところ、スライスの操作を簡単にするために、Goは組み込みのジェネリック append
関数を提供しています。これは、int
スライスバージョンと同じように動作しますが、任意のスライス型に対して機能します。
スライスヘッダーは常に append
の呼び出しによって更新されるため、呼び出し後に返されたスライスを保存する必要があることを覚えておいてください。実際、コンパイラは結果を保存せずにappendを呼び出すことを許可しません。
以下は、プリントステートメントが混在するいくつかの1行コードです。試して、編集して、調べてみてください。
// 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)
スライスの設計により、この単純な呼び出しがどのように正しく機能するのかを理解するために、例の最後の1行コードについて詳しく考えてみてください。
コミュニティが作成した「Slice Tricks」Wikiページには、append
、copy
、およびスライスのその他の使用方法に関する例が多数あります。
Nil
余談ですが、新たに得た知識により、nil
スライスの表現が何であるかを確認できます。当然、それはスライスヘッダーのゼロ値です。
sliceHeader{
Length: 0,
Capacity: 0,
ZerothElement: nil,
}
または単に
sliceHeader{}
重要な詳細は、要素ポインタも nil
であることです。次のものによって作成されたスライス
array[0:0]
は長さがゼロ(場合によってはキャパシティもゼロ)ですが、ポインタは nil
ではないため、nilスライスではありません。
明確にする必要がありますが、空のスライスは(ゼロ以外のキャパシティがあると仮定して)成長できますが、nil
スライスには値を格納する配列がなく、1つの要素を保持するように成長することはありません。
とはいえ、nil
スライスは、何も指していないにもかかわらず、機能的にはゼロ長のストライスと同等です。長さはゼロであり、割り当てによって追加できます。例として、nil
スライスに追加してスライスをコピーする上記の1行コードを見てください。
文字列
ここで、スライスのコンテキストにおけるGoの文字列について簡単に説明します。
文字列は実際には非常に単純です。これらは、言語からのいくつかの追加の構文サポートを備えたバイトの読み取り専用スライスにすぎません。
読み取り専用であるため、キャパシティは必要ありません(成長させることができません)が、それ以外のほとんどの場合、バイトの読み取り専用スライスのように扱うことができます。
まず、個々のバイトにアクセスするためにインデックスを作成できます。
slash := "/usr/ken"[0] // yields the byte value '/'.
文字列をスライスして、サブ文字列を取得できます。
usr := "/usr/ken"[0:4] // yields the string "/usr"
文字列をスライスするときに舞台裏で何が起こっているかは、もう明らかでしょう。
また、通常のバイトのスライスを取得し、単純な変換を使用して文字列を作成することもできます。
str := string(slice)
逆方向にも進むことができます。
slice := []byte(usr)
文字列の基となる配列は隠蔽されており、文字列を通してしかその内容にアクセスする方法はありません。つまり、これらの変換のどちらを行う場合でも、配列のコピーを作成する必要があります。もちろんGoはこれを処理してくれるので、ユーザーが気にする必要はありません。これらの変換のいずれかの後、バイトスライスの基となる配列への変更は、対応する文字列には影響しません。
文字列のこのスライスのような設計の重要な結果は、部分文字列の作成が非常に効率的であるということです。必要なのは、2つのワードからなる文字列ヘッダーの作成だけです。文字列は読み取り専用であるため、元の文字列とスライス操作の結果である文字列は、同じ配列を安全に共有できます。
歴史的な注釈:文字列の初期の実装では常にメモリ割り当てを行っていましたが、スライスが言語に追加されたとき、それらは効率的な文字列処理のモデルを提供しました。結果として、いくつかのベンチマークで大幅なスピードアップが見られました。
もちろん、文字列には他にも多くのことがありますが、それらについては別のブログ記事でより詳しく説明しています。
結論
スライスがどのように機能するかを理解するには、それらがどのように実装されているかを理解することが役立ちます。スライス変数に関連付けられているのは、スライスヘッダーと呼ばれる小さなデータ構造であり、そのヘッダーは別途割り当てられた配列のセクションを表します。スライス値を渡すとき、ヘッダーはコピーされますが、それが指す配列は常に共有されます。
それらがどのように機能するかを理解すれば、スライスは使いやすいだけでなく、特に組み込み関数copy
とappend
の助けを借りて、強力で表現力豊かになります。
さらに読む
Goのスライスについては、インターネット上で多くの情報が見つかります。前述したように、「Slice Tricks」Wikiページには多くの例があります。Go Slicesのブログ記事では、メモリレイアウトの詳細を分かりやすい図で説明しています。Russ CoxのGo Data Structuresの記事には、Goの他の内部データ構造とともにスライスに関する議論が含まれています。
他にも多くの資料がありますが、スライスについて学ぶ最良の方法は、それらを使用することです。
次の記事:Goの文字列、バイト、ルーン、文字
前の記事:最初のGoプログラム
ブログインデックス