はじめに Go1.24リリース連載 の Go 1.24で新たに追加されたstring
bytes
パッケージの関数、およびencoding
パッケージに追加されたインターフェースTextAppender、BinaryAppenderとその実装について扱います。
strings, bytesに新規に追加された関数 Go 1.24で strings
bytes
パッケージに以下の関数がそれぞれ以下の関数が追加されました。
これらの関数の返り値は strings
パッケージのほうは、iter.Seq[string]
、bytesパッケージのほうはiter.Seq[[]byte]
となっています。strings
bytes
パッケージで同様の関数のため strings
パッケージをメインに解説します。
新規追加関数実装サンプル 今回新たに追加された関数サンプルを示します。
新規追加関数実装サンプル package mainimport ( "fmt" "strings" "unicode" ) func main () { multiLine := `aaa bbb ccc` str := "aaa,bbb,ccc" str2 := "aaa bbb ccc" str3 := "aaa;bbb,ccc" fmt.Println("--- Lines ---" ) for v := range strings.Lines(multiLine) { fmt.Printf("print: %q\n" , v) } fmt.Println("--- SplitSeq ---" ) for v := range strings.SplitSeq(str, "," ) { fmt.Printf("print: %q\n" , v) } fmt.Println("--- SplitAfterSeq ---" ) for v := range strings.SplitAfterSeq(str, "," ) { fmt.Printf("print: %q\n" , v) } fmt.Println("--- FieldsSeq ---" ) for v := range strings.FieldsSeq(str2) { fmt.Printf("print: %q\n" , v) } fmt.Println("--- FieldsFuncSeq ---" ) for v := range strings.FieldsFuncSeq(str3, func (r rune ) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) }) { fmt.Printf("print: %q\n" , v) } }
それぞれの関数の特徴をまとめると以下のようになります。
既存パッケージ、関数との関係 返り値が[]string
のものがありましたが、今回新規に iter.Seq
の返り値の関数が追加された形になります。iter
パッケージは1.23で新規に追加されたパッケージです。iter
パッケージについては1.23リリースでの弊社解説記事 をご参照ください。
iter.Seq
を返すことになったことによりrangeでのループ処理でインデックスの返り値が不要になりました。
strings.Split, strings.SplitSeqそれぞれのループサンプル package mainimport ( "fmt" "strings" ) func main () { str := "aaa,bbb,ccc" fmt.Println("--- Split ---" ) for _, v := range strings.Split(str, "," ) { fmt.Printf("print: %q\n" , v) } fmt.Println("--- SplitSeq ---" ) for v := range strings.SplitSeq(str, "," ) { fmt.Printf("print: %q\n" , v) } }
また、iter.Seq
が返り値のため、iter.Pull
を利用することで next()
の形でループ処理をすることも可能です。
iter.Pullを利用したnext()パターンのループ処理 package mainimport ( "fmt" "iter" "strings" ) func main () { str := "aaa,bbb,ccc" next, stop := iter.Pull(strings.SplitSeq(str, "," )) defer stop() for { v, ok := next() if !ok { break } fmt.Printf("print: %q\n" , v) } }
encoding.TextAppender, encoding.BinaryAppender 1.24からencodingパッケージに encoding.TextAppender , encoding.BinaryAppender のインターフェースが追加されました。
TextAppender, BinaryAppenderインターフェース type TextAppender interface { AppendText(b []byte ) ([]byte , error ) } type BinaryAppender interface { AppendBinary(b []byte ) ([]byte , error ) }
似たようなインターフェースとしてencoding.TextMarshaler , encoding.BinaryMarshaler が以前から存在します。
TextMarshaler, BinaryMarshalerインターフェース type TextMarshaler interface { MarshalText() (text []byte , err error ) } type BinaryMarshaler interface { MarshalBinary() (data []byte , err error ) }
issue によると、すぐ不要になる有効期限が短い文字列を生成ことになり、無駄が発生していることの改善のようです。あらかじめ、キャパシティを確保したスライスを用意し引数として渡すことにより、新規に文字列を作成せず効率よくスライスに追記できるようです。
似たような解説を1.22リリース時の弊社記事 でも解説しているため、合わせてお読みください。
インターフェース追加に合わせて net/netip や、time などで実装が追加されています。
encoding.TextAppenderの内部処理 例えば、net/netip
の Addr
の内部処理 を見てみると、以下のように元のbyteスライス ret
に対しappendを行い、追記するように実装されており効率的な処理になっています。
net/netip.Addr内部処理 func (ip Addr) appendTo4(ret []byte ) []byte { ret = appendDecimal(ret, ip.v4(0 )) ret = append (ret, '.' ) ret = appendDecimal(ret, ip.v4(1 )) ret = append (ret, '.' ) ret = appendDecimal(ret, ip.v4(2 )) ret = append (ret, '.' ) ret = appendDecimal(ret, ip.v4(3 )) return ret }
TextMarshalerとTextAppenderの性能比較 encoding.TextAppender
によって無駄な処理が改善させることなので、いくつかのパッケージでencoding.TextMarshaler
(MarshalText
)encoding.TextAppender
(AppendText
)とを比較してみました。 また、執筆時点での最新バージョン(1.23.5)とも比較しました。
検証対象
net/netip.Addr
AppendText (1.24のみ)
AppendTo (AppendTextのエイリアス)
MarshalText
time.Time
AppendText (1.24のみ)
MarshalText
regexp.Regexp
AppendText (1.24のみ)
MarshalText
また、appendに必要なメモリは事前に確保した状態で計測しています。
検証プログラム
func.go package mainimport ( "net/netip" "regexp" "time" ) var ( addr = netip.MustParseAddr("192.168.255.255" ) jst, _ = time.LoadLocation("Asia/Tokyo" ) tm = time.Date(2025 , time.January, 2 , 3 , 4 , 5 , 6 , jst) reg = regexp.MustCompile(`^\d+\.\d+\.\d+$` ) ) func NetIPAppendTo () { b := make ([]byte , 0 , 15000 ) for range 1000 { b = addr.AppendTo(b) } } func NetIPMarshalText () { b := make ([]byte , 0 , 15000 ) for range 1000 { ba, err := addr.MarshalText() if err != nil { panic (err) } b = append (b, ba...) } } func TimeMarshalText () { b := make ([]byte , 0 , 35000 ) for range 1000 { ba, err := tm.MarshalText() if err != nil { panic (err) } b = append (b, ba...) } } func RegexpMarshalText () { b := make ([]byte , 0 , 15000 ) for range 1000 { ba, err := reg.MarshalText() if err != nil { panic (err) } b = append (b, ba...) } }
func_1.24.go package mainfunc NetIPAppendText () { b := make ([]byte , 0 , 15000 ) var err error for range 1000 { b, err = addr.AppendText(b) if err != nil { panic (err) } } } func TimeAppendText () { b := make ([]byte , 0 , 35000 ) var err error for range 1000 { b, err = tm.AppendText(b) if err != nil { panic (err) } } } func RegexpAppendText () { b := make ([]byte , 0 , 15000 ) var err error for range 1000 { b, err = reg.AppendText(b) if err != nil { panic (err) } } }
func_test.go package mainimport "testing" func BenchmarkNetIPAppendTo (b *testing.B) { for range b.N { NetIPAppendTo() } } func BenchmarkNetIPMarshalText (b *testing.B) { for range b.N { NetIPMarshalText() } } func BenchmarkTimeMarshalText (b *testing.B) { for range b.N { TimeMarshalText() } } func BenchmarkRegexpMarshalText (b *testing.B) { for range b.N { RegexpMarshalText() } }
func_1.24_test.go package mainimport "testing" func BenchmarkNetIPAppendText (b *testing.B) { for range b.N { NetIPAppendText() } } func BenchmarkTimeAppendText (b *testing.B) { for range b.N { TimeAppendText() } } func BenchmarkRegexpAppendText (b *testing.B) { for range b.N { RegexpAppendText() } }
検証結果 以下が結果になります。テストは1000連続関数を実行する処理を1セットとして、それぞれ5回試行を行いました。 表の値は5回の平均値となります。
実行結果ログ
golang1.24rc2実行結果 > go.exe test -bench . -benchmem -count 5 goos: windows goarch: amd64 pkg: appender_bench cpu: Intel(R) Core(TM) Ultra 5 135U BenchmarkNetIPAppendText-14 47016 27704 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendText-14 43274 27663 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendText-14 42195 27916 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendText-14 43123 26976 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendText-14 42027 26224 ns/op 0 B/op 0 allocs/op BenchmarkTimeAppendText-14 15796 78509 ns/op 0 B/op 0 allocs/op BenchmarkTimeAppendText-14 15657 75823 ns/op 0 B/op 0 allocs/op BenchmarkTimeAppendText-14 14817 74074 ns/op 0 B/op 0 allocs/op BenchmarkTimeAppendText-14 15880 78863 ns/op 0 B/op 0 allocs/op BenchmarkTimeAppendText-14 14853 75814 ns/op 0 B/op 0 allocs/op BenchmarkRegexpAppendText-14 257486 4768 ns/op 0 B/op 0 allocs/op BenchmarkRegexpAppendText-14 232052 4731 ns/op 0 B/op 0 allocs/op BenchmarkRegexpAppendText-14 256496 4718 ns/op 0 B/op 0 allocs/op BenchmarkRegexpAppendText-14 233506 4720 ns/op 0 B/op 0 allocs/op BenchmarkRegexpAppendText-14 236343 5156 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 42174 28008 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 42642 28923 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 40321 26834 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 46222 25998 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 46298 26242 ns/op 0 B/op 0 allocs/op BenchmarkNetIPMarshalText-14 19948 58916 ns/op 16000 B/op 1000 allocs/op BenchmarkNetIPMarshalText-14 18686 67485 ns/op 16000 B/op 1000 allocs/op BenchmarkNetIPMarshalText-14 17877 67474 ns/op 16000 B/op 1000 allocs/op BenchmarkNetIPMarshalText-14 17476 67064 ns/op 16000 B/op 1000 allocs/op BenchmarkNetIPMarshalText-14 18458 59124 ns/op 16000 B/op 1000 allocs/op BenchmarkTimeMarshalText-14 14780 81297 ns/op 0 B/op 0 allocs/op BenchmarkTimeMarshalText-14 14739 83955 ns/op 0 B/op 0 allocs/op BenchmarkTimeMarshalText-14 14338 77062 ns/op 0 B/op 0 allocs/op BenchmarkTimeMarshalText-14 15738 86937 ns/op 0 B/op 0 allocs/op BenchmarkTimeMarshalText-14 14629 83365 ns/op 0 B/op 0 allocs/op BenchmarkRegexpMarshalText-14 18852 58042 ns/op 16000 B/op 1000 allocs/op BenchmarkRegexpMarshalText-14 21859 56822 ns/op 16000 B/op 1000 allocs/op BenchmarkRegexpMarshalText-14 21846 53657 ns/op 16000 B/op 1000 allocs/op BenchmarkRegexpMarshalText-14 32487 40737 ns/op 16000 B/op 1000 allocs/op BenchmarkRegexpMarshalText-14 28564 42638 ns/op 16000 B/op 1000 allocs/op PASS ok appender_bench 59.297s
golang1.23.5実行結果 > go.exe test -bench . -benchmem -count 5 goos: windows goarch: amd64 pkg: appender_bench cpu: Intel(R) Core(TM) Ultra 5 135U BenchmarkNetIPAppendTo-14 42561 28971 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 43580 30330 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 39480 30816 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 44533 29090 ns/op 0 B/op 0 allocs/op BenchmarkNetIPAppendTo-14 40624 28365 ns/op 0 B/op 0 allocs/op BenchmarkNetIPMarshalText-14 18144 63204 ns/op 16000 B/op 1000 allocs/op BenchmarkNetIPMarshalText-14 18961 63907 ns/op 16000 B/op 1000 allocs/op BenchmarkNetIPMarshalText-14 18967 65268 ns/op 16000 B/op 1000 allocs/op BenchmarkNetIPMarshalText-14 18006 62820 ns/op 16000 B/op 1000 allocs/op BenchmarkNetIPMarshalText-14 18301 63669 ns/op 16000 B/op 1000 allocs/op BenchmarkTimeMarshalText-14 7777 142480 ns/op 48000 B/op 1000 allocs/op BenchmarkTimeMarshalText-14 8080 135352 ns/op 48000 B/op 1000 allocs/op BenchmarkTimeMarshalText-14 7572 141879 ns/op 48000 B/op 1000 allocs/op BenchmarkTimeMarshalText-14 7544 146622 ns/op 48000 B/op 1000 allocs/op BenchmarkTimeMarshalText-14 8780 146005 ns/op 48000 B/op 1000 allocs/op BenchmarkRegexpMarshalText-14 220700 5176 ns/op 0 B/op 0 allocs/op BenchmarkRegexpMarshalText-14 216325 5317 ns/op 0 B/op 0 allocs/op BenchmarkRegexpMarshalText-14 210703 5404 ns/op 0 B/op 0 allocs/op BenchmarkRegexpMarshalText-14 234162 5148 ns/op 0 B/op 0 allocs/op BenchmarkRegexpMarshalText-14 234038 5581 ns/op 0 B/op 0 allocs/op PASS ok appender_bench 29.149s
net/netip.Addr
time.Time
regexp.Regexp
結果まとめ
net/netip.Addr
AppendText、AppendToではあまり差が見られず、メモリアロケーションは発生しなかった
MarshalTextではメモリアロケーションが毎回発生し、AppendText、AppendToと比較し3倍ほど遅かった
バージョン1.24rc2と1.23.5での際はあまり見られなかった
time.Time
1.24rc2ではAppendTextのほうがややMarshalTextよりも高速だった(ともにメモリアロケーションなし)
MarshalTextは1.24rc2と1.23.5で大きな差異があり、1.23.5ではメモリアロケーションが毎回発生し、速度も1.24rc2より2倍程度遅かった
regexp.Regexp
1.24rc2のAppendTextと1.23.5のMarshalTextではあまり差が見られず、メモリアロケーションは発生しなかった
1.24rc2のMarshalTextは他と比較し10倍程度遅く、また毎回メモリアロケーションが発生した
結果考察 バージョンアップによって結果が悪くなったregexp.Regexp
について考察します。 1.23.5, 1.24rc2のMarshalText, AppendTextの実装は以下のようになっています。
regexp.Regexpの内部処理(1.23.5) func (re *Regexp) MarshalText() ([]byte , error ) { return []byte (re.String()), nil }
regexp.Regexpの内部処理(1.24rc2) func (re *Regexp) AppendText(b []byte ) ([]byte , error ) { return append (b, re.String()...), nil } func (re *Regexp) MarshalText() ([]byte , error ) { return re.AppendText(nil ) }
1.24rc2ではMarshalText
内でAppendText
を利用するように変更になっています。
そのためMarshalText
の処理が実質的に…
return append ([]byte (nil ), []byte (re.String())...), nil
…になります。
1.23.5ではそのままバイトスライスにキャストして返していた処理が、AppendTextを利用するようになりバイトスライスに一旦詰め替える処理変更されたため、遅くなりメモリアロケーションも発生することになったようです。
また、1.24rc2のAppendTextの処理内でre.String()
を呼んでいるため、一旦全量を作成しappendする形になっているため、1.24rc2のAppendText
と1.23.5のMarshalText
の性能が変わらないようです。(re.String
の処理はRegexp.exp値を返しているだけで新規に文字列処理は行っていないため、AppnedTextの恩恵は薄いようです。)
regexp.Regexpの内部処理(*Regexp.String) func (re *Regexp) String() string { return re.expr }
net/netip.Addr
については、1.24で encoding.TextAppender
の形式に対応しましたが、特に性能的な影響なし、time.Time
については、encoding.(Binary|Text)Appender対応コミット(819b1b4 , 2b664d5 )を確認してみましたが、変更前後で性能的な差が無いように見えため本件とは別で機能改善されたようです。
おわりに Go 1.24で新たに追加されたstring, bytes
パッケージの関数、およびencodingパッケージに追加されたインターフェースTextAppender、BinaryAppenderとその実装について扱いました。
Go1.24リリース連載 でGo 1.24でアップデートした他の内容についても解説しているため、ぜひ合わせてご覧ください。