今回は Go 1.24 で bytes / strings に追加されたイテレータ関数群を試してみたいと思います。
Go 1.22 で導入されたイテレータ仕様 (iter.Seq) を活用しており、for-range に直接渡せるようになっています。
従来の Split 系関数が []string を一括で返していたのに対し、1要素ずつ呼び出し元に渡す設計になっているのが主な変更点です。
| 関数 | パッケージ | 説明 | 旧来の相当関数 |
|---|---|---|---|
Lines(s) | strings | 改行区切りで各行を yield する | Split(s, "\n") |
SplitSeq(s, sep) | strings | sep で分割して yield する | Split(s, sep) |
SplitAfterSeq(s, sep) | strings | sep を含んだまま分割して yield する | SplitAfter(s, sep) |
FieldsSeq(s) | strings | 空白文字で分割して yield する | Fields(s) |
FieldsFuncSeq(s, f) | strings | 任意の条件 (func) で分割して yield する | FieldsFunc(s, f) |
Lines(b) | bytes | 改行区切りで各行を yield する ([]byte 版) | Split(b, []byte("\n")) |
SplitSeq(b, sep) | bytes | sep で分割して yield する ([]byte 版) | Split(b, sep) |
iter.Seq とはiter.Seq[V] は func(yield func(V) bool) という関数型のエイリアスで Go 1.23 で導入されており、yield に値を渡すことで呼び出し元の for-range ループへ 1件ずつ届ける仕組みになっています。
var seq iter.Seq[string] = func(yield func(string) bool) {
for _, s := range []string{"a", "b", "c"} {
if !yield(s) {
return // yield が false を返したら break された合図
}
}
}
for s := range seq {
fmt.Println(s) // a, b, c
}
break するとイテレータ側の yield が false を返し、処理を止められます。
// yield: "a"
// yield: "b"
// break を検知 → イテレータ終了 ← "c" 以降は yield すら呼ばれない
strings.Lines はこのイテレータ仕様を使い、実際には次のように実装されています。※github - golang
func Lines(s string) iter.Seq[string] {
return func(yield func(string) bool) {
for len(s) > 0 {
var line string
if i := IndexByte(s, '\n'); i >= 0 {
line, s = s[:i+1], s[i+1:]
} else {
line, s = s, ""
}
if !yield(line) {
return
}
}
}
}
strings.Split はセパレータ (第 2 引数で指定する区切り文字) を取り除いた各要素を []string として返します。
一方、strings.Lines は改行文字 \n を各行の末尾に含んだまま yield する点が大きな違いです。
s := "apple\nbanana\ncherry"
// 旧: strings.Split — セパレータは除去される
got := strings.Split(s, "\n")
// → ["apple" "banana" "cherry"]
// 新: strings.Lines — 改行文字を含んで yield する
for line := range strings.Lines(s) {
fmt.Print(line)
}
// → "apple\n" "banana\n" "cherry"
補足: strings.Lines は \n を含んだまま yield しますが、改行文字を除いた値が欲しい場合は strings.TrimRight(line, "\n") で除去できます。
strings.Split で末尾に \n がある文字列を分割すると、空文字列が末尾に入ってしまいます。テキストファイルの末尾には改行がつくことが多いため、この挙動は意図しないバグを生みやすいです。
s := "first\nsecond\nthird\n"
strings.Split(s, "\n")
// 行数: 4
// 内容: ["first" "second" "third" ""] ← 末尾に空文字列が入る
strings.Lines はこの問題がありません。末尾の空行は yield されず、得られる行数は直感と一致します。
for line := range strings.Lines(s) { ... }
// 行数: 3
// 内容: ["first\n" "second\n" "third\n"] ← 空文字列なし
そのため strings.Split で末尾改行を正しく扱うには TrimRight との組み合わせが必要でした。
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
strings.Lines はこの処理が内部に組み込まれており、呼び出し側で特別な前処理を書く必要がないため、この点は使い勝手が良くなったように思えます。
strings.Split はループ開始前に全行分の []string を一度に確保します。つまり最初の 2 行しか処理しない場合でも、残り全行のメモリは確保済みになります。
strings.Lines はイテレータなので、break した時点で残りの行は yield すら行われません。よって、大きなテキストを部分的に処理したい場合に、無駄なメモリ確保を避けられるようにもなるかと思えます。
s := "line1\nline2\nline3\nline4\nline5\n"
// 旧: strings.Split → ループ前に全行分の []string がすでに確保される
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
// ループ前に確保済みの行数: 5
// 確保されたが処理しなかった行: [line3 line4 line5]
// 新: strings.Lines → break した時点で残りは yield されない
for line := range strings.Lines(s) {
if len(processed) == 2 {
break
}
}
// 実際に処理した行: [line1 line2]
// line3〜line5 は yield すら行われていない
strings.Lines の主な価値は「必要な分だけ処理できる」点にあります。
全行を処理する場合はどちらを使っても結果は同じですが、早期終了が発生しうるケースでは strings.Lines のほうがメモリ効率で有利なように思えます。
空白文字 (スペース・タブ・改行など) を区切りとして分割します。連続した空白はまとめて 1 つの区切りとして扱われるため、先頭・末尾の空白も自動で除かれます。
s := " foo bar\tbaz "
// 旧: strings.Fields — 空白区切りの []string を返す
strings.Fields(s)
// 内容: ["foo" "bar" "baz"]
// 新: strings.FieldsSeq — 空白区切りで 1要素ずつ yield する
for field := range strings.FieldsSeq(s) { ... }
// 内容: ["foo" "bar" "baz"]
strings.Split は連続した区切り文字の間に空文字列を生成しますが、strings.FieldsSeq は生成しません。これは strings.Fields の従来の挙動を引き継いでいます。
// strings.Split: 連続する区切り文字の間に空文字列が入る
strings.Split("a,,b,,c", ",")
// 要素数: 5 内容: ["a" "" "b" "" "c"]
// strings.FieldsSeq: 連続する空白をまとめて区切るため空文字列なし
strings.FieldsSeq("a b c")
// 要素数: 3 内容: ["a" "b" "c"]
strings.Split でセパレータを固定して分割する場合と異なり、strings.FieldsSeq は連続する空白をまとめて 1 つの区切りとして扱うため、余分な空文字列が混入しません。
strings.SplitSeq の []byte 版です。セパレータで分割した各要素を yield します。
結果は bytes.Split と同じですが、[][]byte を事前確保しません。
b := []byte("a,b,c,d")
// 旧: bytes.Split — 全要素の [][]byte を一度に返す
bytes.Split(b, []byte(","))
// 内容: ["a" "b" "c" "d"]
// 新: bytes.SplitSeq — 1要素ずつ yield する
for part := range bytes.SplitSeq(b, []byte(",")) { ... }
// 内容: ["a" "b" "c" "d"]
イテレータ版は「必要な分だけ処理できる」点が最大の強みのように見えます。
全要素を処理しきる場合は従来の関数とほぼ同等ですが、早期終了が絡むケースではパフォーマンスにそこそこの差が出ると考えられます。
| 状況 | 有利な方 |
|---|---|
| 途中で break する | イテレータ版 (strings.Lines など) |
| 巨大な文字列を扱う | イテレータ版 |
| 全行処理する | ほぼ同等 |
| インデックスアクセスが必要 | 従来版 (strings.Split など) |
インデックスでランダムアクセスしたい、あるいは要素数を事前に知りたい場合は従来の []string を返す関数が適しています。逆に先頭から順番に処理するだけであれば、イテレータ版を選ぶ理由が十分にあります。
Go 1.23 のイテレータ仕様が標準ライブラリに浸透してきた例として、今回の bytes / strings の更新はわかりやすいと思います。
strings.Split の末尾改行問題や TrimRight の必要性など、従来の小さな不便が Lines 系の関数で解消されているのは派手ではないものの実用的な改善だと感じました。
Go 1.24で追加されたtesting.B.Loopは、従来のb.Nループより簡潔で安全なベンチマーク記述方法。b.ResetTimer()が不要で、初期化処理が1回のみ実行され、b.N参照漏れやResetTimer忘れなどの問題を根本的に解決する。
Grafana × Alloy × Tempoを使用したOpenTelemetryのトレース検証。Grafanaで15件のリクエストから生成されたトレースを確認し、http-server、ArticleUsecase、ArticleRepositoryの3つのspanの階層構造と処理時間の内訳を分析。DB処理3%、usecase処理15%、HTTP処理82%の結果から、ボトルネック特定の有効性を実証。
論文PDFを構造化JSONに変換するパイプラインの経路B(画像をBedrockに直接読ませる方式)について、Step Functionsでページを並列処理し、各Lambdaがページ画像からモデルで構造化データを抽出、S3に保存、最後にfinalizerで結合・正規化・検証する流れを詳説。
論文PDFを構造化JSONに変換するパイプラインの経路A(Textract+Bedrock)について、各ステップの設計理由と実装を詳細に解説。出力上限問題への対処として、モデルに本文を書かせず位置情報のみ返させることで、生成時間を半減させた改善事例を紹介。
VS Code の PostgreSQL 拡張機能を使用して、ベクトル検索を用いた関連記事レコメンド機能の SQL をEXPLAIN分析し、CTE を活用して絞り込みを早期化することで実行時間を約65%削減した改善事例。