strings.SplitAfterSeq ist mit Go 1.24 in die Standardbibliothek gekommen und ergänzt die klassischen Split-Funktionen um eine Iterator-Variante, die — anders als SplitSeq — den Separator am Ende jedes Substrings erhält. Damit reiht sie sich in die Familie der neuen iter.Seq-Splitter ein (SplitSeq, SplitAfterSeq, FieldsSeq, FieldsFuncSeq, Lines), die das alte []string-Allokationsmodell durch lazy Range-over-Func-Iteration ersetzen.

Der entscheidende Unterschied zu SplitSeq: bei SplitAfterSeq ist der Separator nicht verloren, sondern wandert in den vorangehenden Substring. Das macht die Iteration verlustfrei — die Konkatenation aller yieldenden Substrings ergibt wieder die Original-Eingabe. Für Tokenizer, Streaming-Parser und alle Aufgaben, bei denen Trennzeichen Teil der Bedeutung sind (Zeilenenden, Satzzeichen, Frame-Marker), ist das die natürliche Wahl.

Die Signatur folgt dem Muster der neuen Seq-Splitter: zwei Strings rein, ein iter.Seq[string] raus. Die Rückgabe ist kein Slice, sondern eine Funktion, die bei Iteration die Substrings sukzessive yieldet.

Go signatur.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	for part := range strings.SplitAfterSeq("a,b,c,", ",") {
		fmt.Printf("%q\n", part)
	}
}
Output
"a,"
"b,"
"c,"
""

iter.Seq[string] ist ein Alias für func(yield func(string) bool) aus dem Paket iter. Der Iterator wird mit range konsumiert. Im Gegensatz zur slicebasierten strings.SplitAfter wird nichts vorab allokiert — die Substrings entstehen erst bei jedem Iterationsschritt als String-Header auf das ursprüngliche Backing-Array. Beachte den leeren Substring am Ende: wenn die Eingabe mit dem Separator endet, yieldet der Iterator einen leeren String — exakt wie es SplitAfter mit Slices auch tut. Das ist konsistent und macht die Roundtrip-Eigenschaft erst möglich.

Die beiden Iteratoren sehen auf den ersten Blick gleich aus, unterscheiden sich aber in einem zentralen Punkt: SplitSeq verwirft den Separator, SplitAfterSeq hängt ihn an den vorangehenden Substring.

Go vergleich.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "alpha,beta,gamma"

	fmt.Println("SplitSeq:")
	for p := range strings.SplitSeq(s, ",") {
		fmt.Printf("  %q\n", p)
	}

	fmt.Println("SplitAfterSeq:")
	for p := range strings.SplitAfterSeq(s, ",") {
		fmt.Printf("  %q\n", p)
	}
}
Output
SplitSeq:
  "alpha"
  "beta"
  "gamma"
SplitAfterSeq:
  "alpha,"
  "beta,"
  "gamma"

Praktisch heißt das: wenn du nur Werte zwischen Trennern brauchst (CSV-Felder, Pfad-Komponenten, Tags), nimm SplitSeq. Wenn der Separator semantisch wichtig ist — etwa beim Erhalten von Zeilenumbrüchen, Satzzeichen oder Frame-Begrenzern — ist SplitAfterSeq korrekt.

Die wichtigste mentale Eigenschaft von SplitAfterSeq: die Iteration ist verlustfrei. Konkatenierst du alle yieldenden Substrings, bekommst du Bit für Bit die Eingabe zurück. Das ist eine starke Invariante, die du in Tests und beim Refactoring nutzen kannst.

Go roundtrip.go
package main

import (
	"fmt"
	"strings"
)

func roundtrip(s, sep string) bool {
	var b strings.Builder
	for part := range strings.SplitAfterSeq(s, sep) {
		b.WriteString(part)
	}
	return b.String() == s
}

func main() {
	fmt.Println(roundtrip("foo\nbar\nbaz", "\n"))
	fmt.Println(roundtrip("foo\nbar\nbaz\n", "\n"))
	fmt.Println(roundtrip("", "x"))
}
Output
true
true
true

Bei SplitSeq gilt diese Eigenschaft nicht — dort musst du den Separator beim Join wieder einsetzen, was bei mehrzeichigen oder gemischten Separatoren fehleranfällig wird. SplitAfterSeq schiebt diese Komplexität in den Iterator hinein.

Wie alle Range-over-Func-Iteratoren respektiert SplitAfterSeq ein break aus der Range-Schleife. Dadurch eignet es sich für Streaming-Szenarien, in denen du nur die ersten N Tokens brauchst — der Rest der Eingabe wird gar nicht erst gescannt.

Go early_break.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	bigLog := "INFO startup\nWARN slow\nERROR timeout\nINFO ok\nINFO done\n"

	count := 0
	for line := range strings.SplitAfterSeq(bigLog, "\n") {
		if count >= 3 {
			break
		}
		fmt.Print(line) // line enthält "\n" am Ende
		count++
	}
}
Output
INFO startup
WARN slow
ERROR timeout

Im Vergleich dazu allokiert strings.SplitAfter(bigLog, "\n") immer den kompletten Slice mit allen Zeilen, egal ob du eine oder eine Million davon brauchst. Bei großen Logs oder Streaming-Dateien spart das nicht nur Allokationen, sondern auch wirklich gemessene CPU-Zeit für das vollständige Scannen.

