Go言語の標準ライブラリを触ってみる-“slices”

はじめに

こんにちは、皆さん。
今回は Go 言語の標準ライブラリの中でも特に重要な役割を担う "slices" について詳しく解説していきます。

"slices" は、Go 言語で配列を扱う際に便利な機能を提供してくれるライブラリです。
配列を効率的に操作するために、Go 言語では "slices" を使うことが推奨されています。
今回は、"slices" の機能を詳しく解説していきますので、ぜひ最後までご覧ください。

検証環境

$ go version
go version go1.20.2 darwin/arm64

標準ライブラリ "slices" とは?

Go 言語の標準ライブラリの中でも特に重要な役割を担う "slices" とは、Go 言語で配列を扱う際に便利な機能を提供してくれるライブラリです。
"slices" は、Go 言語で配列を効率的に操作するために、標準ライブラリとして提供されています。

要素の検索、ソートのための便利な機能も提供されています。

使い方

以下のインポート処理をコードのヘッダー部に記述します。

import "golang.org/x/exp/slices"

これで、"slices" 標準ライブラリを使う準備が整いました。

"slices" の機能

"slices" 標準ライブラリでは、以下の機能を提供しています。

Clone : スライスのコピー

以下のサンプルコードを見てみましょう。

package main

import (
    "fmt"
)

func main() {

    s := []int{1, 2, 3, 4, 5}

    // スライスのサブスライスを作成
    sub := s[1:3]

    // スライスを変更
    s[2] = 10

    // サブスライスを表示
    fmt.Println(sub) // [2, 10]
}

上記のコードでは、スライスからサブスライスを作成しています。

しかし、サブスライスを作成した時点で、元のスライスとサブスライスはメモリ領域を共有しているため、元のスライスを変更すると、サブスライスも変更されてしまいます。

そのため、上記のコードでは、s[2]10 に変更した結果、サブスライスの [2] の値も 10 に変更されています。

この問題に対処するために、スライスをコピーするという方法がありますが、コピー処理を記述するのは面倒です。
そんなときに便利なのが slices.Clone です。

package main

import (
    "fmt"

    "golang.org/x/exp/slices"
)

func main() {

    // スライスを定義
    s := []int{1, 2, 3, 4, 5}

    // スライスのコピーを作成
    sub := slices.Clone(s[1:3])

    // スライスを変更
    s[2] = 10

    // スライスのコピーには影響がない
    fmt.Println(sub) // [2, 3]
}

上記のコードでは、slices.Clone 関数を使用して、スライスのコピーを作成しています。

スライスのコピーを作成することで、元のスライスを変更しても、コピーのスライスには影響がないことが確認できます。

Contains : スライスに指定した要素が含まれているかを確認する

以下のエントリで、 lo パッケージにも同様の機能が存在することを紹介していました。

機能としては全く変わりません。

指定された要素がスライスに含まれていれば true を返します。

package main

import (
    "fmt"
    "github.com/golang/slices"
)

func main() {
    s := []int{1, 2, 3}

    // 指定した要素がスライスに含まれているかを確認
    fmt.Println(slices.Contains(s, 2)) // true
    fmt.Println(slices.Contains(s, 4)) // false
}

Delete : スライスから指定された開始位置、終了位置の要素を取り除く

メソッドのシグネチャは以下のようになります。

func Delete(slice S, i, j int) S

s で指定されたスライスから、 s[i:j] のサブスライスを削除し新しいスライスを返します。

ij で指定した範囲が不正な場合( i がマイナスだったり、 j がスライスのサイズを超えていた場合 ) 、パニックにより実行が停止します。

便利そうな関数ではありますが注意が必要です。
以下にサンプルコードを掲載します。

package main

import (
    "fmt"

    "golang.org/x/exp/slices"
)

func main() {
    s := []int{0, 1, 2, 3}

    fmt.Println(s) // [0 1 2 3]

    sub := slices.Delete(s, 1, 3)

    fmt.Println(sub) // [0 3
    fmt.Println(s)   // [0 3 2 3]

    // サブスライスを変更すると元のスライスも変更される
    sub[1] = 100

    fmt.Println(s) // [100 3 2 3]
}

