The Gopher character is based on the Go mascot designed by Renée French .
Gopherのイラストはegonelbre/gophers を利用しました。
はじめに 本記事はGo1.21連載 の記事です。
こんにちは。TIG/EXユニット所属宮永です。
先日、第1回日本遺産検定 に晴れて合格し、日本遺産ソムリエになりました。夏の旅行先をまだお決めでない方は、日本遺産公式サイト を参考にして、日本各地の魅力的なスポットを訪れてみてはいかがでしょうか。
日本遺産については公式サイト の説明が非常にわかりやすいのでご覧になってください。
さて、記事のメインテーマとは異なる事柄を挟みましたが、ここからはGo1.21に追加されたslicesパッケージの解説を行います。
Go1.21のアップデート内容に関しては、すでに多くの解説記事が公開されています。
今回注目するslicesパッケージは、Go1.21以前にはx/exp/slicesとして運用されていたものが正式に取り込まれ、多数の関数が追加されたものです。この記事では、それを「slicesパッケージのチートシート」としてまとめ、解説します。
記事の構成 本記事ではGo1.21で追加されたslicesパッケージを以下5つのカテゴリに分類し、それぞれ使い方などを解説します。また、末尾にはソートされたスライス用のsortedslicesパッケージ を渋川さん(@shibu_jp )が作成されているので簡単に紹介しています。
slicesパッケージ機能紹介 関数の説明とサンプルコードを紹介します(公式のサンプルが存在する場合、原則そのまま引用しています。ない場合は適宜補完しています)
また、感想も適宜コメントしています。
本家のGo Docsはこちら
検索関連
BinarySearch/BinarySearchFunc
説明
スライスの中に指定した要素が存在するか二分探索します
BinarySearhcを適用するスライスはソートされている必要があります
戻値として要素のインデックスと存在可否を示すboolが返却されます
スライスに要素が含まれた場合はboolがtrueとなり、要素のインデックスを返却します
スライスに要素が含まれない場合、boolはfalseとなり、要素が存在するべきインデックスを返却します
コメント
要素が存在しない場合に挿入すべき箇所が即座にわかるという点で太字の機能は意外と便利な機能だと思います
適用するスライスがソートされているという前提が注意するべき点ですね
▼BinarySearchの使用例
package mainimport ( "fmt" "slices" ) func main () { names := []string {"Alice" , "Bob" , "Vera" } n, found := slices.BinarySearch(names, "Vera" ) fmt.Println("Vera:" , n, found) n, found = slices.BinarySearch(names, "Bill" ) fmt.Println("Bill:" , n, found) }
Output: Vera: 2 true Bill: 1 false
▼BinarySearchFuncの使用例
package mainimport ( "cmp" "fmt" "slices" ) func main () { type Person struct { Name string Age int } people := []Person{ {"Alice" , 55 }, {"Bob" , 24 }, {"Gopher" , 13 }, } n, found := slices.BinarySearchFunc(people, Person{"Bob" , 0 }, func (a, b Person) int { return cmp.Compare(a.Name, b.Name) }) fmt.Println("Bob:" , n, found) }
Contains/ContainsFunc
説明
スライスの中に指定した要素が存在するか探索します
戻値として存在可否を示すboolが返却されます
ContainsFuncでは指定した関数を満たす要素が存在する場合trueを返却します
コメント
個人的にはslicesパッケージの中で一番利用している関数です
内部的には次に説明するIndexを呼び出しています
▼Containsの使用例
package mainimport ( "fmt" "slices" ) func main () { numbers := []int {0 , 42 , -10 , 8 } hasNegative := slices.Contains(numbers, -10 ) fmt.Println("Has a negative:" , hasNegative) }
Output: Has a negative: true
▼ContainsFuncの使用例
package mainimport ( "fmt" "slices" ) func main () { numbers := []int {0 , 42 , -10 , 8 } hasNegative := slices.ContainsFunc(numbers, func (n int ) bool { return n < 0 }) fmt.Println("Has a negative:" , hasNegative) hasOdd := slices.ContainsFunc(numbers, func (n int ) bool { return n%2 != 0 }) fmt.Println("Has an odd number:" , hasOdd) }
Output: Has a negative: true Has an odd number: false
Index/IndexFunc
説明
スライスの中に指定した要素が存在する場合インデックスを返却します
存在しない場合は-1を返却します
コメント
こちらもContains同様シンプルで使い勝手が良さそうですね
▼Indexの使用例
package mainimport ( "fmt" "slices" ) func main () { numbers := []int {0 , 42 , 8 } fmt.Println(slices.Index(numbers, 8 )) fmt.Println(slices.Index(numbers, 7 )) }
▼IndexFuncの使用例
package mainimport ( "fmt" "slices" ) func main () { numbers := []int {0 , 42 , -10 , 8 } i := slices.IndexFunc(numbers, func (n int ) bool { return n < 0 }) fmt.Println("First negative at index" , i) }
Output: First negative at index 2
操作関連
Clip
▼Clipの使用例
package mainimport ( "fmt" "slices" ) func main () { s := make ([]int , 0 , 2 ) fmt.Printf("slice: %v, cap:%v , len: %v \n" , s, cap (s), len (s)) s = append (s, 0 ) fmt.Printf("slice: %v, cap:%v , len: %v \n" , s, cap (s), len (s)) s = slices.Clip(s) fmt.Printf("slice: %v, cap:%v , len: %v \n" , s, cap (s), len (s)) }
Output: slice: [], cap:2 , len: 0 slice: [0], cap:2 , len: 1 slice: [0], cap:1 , len: 1
Clone
説明
スライスのコピーを作成します
コピーはShallow Copyであることに注意が必要です
コメント
Slice Tricks ではCopyとして説明しているのに関数名をCloneとしたのはなにか理由があるのだろうか…
↑を調べたたところ、comment#45038 に以下のコメントがされていました
Copy is no good because this is different from the builtin copy.
Copyという名称はすでにビルトインのcopyで利用されているため避けたいということみたいですね。
▼Cloneの使用例
package mainimport ( "fmt" "slices" ) func main () { s := []int {1 , 2 , 3 , 4 , 5 } c := slices.Clone(s) fmt.Printf("s: %v \n" , s) fmt.Printf("c : %v \n" , c) }
Output: s: [1 2 3 4 5] c : [1 2 3 4 5]
Delete/DeleteFunc
説明
指定したiからjまでを削除したスライスを返します
もとのスライスは書き換えられるため注意が必要です
コメント
▼Deleteの使用例
package mainimport ( "fmt" "slices" ) func main () { s := []int {1 , 2 , 3 , 4 , 5 } d := slices.Delete(s, 1 , 3 ) fmt.Printf("s: %v \n" , s) fmt.Printf("d : %v \n" , d) }
Output: s: [1 4 5 4 5] d : [1 4 5]
▼DeleteFuncの使用例
package mainimport ( "fmt" "slices" ) func main () { seq := []int {0 , 1 , 1 , 2 , 3 , 5 , 8 } seq = slices.DeleteFunc(seq, func (n int ) bool { return n%2 != 0 }) fmt.Println(seq) }
Compact/CompactFunc
説明
連続する同じ要素を持つ値を1つの要素に置き換えます
重複排除の目的で利用する場合は適用前にソートする必要があります
コメント
こちらも頻繁に利用するケースがありそうな関数です
CompactFuncの利用方法は少しクセがあります
▼Compactの使用例
package mainimport ( "fmt" "slices" ) func main () { seq := []int {0 , 1 , 1 , 2 , 3 , 5 , 8 } seq = slices.Compact(seq) fmt.Println(seq) }
▼CompactFuncの使用例
package mainimport ( "fmt" "slices" "strings" ) func main () { names := []string {"bob" , "Bob" , "alice" , "Vera" , "VERA" } names = slices.CompactFunc(names, func (a, b string ) bool { return strings.ToLower(a) == strings.ToLower(b) }) fmt.Println(names) }
Grow
▼Growの使用例
package mainimport ( "fmt" "slices" ) func main () { s := make ([]int , 0 , 2 ) fmt.Printf("slice: %v, cap:%v , len: %v \n" , s, cap (s), len (s)) s = append (s, 0 ) fmt.Printf("slice: %v, cap:%v , len: %v \n" , s, cap (s), len (s)) s = slices.Grow(s, 10 ) fmt.Printf("slice: %v, cap:%v , len: %v \n" , s, cap (s), len (s)) }
Output: slice: [], cap:2 , len: 0 slice: [0], cap:2 , len: 1 slice: [0], cap:12 , len: 1
Insert
説明
指定した箇所に要素を挿入します
挿入にはスライスも許容します
▼Insertの使用例
package mainimport ( "fmt" "slices" ) func main () { names := []string {"Alice" , "Bob" , "Vera" } names = slices.Insert(names, 1 , "Bill" , "Billie" ) names = slices.Insert(names, len (names), "Zac" ) fmt.Println(names) }
Output: [Alice Bill Billie Bob Vera Zac]
Replace
説明
あるスライスの指定したiからjまでを指定したスライスで置き換えます
▼Replaceの使用例
package mainimport ( "fmt" "slices" ) func main () { names := []string {"Alice" , "Bob" , "Vera" , "Zac" } names = slices.Replace(names, 1 , 3 , "Bill" , "Billie" , "Cat" ) fmt.Println(names) }
Output: [Alice Bill Billie Cat Zac]
Reverse
▼Reverseの使用例
package mainimport ( "fmt" "slices" ) func main () { names := []string {"alice" , "Bob" , "VERA" } slices.Reverse(names) fmt.Println(names) }
ソート関連
IsSorted/IsSortedFunc
説明
スライスが昇順にソートされている場合trueを返却します
▼IsSortedの使用例
package mainimport ( "fmt" "slices" ) func main () { fmt.Println(slices.IsSorted([]string {"Alice" , "Bob" , "Vera" })) fmt.Println(slices.IsSorted([]int {0 , 2 , 1 })) }
▼IsSortedFuncの使用例
package mainimport ( "cmp" "fmt" "slices" "strings" ) func main () { names := []string {"alice" , "Bob" , "VERA" } isSortedInsensitive := slices.IsSortedFunc(names, func (a, b string ) int { return cmp.Compare(strings.ToLower(a), strings.ToLower(b)) }) fmt.Println(isSortedInsensitive) fmt.Println(slices.IsSorted(names)) }
Sort/SortFunc/SortStableFunc
▼Sortの使用例
package mainimport ( "fmt" "slices" ) func main () { smallInts := []int8 {0 , 42 , -10 , 8 } slices.Sort(smallInts) fmt.Println(smallInts) }
▼SortFuncの使用例
import ( "cmp" "fmt" "slices" ) func main () { type Person struct { Name string Age int } people := []Person{ {"Gopher" , 13 }, {"Alice" , 55 }, {"Bob" , 24 }, {"Alice" , 20 }, } slices.SortFunc(people, func (a, b Person) int { if n := cmp.Compare(a.Name, b.Name); n != 0 { return n } return cmp.Compare(a.Age, b.Age) }) fmt.Println(people) }
Output: [{Alice 20} {Alice 55} {Bob 24} {Gopher 13}]
SortStableFuncは要素同士が等しいと判定された場合に、オリジナルの順序を変更しません
▼SortStableFuncの使用例
import ( "cmp" "fmt" "slices" ) func main () { type Person struct { Name string Age int } people := []Person{ {"Gopher" , 13 }, {"Alice" , 20 }, {"Bob" , 24 }, {"Alice" , 55 }, } slices.SortStableFunc(people, func (a, b Person) int { return cmp.Compare(a.Name, b.Name) }) fmt.Println(people) }
Output: [{Alice 20} {Alice 55} {Bob 24} {Gopher 13}]
比較関連
Compare/CompareFunc
説明
与えられた2つのスライスs1,s2を比較します
すべての要素が同じである場合は0を返却します
最初に一致しない要素が見つかった場合、要素の比較を行い、s1の要素<s2の要素の場合は-1を返却します(その逆で1を返却します)
s1とs2の要素が異なる場合、len(s1)<len(s2)で-1を返却します(その逆で1を返却します)
コメント
-1と1の判定がやや複雑かなという印象です
CompareFuncもやや使いこなすには工夫が必要そうです
▼Compareの使用例
package mainimport ( "fmt" "slices" ) func main () { names := []string {"Alice" , "Bob" , "Vera" } fmt.Println("Equal:" , slices.Compare(names, []string {"Alice" , "Bob" , "Vera" })) fmt.Println("V < X:" , slices.Compare(names, []string {"Alice" , "Bob" , "Xena" })) fmt.Println("V > C:" , slices.Compare(names, []string {"Alice" , "Bob" , "Cat" })) fmt.Println("3 > 2:" , slices.Compare(names, []string {"Alice" , "Bob" })) }
Output: Equal: 0 V < X: -1 V > C: 1 3 > 2: 1
▼CompareFuncの使用例
package mainimport ( "cmp" "fmt" "slices" "strconv" ) func main () { numbers := []int {0 , 43 , 8 } strings := []string {"0" , "0" , "8" } result := slices.CompareFunc(numbers, strings, func (n int , s string ) int { sn, err := strconv.Atoi(s) if err != nil { return 1 } return cmp.Compare(n, sn) }) fmt.Println(result) }
Equal/EqualFunc
説明
与えられた2つのスライスs1,s2を比較して、同じであればtrueを返却します
コメント
シンプルな分Compareよりも使うケースは多いのかなという印象です
▼Equalの使用例
package mainimport ( "fmt" "slices" ) func main () { numbers := []int {0 , 42 , 8 } fmt.Println(slices.Equal(numbers, []int {0 , 42 , 8 })) fmt.Println(slices.Equal(numbers, []int {10 })) }
▼EqualFuncの使用例
package mainimport ( "fmt" "slices" "strconv" ) func main () { numbers := []int {0 , 42 , 8 } strings := []string {"000" , "42" , "0o10" } equal := slices.EqualFunc(numbers, strings, func (n int , s string ) bool { sn, err := strconv.ParseInt(s, 0 , 64 ) if err != nil { return false } return n == int (sn) }) fmt.Println(equal) }
最大-最小関連
Max/MaxFunc
説明
コメント
MaxFuncを使えば構造体のスライスに対してフィールドの最大値を評価することもできます
▼Maxの使用例
package mainimport ( "fmt" "slices" ) func main () { numbers := []int {0 , 42 , -10 , 8 } fmt.Println(slices.Max(numbers)) }
▼MaxFuncの使用例
import ( "cmp" "fmt" "slices" ) func main () { type Person struct { Name string Age int } people := []Person{ {"Gopher" , 13 }, {"Alice" , 55 }, {"Vera" , 24 }, {"Bob" , 55 }, } firstOldest := slices.MaxFunc(people, func (a, b Person) int { return cmp.Compare(a.Age, b.Age) }) fmt.Println(firstOldest.Name) }
Min/MinFunc
説明
与えられた2つのスライスのなかの最小値を返却します
コメント
MinFuncを使えば構造体のスライスに対してフィールドのさいしょ値を評価することもできます
▼Minの使用例
package mainimport ( "fmt" "slices" ) func main () { numbers := []int {0 , 42 , -10 , 8 } fmt.Println(slices.Min(numbers)) }
▼MinFuncの使用例
import ( "cmp" "fmt" "slices" ) func main () { type Person struct { Name string Age int } people := []Person{ {"Gopher" , 13 }, {"Bob" , 5 }, {"Vera" , 24 }, {"Bill" , 5 }, } firstYoungest := slices.MinFunc(people, func (a, b Person) int { return cmp.Compare(a.Age, b.Age) }) fmt.Println(firstYoungest.Name) }
番外編sortedslicesパッケージ 渋川さん(@shibu_jp )が作成したsortedslicesパッケージについて簡単に紹介させてください。
https://github.com/shibukawa/sortedslices
このパッケージではソートされたスライスに対しての関数群を提供しています。
例えばソートされたスライスを複数指定することで単一のソートされたスライスを返却するUnion
関数などがあります。
▼Unionの使用例
package mainimport ( "fmt" "github.com/shibukawa/sortedslices" ) type Sale struct { Date string Value int } func main () { slice1 := []Sale{ {"2023-01-01" , 100 }, {"2023-01-03" , 150 }, {"2023-01-05" , 200 }, } slice2 := []Sale{ {"2023-01-02" , 120 }, {"2023-01-04" , 170 }, } slice3 := []Sale{ {"2023-01-01" , 90 }, {"2023-01-06" , 220 }, } comparisonFunc := func (s1, s2 Sale) int { if s1.Date < s2.Date { return -1 } if s1.Date > s2.Date { return 1 } return 0 } unioned := sortedslices.UnionFunc(comparisonFunc, slice1, slice2, slice3) for _, v := range unioned { fmt.Println(v) } }
Output: {2023-01-01 100} {2023-01-01 90} {2023-01-02 120} {2023-01-03 150} {2023-01-04 170} {2023-01-05 200} {2023-01-06 220}
単一のスライスにマージしてからソートし直すのとどちらが性能が良いのでしょうか。
簡単なベンチマークを測定してみました。⤵
▼Unionのベンチマーク
union_test.go package mainimport ( "github.com/shibukawa/sortedslices" "math/rand" "sort" "testing" "time" ) func BenchmarkUnionFuncLarge (b *testing.B) { rand.Seed(time.Now().UnixNano()) a := generateSortedSlice(100000 ) bSlice := generateSortedSlice(100000 ) cmp := func (e1, e2 int ) int { if e1 < e2 { return -1 } else if e1 > e2 { return 1 } return 0 } b.ResetTimer() for i := 0 ; i < b.N; i++ { _ = sortedslices.UnionFunc(cmp, a, bSlice) } } func BenchmarkMergeAndSortLarge (b *testing.B) { rand.Seed(time.Now().UnixNano()) a := generateSortedSlice(100000 ) bSlice := generateSortedSlice(100000 ) b.ResetTimer() for i := 0 ; i < b.N; i++ { merged := append (a, bSlice...) sort.Ints(merged) } } func generateSortedSlice (size int ) []int { slice := make ([]int , size) for i := range slice { slice[i] = rand.Int() } sort.Ints(slice) return slice }
❯ go test -bench . goos: linux goarch: amd64 pkg: github/sample cpu: AMD Ryzen 5 5600X 6-Core Processor BenchmarkUnionFuncLarge-12 506 2575694 ns/op BenchmarkMergeAndSortLarge-12 79 16547631 ns/op PASS ok github/sample 3.005s
結果からみてもUnion
を利用したほうが圧倒的に性能が良いことがわかりますね。
他にもスライスから共通要素を抽出するIntersection
関数、差分要素を抽出するDifference
関数などもありますので、ぜひ利用してみてください。
おわりに Go1.21では待望のslicesパッケージが追加されました。スライスやマップは頻繁に利用するので標準関数が増えるのはとても嬉しいですね。
#56353 ではConcat関数も提案されており、既にアクセプトされています。今後のslicesの機能追加からも目が離せないですね。
最後までお付き合いいただきありがとうございました。