Mit Go 1.24 hat das strings-Paket eine neue Generation von Split-Funktionen bekommen, die statt eines []string einen Iterator vom Typ iter.Seq[string] zurückgeben. strings.FieldsFuncSeq ist dabei das flexibelste Mitglied dieser Familie — es übernimmt das Konzept des klassischen strings.FieldsFunc und kombiniert es mit dem Range-over-Func-Mechanismus aus Go 1.23. Statt einem festen Whitespace-Begriff oder einer fixen Trennzeichen-Menge entscheidet ein benutzerdefiniertes Prädikat func(rune) bool, an welchen Runen aufgespalten wird.
Der entscheidende Vorteil gegenüber FieldsFunc: es entsteht kein zwischenzeitliches Slice. Tokens werden faul, einer nach dem anderen, produziert — und sobald die Range-Schleife per break oder return verlassen wird, hält der Iterator an. Zusätzlich behält die Funktion die „smarte" Semantik ihres Vorbilds bei: aufeinanderfolgende Trennzeichen erzeugen keine leeren Felder, und führende oder schließende Trennzeichen werden ebenfalls geschluckt. Damit eignet sich FieldsFuncSeq ideal für Tokenizer in Parsern, große Textströme oder Hot Paths, in denen Allokationen wehtun.
Die Signatur ist eine direkte Iterator-Übersetzung von FieldsFunc. Statt []string liefert sie eine iter.Seq[string], also genau die Form, die für Range-over-Func gebraucht wird.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
// func FieldsFuncSeq(s string, f func(rune) bool) iter.Seq[string]
text := "Hallo, Welt! Wie geht's?"
isSep := func(r rune) bool {
return unicode.IsSpace(r) || unicode.IsPunct(r)
}
for token := range strings.FieldsFuncSeq(text, isSep) {
fmt.Printf("%q\n", token)
}
}"Hallo"
"Welt"
"Wie"
"geht"
"s"Der Parameter s ist der Eingabe-String, f ist das Prädikat. FieldsFuncSeq scannt s rune-weise und liefert genau dann ein neues Token, wenn zwischen zwei Trenner-Runen mindestens eine Nicht-Trenner-Rune liegt. Das Prädikat wird für jede Rune genau einmal aufgerufen, in der Reihenfolge, in der die Runen im String stehen. Es darf zustandsbehaftet sein, sollte aber in der Praxis möglichst rein bleiben — das macht Tests einfacher und schließt subtile Reentrancy-Probleme aus.
Bei FieldsFuncSeq herrscht dieselbe Konvention wie bei FieldsFunc: liefert f(r) den Wert true, gilt r als Trennzeichen und gehört zu keinem Token. Liefert f(r) den Wert false, ist die Rune ein gültiger Bestandteil eines Tokens. Diese Polarität ist die häufigste Fehlerquelle — wer ein Prädikat aus einer Whitelist baut, muss das Ergebnis negieren.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
s := "id_42; name=Anna; city=Bonn"
// Trenner: alles, was weder Buchstabe noch Ziffer ist
notAlphaNum := func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
}
for tok := range strings.FieldsFuncSeq(s, notAlphaNum) {
fmt.Println(tok)
}
}id
42
name
Anna
city
BonnDas Beispiel zeigt einen weiteren wichtigen Punkt: die Prädikat-Logik bestimmt vollständig, was als „zusammengehörig" gilt. Wer Unterstriche als Wortbestandteil behalten will, muss sie im Prädikat explizit ausnehmen — r == '_' als Sonderfall mit false. Ohne diese Ausnahme zerfällt id_42 in zwei Felder.
Die Schwesterfunktion strings.FieldsSeq arbeitet ebenfalls als Iterator, hat aber ein festverdrahtetes Prädikat: unicode.IsSpace. Sie ist damit der direkte Iterator-Pendant zu strings.Fields. FieldsFuncSeq lässt sich als Verallgemeinerung verstehen — alles, was FieldsSeq kann, kann FieldsFuncSeq auch, aber mit dem Preis eines zusätzlichen Funktionsaufrufs pro Rune.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
s := " rot gruen\tblau\n"
fmt.Println("FieldsSeq:")
for f := range strings.FieldsSeq(s) {
fmt.Printf(" %q\n", f)
}
fmt.Println("FieldsFuncSeq:")
for f := range strings.FieldsFuncSeq(s, unicode.IsSpace) {
fmt.Printf(" %q\n", f)
}
}FieldsSeq:
"rot"
"gruen"
"blau"
FieldsFuncSeq:
"rot"
"gruen"
"blau"Die Faustregel ist einfach: solange das Trennkriterium „irgendeine Form von Whitespace" ist, ist FieldsSeq schneller und klarer. Sobald das Kriterium spezifisch wird — Satzzeichen, Custom-Delimiter, kontextabhängige Regeln —, ist FieldsFuncSeq die richtige Wahl. Beide Funktionen teilen sich die Smart-Skip-Semantik: leere Tokens kommen nie vor.
Das Prädikat erhält eine rune (also einen Unicode-Codepoint), nicht ein einzelnes Byte. Damit ist FieldsFuncSeq UTF-8-sicher und behandelt Mehrbyte-Zeichen korrekt. Bytes wie 0xC3 aus einer UTF-8-Sequenz tauchen im Prädikat nie auf — die Funktion dekodiert intern.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
// Zeichen mit Diakritika, Emojis, CJK
s := "Café · 寿司 · Smørrebrød · 🍣sushi"
isSep := func(r rune) bool {
return !unicode.IsLetter(r) && r != '\''
}
for tok := range strings.FieldsFuncSeq(s, isSep) {
fmt.Printf("%q\n", tok)
}
}"Café"
"寿司"
"Smørrebrød"
"sushi"Wer mit Bytes statt Runen splitten muss — etwa in einem reinen ASCII-Protokoll, in dem jeder Byte-Wert genau ein Zeichen ist —, sollte trotzdem nicht versuchen, das Prädikat zu „casten". Eine ungültige UTF-8-Sequenz erzeugt eine Ersatzrune (utf8.RuneError), und das Prädikat sieht diese — nicht das ursprüngliche Byte. Für solche Fälle ist bytes.FieldsFunc der passendere Ansatz.
Der größte Vorteil gegenüber strings.FieldsFunc zeigt sich beim frühzeitigen Abbrechen. FieldsFunc alloziert ein komplettes []string und scannt den gesamten String, auch wenn nur das erste Token gebraucht wird. FieldsFuncSeq produziert Tokens faul — ein break stoppt sowohl die Schleife als auch das Scannen.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
log := "ERROR 2026-05-25T10:30:42Z user=42 path=/api ..."
// Wir wollen nur das erste Wort — den Log-Level
for level := range strings.FieldsFuncSeq(log, unicode.IsSpace) {
fmt.Println("level:", level)
break // bricht den Iterator sofort ab
}
}level: ERRORIn einem Log-Aggregator, der pro Zeile nur das Level entscheidet, summiert sich diese Ersparnis: keine Slice-Allokation, kein Scannen unnötiger Tokens. Bei langen Zeilen und hohen Durchsatzraten ist das ein messbarer Faktor — und der Code bleibt trotzdem lesbar.
Wenn s leer ist oder ausschließlich aus Trenner-Runen besteht, liefert der Iterator schlicht keine Werte. Die Range-Schleife läuft also null Mal — es gibt keinen „leeren String" als Token. Das ist konsistent mit FieldsFunc, aber ein häufiger Stolperstein für Entwickler, die das Verhalten von strings.Split gewohnt sind.
package main
import (
"fmt"
"strings"
)
func main() {
isComma := func(r rune) bool { return r == ',' }
cases := []string{"", ",", ",,,,", ",a,", "a,,b"}
for _, c := range cases {
var out []string
for tok := range strings.FieldsFuncSeq(c, isComma) {
out = append(out, tok)
}
fmt.Printf("%-8q -> %q\n", c, out)
}
}"" -> []
"," -> []
",,,," -> []
",a," -> ["a"]
"a,,b" -> ["a" "b"]Diese Smart-Skip-Semantik ist Stärke und Schwäche zugleich. Für Word-Tokenizer ist sie genau richtig. Für CSV-Parsing, bei dem leere Felder eine eigene Bedeutung haben (a,,b sollte drei Spalten ergeben), ist sie ungeeignet — hier ist strings.SplitSeq die passende Wahl.
Ein häufiger Real-World-Fall: eine Konfigurationszeile mit gemischten Trennzeichen — Kommas, Semikolons, Leerzeichen, Tabs. Statt mit strings.NewReplacer vorzubehandeln, lässt sich ein einziges Prädikat formulieren, das alle Varianten gleichzeitig akzeptiert.
package main
import (
"fmt"
"strings"
)
func main() {
raw := "redis-1 ; redis-2,redis-3\tredis-4 redis-5"
isDelim := func(r rune) bool {
switch r {
case ',', ';', ' ', '\t':
return true
}
return false
}
var hosts []string
for h := range strings.FieldsFuncSeq(raw, isDelim) {
hosts = append(hosts, h)
}
fmt.Printf("%d Hosts: %v\n", len(hosts), hosts)
}5 Hosts: [redis-1 redis-2 redis-3 redis-4 redis-5]Beachtenswert: Mehrfach-Delimiter wie , (Komma plus Leerzeichen) werden korrekt als ein einziger Trennblock behandelt — keine leeren Tokens, kein Trimmen nötig. Genau dafür ist die Smart-Skip-Semantik gemacht. Bei strenger CSV mit signifikanten Leerfeldern wäre dieser Code falsch — dort braucht es einen echten CSV-Parser oder SplitSeq.
Ein klassischer Anwendungsfall ist ein einfacher Lexer, der Bezeichner aus einem Code-Schnipsel extrahiert. Trenner sind alle Runen, die weder Buchstabe noch Ziffer noch Unterstrich sind. Mit FieldsFuncSeq lässt sich der Lexer streamen — ideal, wenn nur die ersten paar Bezeichner für eine Heuristik gebraucht werden.
package main
import (
"fmt"
"sort"
"strings"
"unicode"
)
func main() {
src := "func Add(a, b int) int { return a + b }"
isWordSep := func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_'
}
seen := map[string]int{}
for ident := range strings.FieldsFuncSeq(src, isWordSep) {
seen[ident]++
}
keys := make([]string, 0, len(seen))
for k := range seen {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%-8s x%d\n", k, seen[k])
}
}Add x1
a x2
b x2
func x1
int x2
return x1In einer echten Codebasis würde man das mit go/scanner oder einem dedizierten Lexer machen — der Aufwand für korrekte Stringliterale, Kommentare und Schlüsselwörter ist beträchtlich. Aber für eine schnelle Heuristik (etwa: „kommt der Bezeichner TODO in diesem File vor?") ist FieldsFuncSeq mit einem schlanken Prädikat oft die richtige Balance aus Aufwand und Genauigkeit.
Iterator-Variante von FieldsFunc
FieldsFuncSeq ist die iter.Seq[string]-Form von FieldsFunc — gleiche Token-Folge, aber lazy und ohne Slice-Allokation.
Prädikat-Polarität
f(r) == true heißt „trennen", false heißt „Teil des Tokens". Wer aus einer Whitelist denkt, muss negieren.
Smart-Skip aktiv
Aufeinanderfolgende Trenner und führende/schließende Trenner erzeugen keine leeren Tokens — anders als SplitSeq.
Rune-basiert
Das Prädikat bekommt rune, nicht byte. UTF-8 wird intern dekodiert, Mehrbyte-Zeichen funktionieren transparent.
Early break spart Arbeit
break in der Range-Schleife stoppt sowohl Konsum als auch Scannen — ideal für „nur erstes Token"-Pfade.
Verallgemeinerung von FieldsSeq
FieldsSeq ist FieldsFuncSeq mit unicode.IsSpace als festem Prädikat — letzteres ist flexibler, ersteres minimal schneller.
Materialisierung über slices.Collect
Wer doch ein []string braucht, ruft slices.Collect(strings.FieldsFuncSeq(...)) — Wahlfreiheit zwischen lazy und eager.
Nicht für signifikante Leerfelder
Sobald leere Felder Bedeutung haben (echtes CSV, fixe Spaltenzahl), ist SplitSeq die richtige Wahl — FieldsFuncSeq schluckt sie.