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.

Go signature.go
func SplitAfter(s, sep string) []string

Beide 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.

Go splitafter_vs_split.go
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"))
}
Output
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.

Go roundtrip.go
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)
}
Output
Parts:   ["Zeile 1\n" "Zeile 2\r\n" "Zeile 3\n" ""]
Gleich?  true

Bemerkenswert 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.

Go trailing.go
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"))
}
Output
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.

Go edge_cases.go
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("äöü", ""))
}
Output
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.

FunktionTrenner erhalten?LimitTypischer Einsatz
Splitneinunbegrenztreine Inhalts-Zerlegung, CSV-Felder
SplitAfterja (am Ende)unbegrenztZeilen mit \n/\r\n erhalten, verlustfreie Zerlegung
SplitNneinn Tokens maxnur ersten Teil extrahieren, Rest als Block
SplitAfterNja (am Ende)n Tokens maxbegrenzte 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.

Go logfilter.go
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)
}
Output
WARN  retry
ERROR fail

Das \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.

Go anonymize.go
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))
}
Output
Client ***.***.***.*** -> Server ***.***.***.*** OK

Da 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.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht