文字列の連結
Goには文字列を連結する方法は2通りある。
シンプルな連結(+=
で連結する)
スライスの全ての文字列を連結するconcat
関数を作成する。
func concat(values []string) string {
s := ""
for _, value := range values {
s += value
}
return s
}
これだと反復ごとにs
は更新されず、新たな文字列がメモリから再割り当てされる。
strings.Builderを使用する方法(事前割り当てなし)
stringsパッケージのBuilderを用いることで上記の問題が解決し、メモリ上のコピーを最小限にする。
func concat(values []string) string {
sb := strings.Builder{}
for _, value := range values {
_, _ = sb.WriteString(value)
}
return sb.String()
}
WriteString()
は、nil以外のエラーを返すことはない。しかし、strings.Builder
は、単一のメソッドを含むio.StringWriter
インターフェイスを実装しており、WriteString(s string)(n int, err error)
となっているため、エラーを返す必要がある。
string.Builder
は内部的にバイトスライスを保持しており、`WriteString{を呼び出すたびに、appendが呼び出される。この構造体は並行的に使うべきではない。また、スライスの長さがわかっている場合は、事前に割り当てる必要がある。スライスの初期化のベストプラクティス
strings.Builderを使用する方法(事前割り当てあり)
strings.BuildGrow(n int)
メソッドを使用すると、nバイトの領域を別に保証することができる。
func concat(values []string) string {
total := 0
for i := 0; i < len(values); i++ {
total += len(values[i])
}
sb := strings.Builder{}
sb.Grow(total)
for _, value := range values {
_, _ = sb.WriteString(value)
}
return sb.String()
}
結局どれがいいのか
ベンチマークテストでは、3つ目の方法が一番速くなっている。(参考文献から)
反復処理を2回行っても、スライスの初期化で長さを割り当てているので、速くなる。
ただし、2,3個のの文字列を連結するだけなら、strings.Builder
を使用すると可読性が悪くなるので、オススメしない。(5個以上の連結であればstrings.Builder
を使用する方が良い。)
文字列とバイト
GoではほとんどのI/Oは、文字列ではなく、[]byte
で行われる。
例えば、io.Reader, io.Writer, io.ReadAllなどが挙げられる。
type Reader interface {
Read(p []byte) (n int, err error)
}
io.Reader
を入力とし、sanitize
関数を呼び出すgetBytes
関数を作成する。(サニタイズは先頭と末尾の空白を全て取り除くこと。)
stringを使用した実装
func getBytes(reader io.Reader) ([]byte, error) {
b, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return []byte(sanitize(string(b))), nil
}
func sanitize(s string) string {
return strings.TrimSpace(s)
}
[]byte(sanitize(string(b)))
で[]byteを文字列に変換して、文字列を[]byteに変換するという冗長な書き方をしている。また、メモリも変換に余分な割り当てが発生する。
[]byteを使用した実装
[]byteを使用すると、不要な変換を行わずに実装できる。
func getBytes(reader io.Reader) ([]byte, error) {
b, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return sanitize(b), nil
}
func sanitize(b []byte) []byte {
return bytes.TrimSpace(b)
}
stringsパッケージのSplit, Count, Contains, Indexなどはbytesパッケージにも存在する。
文字列の不変性
[]byteから文字列を作成すると、コピーが作成される。
func main() {
b := []byte{'a', 'b', 'c'}
s := string(b)
b[1] = 'x'
fmt.Println(s) // abc
}
Goでは、文字列はイミュータブルである。