フューチャー技術ブログ

Go1.21:slicesパッケージのチートシート

The Gopher character is based on the Go mascot designed by Renée French.

Gopherのイラストはegonelbre/gophersを利用しました。

はじめに

本記事はGo1.21連載の記事です。

こんにちは。TIG/EXユニット所属宮永です。

先日、第一回日本遺産検定に晴れて合格し、日本遺産ソムリエになりました。夏の旅行先をまだお決めでない方は、日本遺産公式サイトを参考にして、日本各地の魅力的なスポットを訪れてみてはいかがでしょうか。

日本遺産については公式サイトの説明が非常にわかりやすいのでご覧になってください。


さて、記事のメインテーマとは異なる事柄を挟みましたが、ここからはGo1.21に追加されたslicesパッケージの解説を行います。

Go1.21のアップデート内容に関しては、すでに多くの解説記事が公開されています。

今回注目するslicesパッケージは、Go1.21以前にはx/exp/slicesとして運用されていたものが正式に取り込まれ、多数の関数が追加されたものです。この記事では、それを「slicesパッケージのチートシート」としてまとめ、解説します。

記事の構成

本記事ではGo1.21で追加されたslicesパッケージを以下5つのカテゴリに分類し、それぞれ使い方などを解説します。また、末尾にはソートされたスライス用のsortedslicesパッケージを渋川さん(@shibu_jp)が作成されているので簡単に紹介しています。

slicesパッケージ機能紹介

関数の説明とサンプルコードを紹介します(公式のサンプルが存在する場合、原則そのまま引用しています。ない場合は適宜補完しています)

また、感想も適宜コメントしています。

本家のGo Docsはこちら

検索関連

関数名 説明
BinarySearch スライスから要素を二分探索します
BinarySearchFunc 比較関数を使用して要素を二分探索します
Contains スライスが要素を含むかを確認します
ContainsFunc 指定の関数を満たす要素がスライスに含まれているか確認します
Index 指定の要素のインデックスを返します
IndexFunc 指定の関数を満たす要素のインデックスを返します

BinarySearch/BinarySearchFunc

  • 説明

    • スライスの中に指定した要素が存在するか二分探索します
    • BinarySearhcを適用するスライスはソートされている必要があります
    • 戻値として要素のインデックスと存在可否を示すboolが返却されます
      • スライスに要素が含まれた場合はboolがtrueとなり、要素のインデックスを返却します
      • スライスに要素が含まれない場合、boolはfalseとなり、要素が存在するべきインデックスを返却します
  • コメント

    • 要素が存在しない場合に挿入すべき箇所が即座にわかるという点で太字の機能は意外と便利な機能だと思います
    • 適用するスライスがソートされているという前提が注意するべき点ですね

▼BinarySearchの使用例

package main