Beide Funktionen produzieren semantisch dieselben Substrings — die Frage ist nur, wie sie geliefert werden. SplitAfter baut einen []string, SplitAfterSeq einen Iterator.

Go splitafter_vs_seq.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "a,b,c,"

	// Klassisch: ganzer Slice im Speicher
	parts := strings.SplitAfter(s, ",")
	fmt.Printf("Slice (%d Elemente): %q\n", len(parts), parts)

	// Iterator: lazy, eine Zuweisung pro Schritt
	fmt.Println("Iterator:")
	for p := range strings.SplitAfterSeq(s, ",") {
		fmt.Printf("  %q\n", p)
	}
}
Output
Slice (4 Elemente): ["a," "b," "c," ""]
Iterator:
  "a,"
  "b,"
  "c,"
  ""

Faustregel: wenn du den Index brauchst, mehrfach iterieren willst oder ohnehin alle Teile gleichzeitig im Speicher haben musst (z. B. für sort.Strings), nimm SplitAfter. Wenn du einmal linear durchgehst und ggf. früh abbrechst, ist SplitAfterSeq strikt überlegen — keine Slice-Header-Allokation, keine Capacity-Verschwendung.

Ein klassischer Anwendungsfall ist das Lesen mehrzeiliger Logs, bei denen die Zeilenumbrüche nicht verloren gehen sollen — etwa für ein Audit-Log, das später bitgenau wieder geschrieben werden muss.

Go log_filter.go
package main

import (
	"fmt"
	"strings"
)

func filterErrors(log string) string {
	var b strings.Builder
	b.Grow(len(log))
	for line := range strings.SplitAfterSeq(log, "\n") {
		if strings.Contains(line, "ERROR") {
			b.WriteString(line) // Newline ist schon dabei
		}
	}
	return b.String()
}

func main() {
	log := "INFO startup\nERROR db timeout\nINFO retry\nERROR write fail\n"
	fmt.Print(filterErrors(log))
}
Output
ERROR db timeout
ERROR write fail

Vorteil gegenüber SplitSeq + manuelles b.WriteByte('\n'): du musst dir nicht merken, ob die letzte Zeile einen Newline hatte oder nicht. Wenn die Original-Eingabe ohne \n endete, endet auch die letzte gefilterte Zeile ohne \n — das Verhalten ist automatisch korrekt. Bei strings.Lines (ebenfalls Go 1.24) ist das ähnlich, aber dort sind die Separatoren fest auf Zeilenumbrüche beschränkt.

Bei einfachen Wire-Protokollen oder selbstgebauten Frame-Formaten ist der Separator oft ein Marker, den du beim Re-Encoding wieder brauchst. SplitAfterSeq macht das natürlich.

Go frames.go
package main

import (
	"fmt"
	"strings"
)

// Frame-Format: "<payload>;\n<payload>;\n..."
func forwardFrames(stream string) string {
	var out strings.Builder
	for frame := range strings.SplitAfterSeq(stream, ";\n") {
		if frame == "" {
			continue // letzter leerer Part bei trailing Separator
		}
		out.WriteString(frame)
	}
	return out.String()
}

func main() {
	in := "alpha=1;\nbeta=2;\ngamma=3;\n"
	fmt.Print(forwardFrames(in))
}
Output
alpha=1;
beta=2;
gamma=3;

Wichtig ist hier das Abfangen des leeren letzten Substrings, der entsteht, wenn der Stream regulär mit dem Marker endet. Das ist kein Bug, sondern die korrekte Konsequenz aus der Roundtrip-Property — würde der Iterator den leeren Teil unterdrücken, ginge die Information „Stream endete sauber" verloren.

iter.Seq[string]

SplitAfterSeq liefert keinen Slice, sondern einen iter.Seq[string] — konsumiert per range, lazy ausgewertet, ohne Vorab-Allokation aller Teile.

Separator bleibt

Im Gegensatz zu SplitSeq hängt SplitAfterSeq den Separator an das Ende jedes Substrings an — Trennzeichen gehen nicht verloren.

Roundtrip

Die Konkatenation aller yieldenden Substrings ergibt wieder die Original-Eingabe — verlustfreies Splitting per Konstruktion.

Leerer letzter Teil

Endet die Eingabe mit dem Separator, yieldet der Iterator einen leeren String am Schluss — konsistent mit SplitAfter und wichtig für die Roundtrip-Property.

Early break

break aus der Range-Schleife stoppt das Scannen sofort — der Rest der Eingabe wird nicht mehr betrachtet, ideal für Streams.

vs. SplitAfter

Semantisch identisch, aber ohne Slice-Allokation — bevorzugt bei linearem Durchlauf und großen Eingaben.

Go 1.24

Teil der Range-over-Func-Familie der stdlib-Erweiterung in Go 1.24, zusammen mit SplitSeq, FieldsSeq, FieldsFuncSeq, Lines.

Wann nehmen

Immer dann, wenn der Separator semantische Bedeutung hat — Zeilenumbrüche, Satzzeichen, Frame-Marker — und beim Re-Encoding erhalten bleiben muss.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Das strings-Paket — String-Manipulation

Zur Übersicht