スライス
Go では、配列とスライスは外見がほぼ同じですが、機能には大きな違いがあります。配列は固定長のデータ構造で、長さを指定した後では変更できません。一方、スライスは可変長で、容量が不足すると自動的に拡張されます。
配列
保存するデータの長さが事前にわかっており、後続の使用で拡張の必要がない場合は、配列の使用を検討できます。Go の配列は参照型ではなく値型であり、先頭要素へのポインタではありません。
TIP
配列は値型であるため、配列を関数にパラメータとして渡す場合、Go の関数は値渡しであるため、配列全体がコピーされます。
初期化
配列の宣言時、長さは定数でなければなりません。変数は使用できません。変数を宣言して、その変数を配列の長さとして使用することはできません。
// 正しい例
var a [5]int
// 間違いの例
l := 1
var b [l]intまず、長さ 5 の整数配列を初期化します。
var nums [5]int要素で初期化することもできます。
nums := [5]int{1, 2, 3}コンパイラに長さを自動的に推論させることもできます。
nums := [...]int{1, 2, 3, 4, 5} // nums := [5]int{1, 2, 3, 4, 5} と同等。省略記法は必須です。否则生成的是切片,不是数组new 関数を使用してポインタを取得することもできます。
nums := new([5]int)上記のいくつかの方法はすべて nums に固定サイズのメモリを割り当てますが、最後の 1 つだけ取得される値がポインタであるという違いがあります。
配列の初期化時には、長さは定数式でなければなりません。否则将无法通过编译。定数式とは、式の結果が定数であることを意味します。誤った例を以下に示します。
length := 5 // これは変数です
var nums [length]intlength は変数であるため、配列の長さの初期化には使用できません。以下は正しい例です。
const length = 5
var nums [length]int // 定数
var nums2 [length + 1]int // 定数式
var nums3 [(1 + 2 + 3) * 5]int // 定数式
var nums4 [5]int // 最も一般的に使用される使用
配列名とインデックスがあれば、配列内の対応する要素にアクセスできます。
fmt.Println(nums[0])同様に、配列要素を変更することもできます。
nums[0] = 1組み込み関数 len を使用して、配列要素の数をアクセスすることもできます。
len(nums)組み込み関数 cap を使用して、配列容量にアクセスします。配列の容量は配列の長さに等しく、容量はスライスにとって意味があります。
cap(nums)スライシング
配列のスライシングの形式は arr[startIndex:endIndex] で、スライシングする区間は左閉右開です。配列はスライシング後、スライス型になります。例を以下に示します。
nums := [5]int{1, 2, 3, 4, 5}
nums[:] // 子スライス範囲 [0,5) -> [1 2 3 4 5]
nums[1:] // 子スライス範囲 [1,5) -> [2 3 4 5]
nums[:5] // 子スライス範囲 [0,5) -> [1 2 3 4 5]
nums[2:3] // 子スライス範囲 [2,3) -> [3]
nums[1:3] // 子スライス範囲 [1,3) -> [2 3]func main() {
arr := [5]int{1, 2, 3, 4, 5}
fmt.Printf("%T\n", arr)
fmt.Printf("%T\n", arr[1:2])
}出力
[5]int
[]int配列をスライスタイプに変換するには、パラメータなしでスライシングします。変換後のスライスは元の配列と同じメモリを指しており、スライスを変更すると元の配列のコンテンツが変化します。
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:]
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)
}出力
array: [0 2 3 4 5]
slice: [0 2 3 4 5]変換後のスライスを変更する場合は、以下の方法で変換することを推奨します。
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := slices.Clone(arr[:])
slice[0] = 0
fmt.Printf("array: %v\n", arr)
fmt.Printf("slice: %v\n", slice)
}出力
array: [1 2 3 4 5]
slice: [0 2 3 4 5]スライス
スライスは Go での応用範囲が配列よりもはるかに広く、長さのわからないデータを保存するために使用され、後続の使用中に要素の挿入と削除が頻繁に行われる可能性があります。
初期化
スライスの初期化方法には以下のいくつかがあります。
var nums []int // 値
nums := []int{1, 2, 3} // 値
nums := make([]int, 0, 0) // 値
nums := new([]int) // ポインタスライスと配列の外見上の違いは、初期化長さがないことだけです。通常、空のスライスを作成するには make を使用することを推奨します。スライスの場合、make 関数は 3 つのパラメータを受け取ります:型、長さ、容量。長さと容量の違いを説明する例を挙げると、水桶があるとします。水は満杯ではなく、桶の高さが桶の容量で、合計でどれだけの高さの水を装えるかを示します。桶内の水の高さが長さを表します。水の高さは必ず桶の高さ以下でなければなりません。否则水就会溢出。したがって、スライスの長さはスライス内の要素の数を表し、スライスの容量はスライスが装える要素の総数を表します。スライスと配列の最大の違いは、スライスの容量は自動的に拡張されますが、配列は拡張されないことです。詳細は リファレンスマニュアル - 長さと容量 をご覧ください。
TIP
スライスの底辺実装は配列であり、参照型です。底辺配列へのポインタと簡単に理解できます(本質的にスライスは Go において構造体であり、底辺配列へのポインタ、長さ値、容量値を含みます)。したがって、スライスが関数パラメータとして渡される場合、底辺配列はコピーされず、関数内での渡されたスライスへの変更は元のスライスに反映されます。
var nums []int の方法で宣言されたスライスのデフォルト値は nil であるため、メモリは割り当てられません。make を使用して初期化する際は、十分な容量を事前に割り当てることを推奨します。これにより、後続の拡張によるメモリの消耗を効果的に削減できます。
使用
スライスの基本的な使用法は配列と完全に同じですが、スライスの長さを動的に変更できるという違いがあります。以下にいくつかの例を示します。
スライスは append 関数を使用して多くの操作を実行できます。関数シグネチャは以下の通りです。slice は要素を追加するターゲットスライスで、elems は追加する要素です。戻り値は追加後のスライスです。
func append(slice []Type, elems ...Type) []Typeまず、長さ 0、容量 0 の空スライスを作成し、次に末尾にいくつかの要素を挿入し、最後に長さと容量を出力します。
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 長さと容量が一致していないことがわかります。新しいスライスに予約されたバッファ容量のサイズには一定の規則があります。 golang1.18 バージョンの更新之前、网上大多数の文章はスライスの拡張戦略をこのように説明していました: 元のスライスの容量が 1024 未満の場合、新しいスライスの容量は元の 2 倍になります。元のスライスの容量が 1024 を超えると、新しいスライスの容量は元の 1.25 倍になります。 1.18 バージョンの更新後、スライスの拡張戦略は以下のようになりました: 元のスライスの容量 (oldcap) が 256 未満の場合、新しいスライス (newcap) の容量は元の 2 倍になります。元のスライスの容量が 256 を超えると、新しいスライスの容量 newcap = oldcap+(oldcap+3*256)/4 になります。
要素の挿入
スライス要素の挿入も append 関数と組み合わせて使用する必要があります。以下のスライスがあるとします。
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}先頭から要素を挿入します。
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]中間インデックス i から要素を挿入します。
nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3, [1 2 3 4 999 999 5 6 7 8 9 10]末尾から要素を挿入します。これは append の最も原始的な使用方法です。
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]要素の削除
スライス要素の削除も append 関数と組み合わせて使用する必要があります。以下のスライスがあるとします。
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}先頭から n 個の要素を削除します。
nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]末尾から n 個の要素を削除します。
nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]中間の指定されたインデックス i の位置から n 個の要素を削除します。
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2, n=3, [1 2 6 7 8 9 10]すべての要素を削除します。
nums = nums[:0]
fmt.Println(nums) // []コピー
スライスのコピー時には、ターゲットスライスに十分な長さがあることを確認する必要があります。
func main() {
dest := make([]int, 0)
src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(src, dest)
fmt.Println(copy(dest, src))
fmt.Println(src, dest)
}[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []長さを 10 に変更すると、出力は以下のようになります。
[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]トラバース
スライスのトラバースは配列と完全に同じです。for ループ
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for i := 0; i < len(slice); i++ {
fmt.Println(slice[i])
}
}for range ループ
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for index, val := range slice {
fmt.Println(index, val)
}
}多次元スライス
以下の例を見てください。公式ドキュメントにも説明があります:Effective Go - 二次元スライス
var nums [5][5]int
for _, num := range nums {
fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
fmt.Println(slice)
}出力結果は
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[]
[]
[]
[]
[]同じ二次元配列とスライスでも、内部構造が異なることがわかります。配列は初期化時に、一次元と二次元の長さがすでに固定されています。一方、スライスの長さは固定されておらず、スライス内の各スライスの長さは異なる可能性があります。したがって、個別に初期化する必要があります。スライスの初期化部分を以下のコードに変更するだけで済みます。
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
slices[i] = make([]int, 5)
}最終的な出力結果は
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]拡張式
TIP
拡張式はスライスのみで使用できます
スライスと配列はどちらも単純な式を使用してスライシングできますが、拡張式はスライスのみで使用できます。この機能は Go1.2 バージョンで追加され、主にスライスが底辺配列の読み書きを共有する問題を解決するためのものです。主な形式は以下の通りで、low<= high <= max <= cap の関係を満たす必要があります。拡張式でスライシングされたスライスの容量は max-low です。
slice[low:high:max]low と high は元の意味のままで、追加された max は最大容量を指します。例えば、以下の例では max が省略されているため、s2 の容量は cap(s1)-low になります。
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6これには明らかな問題があります。s1 と s2 は同じ底辺配列を共有しており、s2 の読み書き時に s1 のデータに影響を与える可能性があります。以下のコードはそのような状況に属します。
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6
s2 = append(s2, 1) // 新しい要素を追加。容量は 6 のため、拡張されず、底辺配列を直接変更
fmt.Println(s2)
fmt.Println(s1)最終的な出力は
[4 1]
[1 2 3 4 1 6 7 8 9]s2 に要素を追加しているだけなのに、s1 も一緒に変更されていることがわかります。拡張式はこの種の問題を解決するために生まれました。少し変更するだけで解決できます。
func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4:4] // cap = 4 - 3 = 1
s2 = append(s2, 1) // 容量不足のため、新しい底辺配列を割り当て
fmt.Println(s2)
fmt.Println(s1)
}これで正常な結果が得られます。
[4 1]
[1 2 3 4 5 6 7 8 9]clear
Go1.21 で clear 組み込み関数が追加されました。clear はスライス内のすべての値をゼロ値に設定します。
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4}
clear(s)
fmt.Println(s)
}出力
[0 0 0 0]スライスを空にしたい場合は、以下のようにできます。
func main() {
s := []int{1, 2, 3, 4}
s = s[:0:0]
fmt.Println(s)
}スライシング後の容量を制限することで、元のスライスの後続要素の上書きを回避できます。
