strings.Split zerlegt einen String s an jedem Vorkommen eines exakten Trenner-Strings sep und liefert das Ergebnis als frisch allozierten []string. Anders als Fields, das ganze Whitespace-Bereiche als einen Trenner behandelt, ist Split striktes Pattern-Matching: jeder einzelne Treffer erzeugt eine Grenze, und aufeinanderfolgende Trenner produzieren leere Felder, die im Ergebnis-Slice erhalten bleiben. Damit ist Split die richtige Wahl, wenn die Position der Felder zählt — etwa beim Zerlegen einer CSV-light-Zeile, eines POSIX-Pfads oder einer Header-Komponente mit fester Stellenzahl.
Der Trennvergleich erfolgt byte-genau auf der UTF-8-Repräsentation: sep wird buchstäblich als Byte-Folge gesucht, ohne Unicode-Normalisierung. Split alloziert das Resultat-Slice immer neu — für Hot-Paths mit Millionen Splits gibt es seit Go 1.24 mit strings.SplitSeq einen Iterator, der ohne Slice-Allokation auskommt.
Die Signatur ist denkbar einfach: zwei String-Parameter rein, ein String-Slice raus. s ist der zu zerlegende Eingangsstring, sep der buchstäbliche Trenner. Es gibt keinen Fehlerwert — Split kann nicht fehlschlagen, lediglich der Sonderfall sep == "" ändert die Semantik (siehe Abschnitt 04).
package main
import (
"fmt"
"strings"
)
// func Split(s, sep string) []string
func main() {
teile := strings.Split("rot,gruen,blau", ",")
fmt.Printf("%q\n", teile)
fmt.Println("Anzahl:", len(teile))
}["rot" "gruen" "blau"]
Anzahl: 3Der Trenner "," wird zweimal gefunden, also entstehen drei Felder. Beachte, dass Split den Trenner selbst nicht in die Ergebnisstrings übernimmt — er wird verbraucht. Soll der Trenner am Ende jedes Feldes erhalten bleiben, ist SplitAfter die richtige Variante.
Der zentrale Unterschied zu strings.Fields: Split kollabiert nichts. Stehen zwei Trenner direkt nebeneinander, entsteht ein leeres Feld "" zwischen ihnen. Beginnt der String mit dem Trenner, ist das erste Feld leer; endet er mit dem Trenner, ist das letzte Feld leer. Diese Treue zur Eingabe ist genau das, was man bei feldorientierten Formaten will — eine fehlende Spalte muss erkennbar bleiben.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Printf("%q\n", strings.Split("a,,b", ",")) // mittleres Feld leer
fmt.Printf("%q\n", strings.Split(",a,b", ",")) // fuehrendes Feld leer
fmt.Printf("%q\n", strings.Split("a,b,", ",")) // abschliessendes Feld leer
fmt.Printf("%q\n", strings.Split(",,", ",")) // drei leere Felder
}["a" "" "b"]
["" "a" "b"]
["a" "b" ""]
["" "" ""]Genau dieses Verhalten macht Split für CSV-light, TSV oder pipe-separierte Logs brauchbar: eine leere Zelle bleibt eine leere Zelle und verschiebt nicht die Spalten-Indizes. Wer dagegen Whitespace-getrennte Tokens haben will und mehrere Leerzeichen ignorieren möchte, greift zu Fields.
Übergibt man sep == "", schaltet Split in einen Spezialmodus: der String wird rune-weise zerlegt, also pro Unicode-Codepoint, nicht pro Byte. Das ist wichtig für Umlaute und andere Mehrbyte-Zeichen — ein ä belegt zwei UTF-8-Bytes, bleibt aber als zusammengehöriger Eintrag im Slice. Eine byte-weise Zerlegung würde das ä zerreißen und ungültige UTF-8-Sequenzen produzieren; Split tut das nicht.
package main
import (
"fmt"
"strings"
)
func main() {
runen := strings.Split("für", "")
fmt.Printf("%q\n", runen)
fmt.Println("Anzahl:", len(runen))
// Byte-Sicht zum Vergleich
fmt.Println("Bytes:", len("für"))
}["f" "ü" "r"]
Anzahl: 3
Bytes: 4Der String "für" ist vier Bytes lang (das ü belegt zwei), enthält aber drei Runen — und genau drei Elemente liefert Split. Für reine Rune-Iteration ohne Slice-Allokation ist allerdings range s oder []rune(s) meist passender; der leere Trenner ist eher ein Edge-Case, den man bewusst nicht versehentlich auslösen sollte.
Kommt sep im String gar nicht vor, liefert Split einen Slice mit genau einem Element zurück: dem unveränderten Original-String. Es wird nicht nil zurückgegeben und auch kein leerer Slice. Damit kann man das Ergebnis bedenkenlos mit len(result) > 1 darauf prüfen, ob überhaupt zerlegt wurde.
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.Split("kein-trenner-hier", ",")
fmt.Printf("%q\n", r)
fmt.Println("Laenge:", len(r))
// Leerer Eingangsstring
r2 := strings.Split("", ",")
fmt.Printf("%q\n", r2)
fmt.Println("Laenge:", len(r2))
}["kein-trenner-hier"]
Laenge: 1
[""]
Laenge: 1Auch der leere Eingangsstring ist kein Sonderfall im negativen Sinn — er liefert einen Slice mit einem einzigen leeren Element. Wer auf „nichts zu zerlegen" reagieren will, sollte den Eingangsstring vorher selbst prüfen, nicht erst das Ergebnis.
Split alloziert immer einen neuen []string und dazu — je nach Implementierung — einen Backing-Array für die Substring-Headers. Die Substrings selbst teilen sich den Speicher des Eingangsstrings (Strings sind in Go immutabel und werden geshared), aber das Slice-Objekt ist neu. In Schleifen über Millionen Zeilen kann diese Allokation den GC unter Druck setzen.
Seit Go 1.24 gibt es deshalb strings.SplitSeq, das einen iter.Seq[string] zurückgibt — die Felder werden lazy erzeugt und können direkt per range konsumiert werden, ohne dass je ein Slice entsteht.
package main
import (
"fmt"
"strings"
)
func main() {
// Klassisch: alloziert []string
for _, feld := range strings.Split("a,b,c,d", ",") {
fmt.Println(feld)
}
fmt.Println("---")
// Go 1.24+: keine Slice-Allokation
for feld := range strings.SplitSeq("a,b,c,d", ",") {
fmt.Println(feld)
}
}a
b
c
d
---
a
b
c
dFunktional ist das Ergebnis identisch, der Unterschied liegt allein im Speicherprofil. Solange man die Felder ohnehin nur einmal sequenziell durchläuft und nicht behalten muss, ist SplitSeq die effizientere Wahl.
Die drei Funktionen lösen oberflächlich ähnliche Aufgaben, unterscheiden sich aber fundamental in Semantik und Anwendungsfeld. Die Tabelle fasst zusammen, wann welche das richtige Werkzeug ist.
| Aspekt | Split | Fields | Cut |
|---|---|---|---|
| Trenner | exakter String | beliebige Whitespace-Folge | exakter String |
| Leere Felder | bleiben erhalten | werden geschluckt | n. a. |
| Mehrfache Trenner | erzeugen leere Felder | werden zu einem zusammengefasst | nur erster Treffer |
| Rückgabe | []string | []string | (before, after string, found bool) |
| Anzahl Felder | variabel | variabel | genau 2 |
| Typischer Einsatz | CSV-light, Pfade | Whitespace-Tokens | key=value, Präfix abtrennen |
Faustregel: feldorientierte Formate mit erhaltenen Leerstellen — Split. Freitext mit irgendwie geartetem Whitespace — Fields. Genau ein Trenner, genau zwei Teile gewünscht — Cut.
Für einfache CSV-Zeilen ohne Quoting und ohne Kommas in Feldern reicht Split völlig aus. In der Praxis kombiniert man Split meist mit TrimSpace pro Feld, weil Eingaben oft Leerzeichen rund um die Kommas enthalten. Wichtig: sobald Felder in Anführungszeichen stehen oder Kommas enthalten dürfen, ist das encoding/csv-Paket Pflicht — Split kann diese Fälle nicht korrekt behandeln.
package main
import (
"fmt"
"strings"
)
func parseZeile(zeile string) []string {
felder := strings.Split(zeile, ",")
for i, f := range felder {
felder[i] = strings.TrimSpace(f)
}
return felder
}
func main() {
zeile := " Anna , 34 , Berlin ,"
felder := parseZeile(zeile)
fmt.Printf("%q\n", felder)
fmt.Println("Spalten:", len(felder))
}["Anna" "34" "Berlin" ""]
Spalten: 4Das vierte, leere Feld ist der entscheidende Hinweis: die Zeile endet mit einem Komma, also gibt es eine vierte (leere) Spalte. Mit Fields wäre diese Information verloren — Split bewahrt sie, und das ist bei spaltenorientierten Daten genau richtig.
POSIX-Pfade lassen sich am / zerlegen, um die Hierarchie sichtbar zu machen. Ein absoluter Pfad beginnt mit /, das erzeugt einen leeren ersten String — der ist semantisch korrekt und markiert die Wurzel. Für die Aufspaltung der PATH-Umgebungsvariablen ist allerdings filepath.SplitList zu bevorzugen, weil es das plattformabhängige Trennzeichen (: auf Unix, ; auf Windows) automatisch wählt.
package main
import (
"fmt"
"strings"
)
func main() {
pfad := "/usr/local/bin/go"
teile := strings.Split(pfad, "/")
fmt.Printf("%q\n", teile)
fmt.Println("Erstes Element leer? ", teile[0] == "")
fmt.Println("Letztes Segment: ", teile[len(teile)-1])
}["" "usr" "local" "bin" "go"]
Erstes Element leer? true
Letztes Segment: goDas führende leere Element ist kein Bug, sondern die direkte Konsequenz aus der Split-Semantik: vor dem ersten / steht eben nichts, und Split bildet das ehrlich ab. Wer relative und absolute Pfade einheitlich behandeln will, filtert leere Elemente nach dem Split heraus oder nutzt path.Clean und path.Split aus dem path-Paket.
Exakter Trenner-Vergleich
sep wird byte-genau auf der UTF-8-Repräsentation gesucht, ohne Unicode-Normalisierung.
Leere Felder bleiben erhalten
Aufeinanderfolgende Trenner erzeugen leere Strings im Resultat — anders als bei Fields.
Leerer Trenner zerlegt rune-weise
Split(s, "") liefert pro Unicode-Codepoint ein Element, nicht pro Byte; Umlaute bleiben intakt.
Immer Allokation
Split alloziert grundsaetzlich einen neuen []string; in Hot-Paths kann das spürbar werden.
Mindestens ein Element
Wird sep nicht gefunden, enthält das Ergebnis genau ein Element — den Original-String.
SplitN für Limit
SplitN(s, sep, n) begrenzt die Anzahl der Felder; das letzte Element enthält den unzerlegten Rest.
SplitSeq als Iterator
Ab Go 1.24 liefert SplitSeq einen iter.Seq[string] ohne Slice-Allokation für range-Schleifen.
Cut für Zweier-Split
Wenn nur ein Trenner und genau zwei Teile gewünscht sind, ist Cut schlanker und allokiert nichts.
Weiterführende Ressourcen
Externe Quellen
strings.Splitencoding/csvfür echtes CSVstrings.SplitSeq(Go 1.24+)