注意点は以下のとおりです。

  • Delete 関数は元のスライスを変更します。
    • ( 要素を削除した新たなスライスは、元のスライスの要素 0 を起点に上書きされます。
      )
  • Delete 関数は元のスライスを変更したサブスライスを返します。
  • サブスライスを変更すると元のスライスも変更されます。

この挙動を見ると、元のスライスは使えないと考えたほうが良さそうです

Index : 指定された要素が最初に登場する添字を返す

スライスを順に捜査し、指定された要素が最初に見つかった位置(最初の要素は 0)を返します。

要素が見つからなかった場合には -1 を返します。

package main

import (
    "fmt"

    "golang.org/x/exp/slices"
)

func main() {
    s := []int{0, 1, 2, 3, 2}

    fmt.Println(slices.Index(s, 2)) // 2
    fmt.Println(slices.Index(s, 4)) // -1
}

Insert : 要素をスライスに挿入する

指定位置に要素を挿入します。要素は可変引数となっていて、複数挿入することも可能です。

挿入位置を末尾に指定すれば、 append の代わりとしても利用できます。

package main

import (
    "fmt"

    "golang.org/x/exp/slices"
)

func main() {
    s := []int{0, 1, 2, 3}

    fmt.Println(slices.Insert(s, 2, 4))       // [0 1 4 2 3]
    fmt.Println(slices.Insert(s, 2, 5, 6, 7)) // [0 1 5 6 7 2 3]
  // 末尾に追加 ( append と同じ )
    fmt.Println(slices.Insert(s, 4, 10))      // [0 1 2 3 10]
    // fmt.Println(slices.Insert(s, 5, 10))      // panic
}

Replace : スライスの要素を置換する

Delete + Insert の挙動をします。

指定された開始位置、終了位置の要素を削除した後、要素を挿入します。

このReplace 関数も、元スライスの更新の仕方に関してわかりにくい挙動をします。

以下のコードを見てください。

package main

import (
    "fmt"

    "golang.org/x/exp/slices"
)

func main() {
    s := []int{0, 1, 2, 3}

    // 元スライスは変更されていない
    fmt.Println(slices.Replace(s, 2, 3, 10, 11, 12)) // [0 1 10 11 12 3]

    // 元スライスは変更されていない
    fmt.Println(s) // [0 1 2 3]

    fmt.Println(slices.Replace(s, 2, 3, 20)) // [0 1 20 3]

    // 元スライスが変更されている
    fmt.Println(s) // [0 1 20 3]

}

元スライスの要素数を超えた置換を行う場合には、元スライスのコピーを作成しているようでメモリ領域は別となっています。

しかし、元スライスの要素数が変わらない置換を行う場合は、元スライスとメモリを共有しています。
では要素数を超えた場合は常に元スライスとメモリを別途するかというとそういうわけでもなさそうです。

package main

import (
    "fmt"

    "golang.org/x/exp/slices"
)

func main() {
    s := make([]int, 4, 100)
    s[0] = 0
    s[1] = 1
    s[2] = 2
    s[3] = 3

    fmt.Println(slices.Replace(s, 2, 3, 10, 11, 12)) // [0 1 10 11 12 3]

    // 元スライスが変更されている
    fmt.Println(s) // [0 1 10 11]
}

スライスのキャパシティが拡張されるケースでは元スライスと別領域を作成しているためメモリ領域を共有しないが、拡張不要の場合にはメモリ領域を共有するようです。

この挙動は slices.Insert 関数を使った場合も同様です。

このあたりの挙動がどうしても不安なので、心配なら slices.Clone を事前に実行しておくのが良さそうです。

ひとこと

今回は "slices" 標準ライブラリの関数に触れましたが、途中から脇道にそれてスライスのメモリ領域にの話に触れました。

毎度のことですが、Go 言語を勉強している中でスライスの扱いが漠然と「怖い」と感じてしまいます。意図しないところで元スライスが変更されるケースが怖いので、スライスを関数を渡すときには呼び出し元か関数開始直後に slices.Clone を呼び出してコピーするようにしておきたいです。

Posted by genzouw