2022/09/07

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. Re: Go言語で画像ファイルか確認してみる
  4. net/url に JoinPath が入った。
  5. Go の struct は小さくできる(fieldalignment のススメ)

Go ではバイト列と文字列は異なる内部データとして扱っています。[]byte から string へ変換したり、またその逆を行う際にはキャストが必要になります。ですので string はイミュータブルになります。

しかしイミュータブルなのは理解しつつもバイト列を文字列にする為に無駄なアロケートをしたくない場合もあります。これまで Go ではドキュメントに明文化していなかった為に色々な作法が生まれてしまっていました。その代表的な物が以下です。

s := *(*string)(unsafe.Pointer(&b))

本来は、Go のバイト列の内部は SliceHeader という構造体により管理されています。

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

また string は以下の StringHeader で管理されています。

type StringHeader struct {
    Data uintptr
    Len  int
}

上記の unsafe.Pointer を使ったコードはこの struct の先頭2フィールドを無理やり参照する事で実現しています。実際に Go のコードでも使われており、これが公式の方法だと思って多くの人が使ってしまっていました。

go/builder.go at 3058d38632aea679c96cd41156b2751c97578a2d · golang/go · GitHub
https://github.com/golang/go/blob/3058d38632aea679c96cd41156b2751c97578a2d/src/strings/builder.go#L30
Sign in to GitHub · GitHub
https://github.com/search?l=Go&q=reflect.StringHeader&type=Code
Sign in to GitHub · GitHub
https://github.com/search?l=Go&q=reflect.SliceHeader&type=Code

しかしながらこの実装は、今後 Go の文字列とバイト列に関する最適化の妨げになり得ます。例えば StringHeader と SliceHeader の交換を最適化する事ができなくなります。

そこで今回、unsafe.StringData、unsafe.String、unsafe.SliceData が入りました。これによりバイト列から文字列、またはその逆がミュータブルに変換できる様になりました。当然ですがこれらはバイト列の破壊的な変更により意図しない文字列の変更が行われる為、慎重に取り扱わなければなりません。

func StringToBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

func BytesToString(b []bytestring {
    return unsafe.String(&b[0], len(b))
}

unsafe.StringData は string が持っているバイト列へのポインタが返されます。

fmt.Println(unsafe.StringData("Hello"))
fmt.Println(unsafe.StringData("Hello"))
fmt.Println(unsafe.StringData("Hello1"))

これを実行すると以下の様に表示され、Go のコンパイラが文字列リテラル Hello の2つをまとめて保持しているのが分かります。

0xc67760
0xc67760
0xc6790b

また文字列がスタックに取られている時にはメモリ破壊が発生しますのでランタイムでクラッシュします。

s := "hello"
b := StringToBytes(s)
b[0] = 'h'
fmt.Println(s)

以下の様にヒープにメモリが確保されている場合には期待通りに動作します。

s := string([]byte("hello"))
b := StringToBytes(s)
b[0] = 'h'
fmt.Println(s)

動的なアロケーションが発生しないため高速な変換が可能です。おそらく今後、Go の内部実装の多くがこれらで置き換わっていき、幾らか高速化が見込まれます。

Posted at by | Edit