はじめに
スライスや配列をスライス化する際に、メモリリークが発生する可能性がありますが、文字列でも同様の事象が考えられます。(スライスについては以下を参照)
部分文字列
func main() {
s1 := "Hello"
s2 := s1[:5]
fmt.Println(s2) // Hello
}
s2
はs1
の部分文字列となっている。ただし、最初の5つのruneからではなく、5バイトから文字列を作成している。
ちなみに、runeについては以下で詳しく解説しています。
エンコードすると複数バイトのruneになる場合は、上記のような部分文字列は使用してはいけない。
func main() {
s1 := "こんにちは"
s2 := s1[:5]
fmt.Println(s2) // こ��
}
まず、[]runeに変換しなければいけない。
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()
か文字列の変換によってコピーを行うことで解決する