import (
"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 main

import (
"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)
}
Output:

Bob: 1 true

Contains/ContainsFunc

  • 説明
    • スライスの中に指定した要素が存在するか探索します
    • 戻値として存在可否を示すboolが返却されます
    • ContainsFuncでは指定した関数を満たす要素が存在する場合trueを返却します
  • コメント
    • 個人的にはslicesパッケージの中で一番利用している関数です
    • 内部的には次に説明するIndexを呼び出しています

▼Containsの使用例

package main

import (
"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 main

import (
"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 main

import (
"fmt"
"slices"
)

func main() {
numbers := []int{0, 42, 8}
fmt.Println(slices.Index(numbers, 8))
fmt.Println(slices.Index(numbers, 7))
}
Output:

2
-1

▼IndexFuncの使用例

package main

import (
"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 スライスの未使用のキャパシティを取り除きます
Clone スライスのクローンを作成します
Compact 連続する同じ要素を削除したスライスを返します
CompactFunc 指定の関数で重複を判定して、要素を削除したスライスを返します
Delete スライスの範囲を削除します
DeleteFunc 指定の関数を満たす要素をスライスから削除します
Grow スライスの容量を増やします
Insert スライスに要素を挿入します
Replace スライスの範囲の要素を置換します
Reverse スライスの要素の順序を反転します

Clip

▼Clipの使用例

package main

import (
"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 main

import (
"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までを削除したスライスを返します
    • もとのスライスは書き換えられるため注意が必要です
  • コメント
    • Slice TricksではCutで紹介されていた内容がこのslices.Deleteです

▼Deleteの使用例

package main

import (
"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 main

import (
"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 // delete the odd numbers
})
fmt.Println(seq)
}
Output:

[0 2 8]

Compact/CompactFunc

  • 説明
    • 連続する同じ要素を持つ値を1つの要素に置き換えます
    • 重複排除の目的で利用する場合は適用前にソートする必要があります
  • コメント
    • こちらも頻繁に利用するケースがありそうな関数です
    • CompactFuncの利用方法は少しクセがあります

▼Compactの使用例

package main

import (
"fmt"
"slices"
)

func main() {
seq := []int{0, 1, 1, 2, 3, 5, 8}
seq = slices.Compact(seq)
fmt.Println(seq)
}
Output:

[0 1 2 3 5 8]

▼CompactFuncの使用例

package main

import (
"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)
}
Output:

[bob alice Vera]

Grow

  • 説明
    • 指定した分だけスライスの容量を増やします

▼Growの使用例

package main

import (
"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 main

import (
"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 main

import (
"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 main

import (
"fmt"
"slices"
)

func main() {
names := []string{"alice", "Bob", "VERA"}
slices.Reverse(names)
fmt.Println(names)
}
Output:

[VERA Bob alice]

ソート関連

関数名 説明
IsSorted スライスがソートされているか確認します
IsSortedFunc 指定の関数でスライスがソートされているか確認します
Sort スライスをソートします。
SortFunc 指定の関数でスライスをソートします。
SortStableFunc 指定の関数でスライスを安定的にソートします。

IsSorted/IsSortedFunc

  • 説明
    • スライスが昇順にソートされている場合trueを返却します

▼IsSortedの使用例

package main

import (
"fmt"
"slices"
)

func main() {
fmt.Println(slices.IsSorted([]string{"Alice", "Bob", "Vera"}))
fmt.Println(slices.IsSorted([]int{0, 2, 1}))
}
Output:

true
false

▼IsSortedFuncの使用例

package main

import (
"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))
}
Output:

true
false

Sort/SortFunc/SortStableFunc

  • 説明
    • スライスを昇順にソートします

▼Sortの使用例

package main

import (
"fmt"
"slices"
)

func main() {
smallInts := []int8{0, 42, -10, 8}
slices.Sort(smallInts)
fmt.Println(smallInts)
}
Output:

[-10 0 8 42]

▼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
}
// If names are equal, order by age
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},
}
// Stable sort by name, keeping age ordering of Alices intact
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 2つのスライスを比較します。
CompareFunc 指定の関数で2つのスライスを比較します。
Equal 2つのスライスが等しいか確認します。
EqualFunc 指定の関数で2つのスライスが等しいか確認します。

Compare/CompareFunc

  • 説明
    • 与えられた2つのスライスs1,s2を比較します
    • すべての要素が同じである場合は0を返却します
    • 最初に一致しない要素が見つかった場合、要素の比較を行い、s1の要素<s2の要素の場合は-1を返却します(その逆で1を返却します)
    • s1とs2の要素が異なる場合、len(s1)<len(s2)で-1を返却します(その逆で1を返却します)
  • コメント
    • -1と1の判定がやや複雑かなという印象です
    • CompareFuncもやや使いこなすには工夫が必要そうです

▼Compareの使用例

package main

import (
"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 main

import (
"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)
}

Output:

1

Equal/EqualFunc

  • 説明
    • 与えられた2つのスライスs1,s2を比較して、同じであればtrueを返却します
  • コメント
    • シンプルな分Compareよりも使うケースは多いのかなという印象です

▼Equalの使用例

package main

import (
"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}))
}
Output:

true
false

▼EqualFuncの使用例

package main

import (
"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)
}
Output:

true

最大-最小関連

関数名 説明
Max スライスの最大の要素を返します
MaxFunc 指定の関数でスライスの最大の要素を返します
Min スライスの最小の要素を返します
MinFunc 指定の関数でスライスの最小の要素を返します

Max/MaxFunc

  • 説明
    • 与えられた2つのスライスの中の最大値を返却します
  • コメント
    • MaxFuncを使えば構造体のスライスに対してフィールドの最大値を評価することもできます

▼Maxの使用例

package main

import (
"fmt"
"slices"
)

func main() {
numbers := []int{0, 42, -10, 8}
fmt.Println(slices.Max(numbers))
}

Output:

42

▼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)
}

Output:

Alice

Min/MinFunc

  • 説明
    • 与えられた2つのスライスのなかの最小値を返却します
  • コメント
    • MinFuncを使えば構造体のスライスに対してフィールドのさいしょ値を評価することもできます

▼Minの使用例

package main

import (
"fmt"
"slices"
)

func main() {
numbers := []int{0, 42, -10, 8}
fmt.Println(slices.Min(numbers))
}
Output:

-10

▼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)
}

Output:

Bob

番外編sortedslicesパッケージ

渋川さん(@shibu_jp)が作成したsortedslicesパッケージについて簡単に紹介させてください。

https://github.com/shibukawa/sortedslices

このパッケージではソートされたスライスに対しての関数群を提供しています。

例えばソートされたスライスを複数指定することで単一のソートされたスライスを返却するUnion関数などがあります。

▼Unionの使用例

package main

import (
"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 main

import (
"github.com/shibukawa/sortedslices"
"math/rand"
"sort"
"testing"
"time"
)

func BenchmarkUnionFuncLarge(b *testing.B) {
rand.Seed(time.Now().UnixNano())
a := generateSortedSlice(100000) // 10万の要素を持つスライス
bSlice := generateSortedSlice(100000) // 同じく10万の要素を持つスライス
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) // 10万の要素を持つスライス
bSlice := generateSortedSlice(100000) // 同じく10万の要素を持つスライス

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の機能追加からも目が離せないですね。

最後までお付き合いいただきありがとうございました。