strings.SplitAfterN ist die Verschmelzung zweier Sonderformen von Split: Trenner werden — wie bei SplitAfter — am Ende jedes Stücks behalten, und die Anzahl der Stücke wird — wie bei SplitN — durch einen Parameter n begrenzt. Das letzte Stück enthält den kompletten Rest des Strings einschließlich aller noch nicht ausgewerteten Trenner, was die Funktion zu einem präzisen Werkzeug für „erste N Einheiten einzeln, Rest unangetastet" macht.

In der Praxis trifft man SplitAfterN selten, weil die meisten Probleme entweder mit SplitAfter (alle Stücke) oder SplitN (Trenner weg) ausreichend gelöst sind. Wo die Kombination jedoch passt — Header-Block vom Body trennen, Stream-Anfang vom Rest abgrenzen — ist sie deutlich knapper als manuelle Index-Suche mit strings.Index und Slicing.

Die Signatur ist exakt die von SplitN, nur dass der Trenner stehen bleibt. Das Verhalten von n ist identisch und folgt damit derselben dreistufigen Logik wie bei allen *N-Varianten.

Go signatur.go
func SplitAfterN(s, sep string, n int) []string

Die Reihenfolge der Parameter ist (s, sep, n) — String zuerst, Trenner, dann Limit. Diese Reihenfolge teilt sich SplitAfterN mit SplitN, SplitAfter und Split, sodass das Refactoring zwischen den vier Funktionen reines Symbol-Umbenennen ist.

Der Parameter n steuert die maximale Anzahl der zurückgegebenen Stücke und folgt den gleichen Regeln wie bei SplitN. Die Sonderfälle sind nicht intuitiv, lassen sich aber einmal eingeprägt zuverlässig anwenden.

  • n < 0: alle Stücke, identisch zu SplitAfter(s, sep).
  • n == 0: gibt nil zurück — kein leerer Slice, sondern echtes nil.
  • n == 1: ein einziges Element, das den kompletten Eingabe-String enthält (kein Split passiert).
  • n >= 2: höchstens n Stücke; das n-te Stück bündelt den gesamten Rest.
Go n_parameter.go
package main

import (
	"fmt"
	"strings"
)

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

	fmt.Printf("%q\n", strings.SplitAfterN(s, ",", -1))
	fmt.Printf("%q\n", strings.SplitAfterN(s, ",", 0))
	fmt.Printf("%q\n", strings.SplitAfterN(s, ",", 1))
	fmt.Printf("%q\n", strings.SplitAfterN(s, ",", 3))
}
Output
["a," "b," "c," "d," "e"]
[]
["a,b,c,d,e"]
["a," "b," "c,d,e"]

Besonders der Fall n == 0 ist ein klassischer Stolperstein: das Ergebnis ist nil, was in %q-Ausgabe als [] erscheint, in einer for range-Schleife aber zu null Iterationen führt. Wer hier ein Element erwartet, hat den Fehler oft erst zur Laufzeit.

Wie bei SplitAfter bleibt der Trenner am Ende jedes Stücks erhalten — bis auf das letzte Stück, das den gesamten unverarbeiteten Rest enthält. Bei n == 2 heißt das konkret: das erste Element endet auf dem ersten Trenner, das zweite Element enthält alles dahinter inklusive aller weiteren Trenner.

Go trenner_erhalt.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "kopf;mitte;ende;extra"

	teile := strings.SplitAfterN(s, ";", 2)
	fmt.Printf("erstes: %q\n", teile[0])
	fmt.Printf("zweites: %q\n", teile[1])
}
Output
erstes: "kopf;"
zweites: "mitte;ende;extra"

Das zweite Stück ist also kein weiter zerlegtes Fragment, sondern der pure Rest-String — inklusive aller Trenner, die SplitAfterN nicht mehr angefasst hat. Genau diese Eigenschaft macht die Funktion für Header/Body-Trennungen attraktiv.

SplitAfterN ist verlustfrei: Da kein einziges Zeichen verworfen wird, lässt sich der ursprüngliche String exakt rekonstruieren. strings.Join(result, "") mit leerem Separator setzt die Stücke wieder zum Original zusammen.

Go rekonstruktion.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	original := "eins,zwei,drei,vier"
	teile := strings.SplitAfterN(original, ",", 3)

	rekonstruiert := strings.Join(teile, "")
	fmt.Println(rekonstruiert == original)
	fmt.Printf("%q\n", teile)
}
Output
true
["eins," "zwei," "drei,vier"]

Diese Eigenschaft unterscheidet die SplitAfter-Familie fundamental von Split/SplitN — bei letzteren ist der Trenner endgültig weg und müsste beim Join wieder mit Hand eingesetzt werden, was bei mehrstelligen oder gemischten Trennern fehleranfällig ist.

SplitAfterN löst einen schmalen Use-Case: Die ersten N-1 Einheiten sollen einzeln verarbeitet werden, der Rest bleibt unangetastet und behält seine interne Trenner-Struktur. Klassisches Beispiel ist das Abtrennen eines Header-Blocks von einem Body, ohne den Body neu zusammensetzen zu müssen.

In den meisten Fällen reicht SplitAfter (alle Stücke einzeln) oder bufio.Scanner (zeilenweises Streaming). Erst wenn beide Bedingungen zusammenkommen — Trenner erhalten und Limit — wird SplitAfterN zur passenden Wahl. Alternativ funktioniert oft auch ein einzelnes strings.Index plus Slicing, was bei n == 2 sogar lesbarer sein kann.

