👋

k8s.io/apimachinery/pkg/util/sets の使い道

に公開

sets が便利なので使いましょう!

https://pkg.go.dev/k8s.io/apimachinery/pkg/util/sets

Set は slice のような概念ですが、内部的には map なので要素の重複は許されておらず、そのため要素の追加や削除、存在確認などを短い時間で行えます。
集合の和集合(Union)、差集合(Difference)、交差(Intersection)、対称差(Symmetric Difference)など数学的な集合演算が組み込みで提供されています。
もちろん Insert や Delete 、Equal 、 Clear 、 Clone 、 Len などの一般的な操作をするための関数は用意されています。
また UnsortedList の method があるため最終的な結果を slice にして返却することが可能です。

今回は sets 紹介していきたいと思います。

型的には以下のように定義されています。

type Empty struct{}
type Set[T comparable] map[T]Empty

New

func New[T comparable](items ...T) Set[T]

説明不要ですね、新たな Set を返す関数です。
注意点としては sets.NewString() などの型を指定して作成する関数は Deprecated になっているため、型を指定して全て sets.New() で作成してください。

deprecated
s := sets.NewString()

ではなく

s := sets.New[string]()

のようにして作成してください。

Union

func (s1 Set[T]) Union(s2 Set[T]) Set[T]

s1 と s2 の和集合(全て)を返す関数です。

Difference

func (s1 Set[T]) Difference(s2 Set[T]) Set[T]

s1 と s2 の差集合(s1 だけに含まれ s2 には含まれないもの)を返す関数です。

Intersection

func (s1 Set[T]) Intersection(s2 Set[T]) Set[T]

s1 と s2 の共通部分(s1 にも s2 にも含まれるもの)を返す関数です。

SymmetricDifference

func (s1 Set[T]) SymmetricDifference(s2 Set[T]) Set[T]

Example

関数の説明は Doc を読めば良いと思うのでここからは使用例を紹介していこうと思います。

存在しているかを判定する

例えば Has を用いて map のように指定する値が含まれているか確認する処理を書き直せます。

sets を使用しない方法
disallowedEnvs := map[string]struct{}{
  "RESERVED": {},
}
var pod corev1.Pod
_ := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(hoge), &pod)
for i, container := range pod.Spec.Containers {
  for k, env := range container.Env {
    if _, exists := disallowedEnvs[env.Name]; exists {
      var envList []string
      for env := range disallowedEnvs {
        envList = append(envList, env)
      }
      return field.Invalid(field.NewPath("spec", "containers").Index(i).Child("env").Index(k).Child("name"), env.Name, fmt.Sprintf("must not be same as %v", envList))
    }
  }
}
sets を使用した方法
disallowedEnvs := sets.New("RESERVED")
var pod corev1.Pod
_ := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(hoge), &pod)
for i, container := range pod.Spec.Containers {
  for k, env := range container.Env {
    if disallowedEnvs.Has(env.Name) {
      return field.Invalid(field.NewPath("spec", "containers").Index(i).Child("env").Index(k).Child("name"), env.Name, fmt.Sprintf("must not be same as %v", disallowedEnvs.UnsortedList()))
    }
  }
}

二重ループを回避する

Go で for の二重ループを回避するために map に入れ直すケースはよくあると思います。
sets を使用すると make(map[string]struct{}) のような謎の map を書かずに簡潔に書くことができます。

for の二重ループ
list1 := []string{"a", "b", "c"}
list2 := []string{"b", "c", "d"}
var common []string
for _, item1 := range list1 {
  for _, item2 := range list2 {
    if item1 == item2 {
      common = append(common, item1)
      break
    }
  }
}
// common = ["b", "c"]
map[string]struct{} を使用する方法
list1 := []string{"a", "b", "c"}
list2 := []string{"b", "c", "d"}
set := make(map[string]struct{})
for _, item := range list1 {
  set[item] = struct{}{}
}
var common []string
for _, item := range list2 {
  if _, exists := set[item]; exists {
    common = append(common, item)
  }
}
// common = ["b", "c"]
sets を使用した方法1
list1 := []string{"a", "b", "c"}
list2 := []string{"b", "c", "d"}
s := sets.New(list1...)
commons := sets.New[string]()
for _, item := range list2 {
  if s.Has(item) {
    common.Insert(item)
  }
}

common := commons.UnsortedList()
// common = ["b", "c"]
sets を使用した方法2
list1 := []string{"a", "b", "c"}
list2 := []string{"b", "c", "d"}
common := sets.New(list1...).Intersection(sets.New(list2...)).UnsortedList()
// common = ["b", "c"]

まとめ

全ての関数の例を示すことはできていませんが sets を使用することで、slice の操作時により意図が明確になり、コードの可読性が向上することができます。

サービス側で許可している(していない)値がありユーザーからの値を確認してチェックする場合や、複数の配列を操作する必要がある場合などに使用すると力を感じられると思います。

ぜひ sets を使用してみてください!

Discussion