Go v1.23にて導入されたuniqueパッケージについて調べました

2024-12-16

概要


Go v1.23 が 8 月にリリースされてから数ヶ月が経ちました。 v1.23 における新機能の中で、unique パッケージというものが導入されましたが、実際どのようなユースケースで用いるのか興味が湧きましたので、 本記事にて紹介しようと思います。

当記事を読むことで、unique パッケージを実際の開発に使うモチベーションが高まれば幸いです。

Unique パッケージとはなんなのか


Go v1.23 において unique パッケージが紹介されました。公式ブログの通り、このパッケージの目的は、 メモリ内で共有プール内の値の重複をなくし、単一の正規化された一意なコピーを指すようにすることができます。 これはコンピューターサイエンスではインターニングと呼ばれています。 言葉の定義だけでは分かりづらいかと思いますので、実例を示しながら説明しようかと思います。

Unique パッケージを導入する前の例


下記は大量のテキストデータが含まれるファイルを読み込み、その中から a で始まる単語郡を見つけるためのプログラムです。 説明のため、渡される単語については、appliance のような一つの単語内に同じ文字が含まれる単語が存在しないことを前提とします。

func main() {
	const filePath = "./large_text_file.txt"
	data, err := os.ReadFile(filePath)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	text := string(data)
	aWords := extractAwords(text)
	fmt.Printf("Found %d 'a' words\n", len(aWords))
}

func extractAwords(text string) []string {
	var words []string
	start := -1
	for i, c := range text {
		if isWordChar(c) {
			if start == -1 {
				start = i
			}
		} else {
			if start != -1 {
				word := text[start:i] // 部分文字列を参照 
				if len(word) > 0 && word[0] == 'a' {
					words = append(words, word) // 参照を保持
				}
				start = -1
			}
		}
	}
	return words
}
func isWordChar(c rune) bool {
	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

extractAwords 関数の中では、text から slice を作成し、その単語が a から始まる単語であるかを検証しています。

word := text[start:i] // 部分文字列を参照
if len(word) > 0 && word[0] == 'a' {
  words = append(words, word) // 参照を保持
}

このような場合、word はオリジナルの text を参照し続けているためガベージコレクションの対象とはならず、プログラムは多くのメモリを使用したままになってしまいます。

そこで Go の v1.18 にて紹介されたのが strings.Clone 関数です。 この関数を使うことで部分文字列が新しいメモリにコピーされるため、元の文字列 text への参照が切れます。 この操作により、メモリ使用量を削減することができます。

  word := text[start:i]
  if len(word) > 0 && word[0] == 'a' {
    cloned := strings.Clone(word) // 部分文字列をコピー
    words = append(words, cloned)
  }

ただし、この方法においてもまだ問題があります。 a から始まる単語がそれぞれメモリ上にコピーされており、それぞれの配列の要素は各コピーされた値を参照しています。 ここで apple は同じ値ではありますが、配列のそれぞれの要素は異なるメモリの場所を参照しており、効率的であると言えません。 clone

ここで登場するのがインターニングであり、unique パッケージです。

unique パッケージ導入後の世界


unique パッケージ内の Make メソッドを使用することで、文字列がプール内で一意に管理されるようになります。 そのため同じ文字列のメモリを複数回使用することを防ぎ、メモリ使用量を更に削減することができます。

  word := text[start:i]
  if len(word) > 0 && word[0] == 'a' {
    handle := unique.Make(word)
    words = append(words, handle)
  }

unique

unique.Make(work)の出力値の型はunique.Handle[T]であり、値の比較にも使用することができます。 例えば 2 つの unique.Handle[T]が等しいか比較した場合、通常の文字列比較であれば、char レベルでの比較が必要ですが、 ポインタでの比較になるため比較を高速に行うことができます。

おわりに


本記事では、弊社 Go v1.23 にて導入された unique について記載致しました。 私はインターニングという概念にあまり詳しくなかったのですが、調査を実施したことにより理解が深まりました。 実務でも使えるようなところはどんどん使っていきたいと思います。

最後に、Belong では、共に働くエンジニアを募集しています。新しい技術に興味がある方、Go 言語での開発に興味がある方は以下リンクもご参考いただければ幸いです。 Entrance Book

参考文献

この記事は以下の情報を参考にして執筆しました