フューチャー技術ブログ

Go 1.24 リリース連載 strings関数 + encoding.TextAppender

はじめに

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 main

import (
"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)
}
// --- Lines ---
// print: "aaa\n"
// print: "bbb\n"
// print: "ccc"

fmt.Println("--- SplitSeq ---")
for v := range strings.SplitSeq(str, ",") {
fmt.Printf("print: %q\n", v)
}
// --- SplitSeq ---
// print: "aaa"
// print: "bbb"
// print: "ccc"

fmt.Println("--- SplitAfterSeq ---")
for v := range strings.SplitAfterSeq(str, ",") {
fmt.Printf("print: %q\n", v)
}
// --- SplitAfterSeq ---
// print: "aaa,"
// print: "bbb,"
// print: "ccc"

fmt.Println("--- FieldsSeq ---")
for v := range strings.FieldsSeq(str2) {
fmt.Printf("print: %q\n", v)
}
// --- FieldsSeq ---
// print: "aaa"
// print: "bbb"
// print: "ccc"

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)
}
// --- FieldsFuncSeq ---
// print: "aaa"
// print: "bbb"
// print: "ccc"
}

それぞれの関数の特徴をまとめると以下のようになります。

関数 区切り文字 区切り文字の除去有無
Lines \n (改行コード) 残す
SplitSeq 第2引数で指定した文字 除去する
SplitAfterSeq 第2引数で指定した文字 残す
FieldsSeq スペース (unicode.IsSpace) 除去する
FieldsFuncSeq 第2引数で指定した関数で
trueになったrune
除去する

既存パッケージ、関数との関係

返り値が[]stringのものがありましたが、今回新規に iter.Seq の返り値の関数が追加された形になります。
iter パッケージは1.23で新規に追加されたパッケージです。iter パッケージについては1.23リリースでの弊社解説記事をご参照ください。

1.24新規関数 []string返り値の対応関数
Lines (なし)
SplitSeq Split
SplitAfterSeq SplitAfter
FieldsSeq Fields
FieldsFuncSeq FieldsFunc

iter.Seqを返すことになったことによりrangeでのループ処理でインデックスの返り値が不要になりました。

strings.Split, strings.SplitSeqそれぞれのループサンプル
package main

import (
"fmt"
"strings"
)

func main() {
str := "aaa,bbb,ccc"

fmt.Println("--- Split ---")
for _, v := range strings.Split(str, ",") {
fmt.Printf("print: %q\n", v)
}
// --- Split ---
// print: "aaa"
// print: "bbb"
// print: "ccc"

fmt.Println("--- SplitSeq ---")
for v := range strings.SplitSeq(str, ",") {
fmt.Printf("print: %q\n", v)
}
// --- SplitSeq ---
// print: "aaa"
// print: "bbb"
// print: "ccc"
}

また、iter.Seqが返り値のため、iter.Pullを利用することで next()の形でループ処理をすることも可能です。

iter.Pullを利用したnext()パターンのループ処理
package main

import (
"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)
}
// print: "aaa"
// print: "bbb"
// print: "ccc"
}

encoding.TextAppender, encoding.BinaryAppender

1.24からencodingパッケージに encoding.TextAppender, encoding.BinaryAppenderのインターフェースが追加されました。

TextAppender, BinaryAppenderインターフェース
type TextAppender interface {
// AppendText appends the textual representation of itself to the end of b
// (allocating a larger slice if necessary) and returns the updated slice.
//
// Implementations must not retain b, nor mutate any bytes within b[:len(b)].
AppendText(b []byte) ([]byte, error)
}

