strings.SplitAfter zerlegt einen String an jedem Vorkommen eines Trenners — mit einem entscheidenden Unterschied zu Split: Der Trenner verschwindet nicht, sondern bleibt am Ende des jeweils vorangehenden Stücks erhalten. Wer eine Datei zeilenweise verarbeitet und dabei wissen muss, ob die Zeile mit \n, \r\n oder ganz ohne Zeilenumbruch endete, bekommt mit SplitAfter genau diese Information frei Haus geliefert. Die wichtigste strukturelle Eigenschaft folgt direkt daraus: strings.Join(result, "") rekonstruiert den ursprünglichen String exakt, Byte für Byte. Damit eignet sich SplitAfter als verlustfreie Zerlegung — ideal für Token-Streams, deren Originalstruktur am Ende wieder zusammengesetzt werden muss.
Die Signatur ist identisch zu Split, das Verhalten unterscheidet sich nur an einer Stelle: dem Verbleib des Trenners. Rückgabewert ist ein neu allokierter Slice mit den einzelnen Stücken in Reihenfolge des Auftretens im Eingabe-String.
func SplitAfter(s, sep string) []stringBeide Parameter sind reine Eingaben; der Original-String bleibt unverändert, da Strings in Go unveränderlich sind. Das zurückgegebene Slice referenziert allerdings denselben zugrundeliegenden Speicher — die Stücke sind Substrings, kein Deep Copy.
Der zentrale Unterschied zu Split lässt sich am besten an einem direkten Vergleich zeigen. Bei Split wird der Trenner als Strukturzeichen interpretiert und entfernt, bei SplitAfter bleibt er Teil des vorangehenden Tokens. Aus drei zeilengetrennten Zeichen werden daher drei Tokens, von denen die ersten beiden ihren Zeilenumbruch behalten.
package main
import (
"fmt"
"strings"
)
func main() {
s := "a\nb\nc"
fmt.Printf("Split: %q\n", strings.Split(s, "\n"))
fmt.Printf("SplitAfter: %q\n", strings.SplitAfter(s, "\n"))
}Split: ["a" "b" "c"]
SplitAfter: ["a\n" "b\n" "c"]Beide Funktionen finden dieselben Trenner-Positionen, schneiden den Eingabe-String aber unterschiedlich: Split schneidet vor und nach dem Trenner und verwirft ihn, SplitAfter schneidet ausschließlich nach dem Trenner und nimmt ihn ins Token mit.
Aus dem Verbleib des Trenners folgt eine elegante algebraische Eigenschaft: SplitAfter ist verlustfrei. Da kein Zeichen des Original-Strings verworfen wird, lässt sich das Original durch simples Aneinanderhängen aller Stücke ohne zusätzlichen Separator wiederherstellen. Diese Round-Trip-Garantie macht SplitAfter zum Werkzeug der Wahl, wenn Tokens einzeln bearbeitet, danach aber wieder zur Originalstruktur zusammengefügt werden müssen.
package main
import (
"fmt"
"strings"
)
func main() {
original := "Zeile 1\nZeile 2\r\nZeile 3\n"
parts := strings.SplitAfter(original, "\n")
rebuilt := strings.Join(parts, "")
fmt.Printf("Parts: %q\n", parts)
fmt.Println("Gleich? ", original == rebuilt)
}Parts: ["Zeile 1\n" "Zeile 2\r\n" "Zeile 3\n" ""]
Gleich? trueBemerkenswert ist hier das leere letzte Element: Endet der String mit dem Trenner, hängt SplitAfter ein abschließendes "" an — denn nach dem letzten \n folgt noch ein (leeres) Stück. Genau dieses Element ist nötig, damit Join mit leerem Separator wieder das Original ergibt.
Endet der Eingabe-String nicht mit dem Trenner, trägt das letzte Element konsequenterweise keinen Trenner-Suffix. Das ist kein Sonderfall, sondern die direkte Folge des Schneideprinzips: SplitAfter schneidet nach jedem gefundenen Trenner, und was nach dem letzten Trenner noch übrig bleibt, ist das finale Token — egal ob mit oder ohne abschließenden Separator.
package main
import (
"fmt"
"strings"
)
func main() {
mit := "a\nb\nc\n"
ohne := "a\nb\nc"
fmt.Printf("mit Trenner-Ende: %q\n", strings.SplitAfter(mit, "\n"))
fmt.Printf("ohne Trenner-Ende: %q\n", strings.SplitAfter(ohne, "\n"))
}mit Trenner-Ende: ["a\n" "b\n" "c\n" ""]
ohne Trenner-Ende: ["a\n" "b\n" "c"]Wer Tokens uniform weiterverarbeiten möchte, sollte daher prüfen, ob das letzte Element den erwarteten Suffix trägt — oder vorab über strings.HasSuffix sicherstellen, dass die Eingabe mit dem Trenner endet.
Wie bei Split gelten zwei Sonderregeln. Ein leerer Eingabe-String liefert einen Slice mit einem einzigen leeren Element — nicht etwa einen leeren Slice. Ein leerer Trenner schaltet auf rune-weise Zerlegung um: jedes Element enthält genau eine UTF-8-Rune des Originals.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("leerer Input: %q\n", strings.SplitAfter("", "\n"))
fmt.Printf("leerer Trenner: %q\n", strings.SplitAfter("Go!", ""))
fmt.Printf("rune-weise UTF8: %q\n", strings.SplitAfter("äöü", ""))
}leerer Input: [""]
leerer Trenner: ["G" "o" "!"]
rune-weise UTF8: ["ä" "ö" "ü"]Der rune-weise Modus ist multibyte-sicher: Die deutschen Umlaute werden korrekt als jeweils ein Element zurückgegeben, obwohl sie in UTF-8 zwei Bytes belegen. Eine byte-weise Zerlegung wäre über []byte(s) zu erreichen — das ist semantisch etwas anderes.
Die drei Funktionen unterscheiden sich in zwei orthogonalen Achsen: ob der Trenner erhalten bleibt und ob eine Obergrenze für die Anzahl der Tokens existiert. Die folgende Tabelle ordnet sie ein.
| Funktion | Trenner erhalten? | Limit | Typischer Einsatz |
|---|---|---|---|
Split | nein | unbegrenzt | reine Inhalts-Zerlegung, CSV-Felder |
SplitAfter | ja (am Ende) | unbegrenzt | Zeilen mit \n/\r\n erhalten, verlustfreie Zerlegung |
SplitN | nein | n Tokens max | nur ersten Teil extrahieren, Rest als Block |
SplitAfterN | ja (am Ende) | n Tokens max | begrenzte Zeilen-Verarbeitung mit Originalstruktur |
Faustregel: Brauche ich das Original am Ende wieder, führt der Weg über SplitAfter. Will ich nur die Inhalte zwischen den Trennern, ist Split die richtige Wahl.
Beim Lesen von Logfiles, Konfigurationen oder Quellcode mischen sich in der Praxis oft Unix- (\n) und Windows-Zeilenenden (\r\n). Wer mit Split arbeitet, verliert diese Information und steht später beim Zurückschreiben vor der Frage, welches Zeilenende das richtige war. SplitAfter erhält das Original-Lineending pro Zeile — die Datei lässt sich nach einer Filter-Operation byte-identisch (bis auf entfernte Zeilen) zurückschreiben.
package main
import (
"fmt"
"strings"
)
func main() {
// Gemischte Zeilenenden, wie sie aus Cross-Platform-Quellen kommen.
log := "INFO start\nWARN retry\r\nERROR fail\nINFO done\n"
var kept []string
for _, line := range strings.SplitAfter(log, "\n") {
if line == "" {
continue
}
if strings.HasPrefix(line, "INFO") {
continue // INFO-Zeilen ausfiltern
}
kept = append(kept, line)
}
filtered := strings.Join(kept, "")
fmt.Print(filtered)
}WARN retry
ERROR failDas \r\n der WARN-Zeile bleibt unberührt im Output erhalten, die LF-Endung der ERROR-Zeile ebenfalls. Hätte man hier Split plus manuell angefügtem \n benutzt, wäre \r\n zu \n mutiert — ein subtiler Datenverlust, der bei Roundtrip-Vergleichen oder Diff-Tools auffällt.
Ein klassischer Anwendungsfall ist die selektive Bearbeitung einzelner Tokens innerhalb eines Streams, dessen Gesamtstruktur erhalten bleiben muss — etwa beim Anonymisieren bestimmter Zeilen, beim Annotieren einzelner Sätze oder beim Übersetzen von Code-Kommentaren. Die Kombination SplitAfter + Token-Mapping + Join("") liefert exakt diese Pipeline.
package main
import (
"fmt"
"strings"
)
// Maskiert IPv4-artige Tokens, behält aber Whitespace und Punktuation.
func anonymize(s string) string {
parts := strings.SplitAfter(s, " ")
for i, p := range parts {
trimmed := strings.TrimRight(p, " ")
if strings.Count(trimmed, ".") == 3 {
// Trailing Space rekonstruieren
tail := p[len(trimmed):]
parts[i] = "***.***.***.***" + tail
}
}
return strings.Join(parts, "")
}
func main() {
in := "Client 192.168.1.42 -> Server 10.0.0.1 OK"
fmt.Println(anonymize(in))
}Client ***.***.***.*** -> Server ***.***.***.*** OKDa SplitAfter das nachfolgende Leerzeichen am vorigen Token belässt, bleibt die Wortabstands-Struktur beim Zusammensetzen automatisch korrekt. Ohne diesen Erhalt müsste man die Trenner-Positionen separat tracken oder mit Split plus eigenem Re-Join-Loop arbeiten — beides fehleranfälliger.
Trenner bleibt am Ende
Jedes Token enthält den sep-String als Suffix — der einzige semantische Unterschied zu Split.
Rekonstruierbar via Join mit leerem Separator
strings.Join(strings.SplitAfter(s, sep), "") == s gilt immer — verlustfreie Zerlegung.
Letztes Element ohne Trenner-Suffix möglich
Endet s nicht mit sep, trägt das letzte Token keinen Trenner; endet s mit sep, hängt ein leeres "" als finales Element an.
Leerer Trenner schaltet auf rune-weise
SplitAfter(s, "") liefert jede UTF-8-Rune einzeln als Element — multibyte-sicher, nicht byte-weise.
Ideal für Lineending-Erhalt
Mischbestände aus \n und \r\n bleiben pro Zeile original — kein versehentliches Normalisieren beim Roundtrip.
Alternative zu manuellem Append des Trenners
Ersetzt das fehleranfällige Pattern Split + nachträgliches Zusammenkleben mit künstlichem Separator.
Threadsafe auf Input-Ebene
Liest nur — der Eingabe-String wird nicht verändert, mehrere Goroutinen dürfen denselben String parallel zerlegen.
Allokiert neuen Slice, Tokens teilen Speicher
Das Ergebnis-Slice ist neu, die enthaltenen Stücke referenzieren jedoch den Speicher des Original-Strings — kein Deep Copy.