Die drei Funktionen unterscheiden sich nur in zwei Achsen: Trenner-Erhalt und Limit. Die Tabelle zeigt, welche Kombination jede Variante abdeckt.

FunktionTrenner erhaltenLimit (n)Rekonstruierbar via Join ""
Splitneinneinnein
SplitNneinjanein
SplitAfterjaneinja
SplitAfterNjajaja

Die untere Zeile ist also die maximalkombinierte Form: alles bleibt, aber nur bis zur gewünschten Stückzahl. Diese Symmetrie macht das Standardpaket strings beim Umgang mit Trennern erfreulich orthogonal.

Ein typisches Anwendungsfeld ist das Abtrennen eines Header-Bereichs von einem Nachrichten-Body, ohne den Body weiter zu zerlegen. Mit SplitAfterN(message, "\n", 4) bekommt man die ersten drei Zeilen einzeln (jeweils mit \n am Ende) und den kompletten Rest als unverändertes viertes Element.

Go header_body.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	message := "From: alice@example.com\n" +
		"To: bob@example.com\n" +
		"Subject: Hallo\n" +
		"Body: Erste Zeile\nZweite Zeile\nDritte Zeile\n"

	teile := strings.SplitAfterN(message, "\n", 4)

	for i, t := range teile[:3] {
		fmt.Printf("Header %d: %q\n", i+1, t)
	}
	fmt.Printf("Rest (Body): %q\n", teile[3])
}
Output
Header 1: "From: alice@example.com\n"
Header 2: "To: bob@example.com\n"
Header 3: "Subject: Hallo\n"
Rest (Body): "Body: Erste Zeile\nZweite Zeile\nDritte Zeile\n"

Der Body bleibt damit byteidentisch zu dem, was in message stand — keine verloren gegangenen \n, keine Zusammensetzung nötig. Wer den Body direkt an einen Parser weiterreichen muss, der seinerseits Zeilenumbrüche interpretiert, profitiert hier deutlich von der Trenner-Erhaltung.

Bei records-basierten Formaten, die einen festen Trenner zwischen Datensätzen nutzen (z. B. doppelte Newlines, Pipe-Sequenzen), kann SplitAfterN die ersten N-1 Records sauber abtrennen und den unverarbeiteten Rest als einen einzigen Block stehen lassen. Das ist nützlich, wenn nur die Anfangs-Records inspiziert und der Rest später weitergeleitet wird.

Go stream_records.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	stream := "REC1:alpha|END|REC2:beta|END|REC3:gamma|END|REC4:delta|END|REC5:epsilon|END|"

	teile := strings.SplitAfterN(stream, "|END|", 3)

	fmt.Println("Verarbeitete Records:")
	for i, r := range teile[:len(teile)-1] {
		fmt.Printf("  %d: %q\n", i+1, r)
	}
	fmt.Printf("Rest-Stream (unverarbeitet): %q\n", teile[len(teile)-1])
}
Output
Verarbeitete Records:
  1: "REC1:alpha|END|"
  2: "REC2:beta|END|"
Rest-Stream (unverarbeitet): "REC3:gamma|END|REC4:delta|END|REC5:epsilon|END|"

Der Rest-String enthält die noch nicht ausgewerteten Records inklusive ihrer |END|-Trenner und kann unverändert an einen weiteren Verarbeitungsschritt — etwa eine Goroutine oder einen späteren Parser-Lauf — weitergegeben werden. Genau diese Verlustfreiheit unterscheidet SplitAfterN von einer SplitN-basierten Lösung, bei der jeder |END|-Trenner manuell rekonstruiert werden müsste.

Kombination SplitAfter + SplitN

SplitAfterN ist exakt die orthogonale Verschmelzung: Trenner-Erhalt aus SplitAfter plus n-Limit aus SplitN in einer einzigen Funktion.

n-Semantik identisch zu SplitN

Die Regeln für n sind 1:1 die von SplitN — negativ heisst alle, 0 ergibt nil, 1 ergibt den ganzen String, ab 2 greift das Limit.

Trenner bleibt am Ende

Jedes Stück ausser dem letzten endet auf dem Trenner; das letzte Stück enthält den kompletten Rest inklusive aller noch nicht abgetrennten Trenner.

Rekonstruierbar via Join (leer)

strings.Join(result, "") mit leerem Separator liefert den ursprünglichen Eingabe-String byteidentisch zurück — verlustfrei wie SplitAfter.

Selten direkt benötigt

Der Anwendungsbereich ist schmal; meist reicht SplitAfter oder ein strings.Index mit Slicing aus, weshalb SplitAfterN in produktivem Code selten auftaucht.

Alternative für „erste N + Rest“

Wann immer die ersten N-1 Einheiten einzeln und der Rest unverändert gebraucht wird — Header/Body, Anfangs-Records, Stream-Prefix —, ist SplitAfterN die kürzeste Lösung.

Threadsafe

Die Funktion arbeitet ausschliesslich auf dem übergebenen string (immutable) und allokiert ein neues []string; parallele Aufrufe sind ohne Synchronisation sicher.

Allokiert Slice plus Substrings

Es entsteht ein neuer []string mit bis zu n Einträgen, wobei die Einträge Substring-Views auf das Original sind — der Backing-Array des Eingabe-Strings bleibt referenziert.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht