1
2

はじめに

スライスや配列をスライス化する際に、メモリリークが発生する可能性がありますが、文字列でも同様の事象が考えられます。(スライスについては以下を参照)

部分文字列

func main() {
	s1 := "Hello"
	s2 := s1[:5]
	fmt.Println(s2) // Hello
}

s2s1の部分文字列となっている。ただし、最初の5つのruneからではなく、5バイトから文字列を作成している。

ちなみに、runeについては以下で詳しく解説しています。

エンコードすると複数バイトのruneになる場合は、上記のような部分文字列は使用してはいけない。

Go Playground

func main() {
	s1 := "こんにちは"
	s2 := s1[:5]
	fmt.Println(s2) // こ��
}

まず、[]runeに変換しなければいけない。

Go Playground

func main() {
	s1 := "こんにちは"
	s2 := string([]rune(s1)[:5])
	fmt.Println(s2) // こんにちは
}

部分文字列とメモリリーク

以下のコードは、ログメッセージを文字列として受け取る。メモリに保存するログは、最初に36文字のUUIDでフォーマットされて、その後にメッセージ自身が続く。最新のn個のUUIDのキャッシュを保存することができる。これらのログメッセージは、サイズが大きくなる可能性がある(最大数kbyte)。

func (s store) handleLog(log string) error {
	if len(log) < 36 {
		return errors.New("log is not correctly formatted")
	}
	uuid := log[:36]
	s.store(uuid)
	// Do something
	return nil
}

UUIDを取り出す際にlog[:36]で部分文字列演算を使用している。

Goの仕様では、部分文字列の操作を行う場合、結果の文字列と部分文字列の操作に関わった文字列が同じデータを共有すべきなのかは規定されていない。Goのコンパイラは、両者が同じ規定配列を共有しており、新たな割り当てとコピーを防いでいる。

log[:36]は同じ基底配列を参照する文字列を新規作成するので、メモリに格納されるかくuuidは36byteだけではなく、元のログ文字列のバイト数になる。

この問題は、以下のように部分文字列を[]byteに変換し、文字列に変換することで、コピーすることで解決する。

uuid := string([]byte(log[:36]))

Go1.18以上では、標準ライブラリに文字列の新たなコピーを返すstrings.Cloneが存在する。
strings.Cloneを使って、以下のようにすると、メモリリークを防ぐことができる。

uuid := strings.Clone(log[:36])

まとめ

  • 部分文字列では、スライスの範囲はバイト数に基づいており、runeの数ではない
  • 部分文字列は同じ基底配列を共有するので、メモリリークになる可能性がある
  • メモリリークはstrings.Clone()か文字列の変換によってコピーを行うことで解決する

スライド

参考文献

  1. 100 Go Mistakes and How to Avoid Them
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2