type BinaryAppender interface {
// AppendBinary appends the binary representation of itself to the end of b
// (allocating a larger slice if necessary) and returns the updated slice.
//
// Implementations must not retain b, nor mutate any bytes within b[:len(b)].
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/netipAddr内部処理を見てみると、以下のように元の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 main

import (
"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+$`)
)

// NetIPAppendTo は net/netip.*Addr.NetIPAppendToを利用した処理
// AppendTextのエイリアスもとだが対照として追加
func NetIPAppendTo() {
b := make([]byte, 0, 15000)

for range 1000 {
b = addr.AppendTo(b)
}
}

// NetIPMarshalText は time.*Time.MarshalText (TextMarshaler)を利用した処理
func NetIPMarshalText() {
b := make([]byte, 0, 15000)

for range 1000 {
ba, err := addr.MarshalText()
if err != nil {
panic(err)
}
b = append(b, ba...)
}
}

// TimeMarshalText は time.*Time.MarshalText (TextMarshaler)を利用した処理
func TimeMarshalText() {
b := make([]byte, 0, 35000)

for range 1000 {
ba, err := tm.MarshalText()
if err != nil {
panic(err)
}
b = append(b, ba...)
}
}

// RegexpMarshalText は regexp.*Regexp.MarshalText (TextMarshaler)を利用した処理
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
//go:build go1.24

// golang 1.24 のみ実行

package main

// NetIPAppendText は net/netip.*Addr.AppendText (TextAppender)を利用した処理
func NetIPAppendText() {
b := make([]byte, 0, 15000)

var err error
for range 1000 {
b, err = addr.AppendText(b)
if err != nil {
panic(err)
}
}
}

// TimeAppendText は time.*Time.AppendText (TextAppender)を利用した処理
func TimeAppendText() {
b := make([]byte, 0, 35000)

var err error
for range 1000 {
b, err = tm.AppendText(b)
if err != nil {
panic(err)
}
}
}

// RegexpAppendText は regexp.*Regexp.AppendText (TextAppender)を利用した処理
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 main

import "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
//go:build go1.24

// golang 1.24 のみ実行

package main

import "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

項目 1.24rc2
AppendText
1.24rc2
AppendTo
1.24rc2
MarshalText
1.23.5
AppendTo
1.23.5
MarshalText
実行速度
(ns/op)
10815.6 12484.8 37264.0 10382.4 29839.2
アロケーション
サイズ
(B/op)
0 0 16000 0 16000
アロケーション
回数
(allocs/op)
0 0 1000 0 1000

time.Time

項目 1.24rc2
AppendText
1.24rc2
MarshalText
1.23.5
MarshalText
実行速度
(ns/op)
40889.4 47967.8 79187.2
アロケーション
サイズ
(B/op)
0 0 48000
アロケーション
回数
(allocs/op)
0 0 1000

regexp.Regexp

項目 1.24rc2
AppendText
1.24rc2
MarshalText
1.23.5
MarshalText
実行速度
(ns/op)
2384.6 30566.8 2557.0
アロケーション
サイズ
(B/op)
0 16000 0
アロケーション
回数
(allocs/op)
0 1000 0

結果まとめ

  • 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)
// MarshalText implements [encoding.TextMarshaler]. The output
// matches that of calling the [Regexp.String] method.
//
// Note that the output is lossy in some cases: This method does not indicate
// POSIX regular expressions (i.e. those compiled by calling [CompilePOSIX]), or
// those for which the [Regexp.Longest] method has been called.
func (re *Regexp) MarshalText() ([]byte, error) {
return []byte(re.String()), nil
}
regexp.Regexpの内部処理(1.24rc2)
// AppendText implements [encoding.TextAppender]. The output
// matches that of calling the [Regexp.String] method.
//
// Note that the output is lossy in some cases: This method does not indicate
// POSIX regular expressions (i.e. those compiled by calling [CompilePOSIX]), or
// those for which the [Regexp.Longest] method has been called.
func (re *Regexp) AppendText(b []byte) ([]byte, error) {
return append(b, re.String()...), nil
}

// MarshalText implements [encoding.TextMarshaler]. The output
// matches that of calling the [Regexp.AppendText] method.
//
// See [Regexp.AppendText] for more information.
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)
// String returns the source text used to compile the regular expression.
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でアップデートした他の内容についても解説しているため、ぜひ合わせてご覧ください。