strings.Cut schneidet einen String s am ersten Vorkommen eines Trenners sep in zwei Teile und liefert zusätzlich ein Erfolgs-Flag zurück. Die Signatur (before, after string, found bool) macht zwei Dinge gleichzeitig sichtbar: das Ergebnis und die Frage, ob der Trenner überhaupt vorkam — beides früher nur über mehrere Aufrufe von strings.Index und manuelles Slicing zu erreichen.
Eingeführt wurde die Funktion mit Go 1.18 und gilt seitdem als der idiomatische Ersatz für das alte Drei-Zeilen-Muster aus Index, Längenprüfung und zwei Slice-Ausdrücken. Genau in diesem Muster steckten klassische Bugs: vergessene -1-Prüfung, falsche Offset-Addition für die sep-Länge, off-by-one beim Slice. Cut macht aus dieser fehleranfälligen Sequenz einen einzigen Aufruf, der Key-Value-Parsing, URL-Schema-Trennung, Header-Splits und ähnliche Aufgaben in einer Zeile abbildet.
Die Signatur ist bewusst kompakt gehalten und besteht aus zwei Eingabe-Strings und drei Rückgabewerten. Der erste String ist die Quelle, der zweite der gesuchte Trenner — die Rückgabe liefert die beiden Teile sowie einen bool, der den Trefferstatus dokumentiert.
package main
import (
"fmt"
"strings"
)
// func Cut(s, sep string) (before, after string, found bool)
func main() {
before, after, found := strings.Cut("host:8080", ":")
fmt.Printf("before=%q after=%q found=%v\n", before, after, found)
}before="host" after="8080" found=trueDer Aufruf liest sich wie eine Behauptung: „Zerschneide s an sep — und sag mir, ob das geklappt hat." Genau diese Lesbarkeit ist der Grund, warum Cut in modernem Go-Code fast immer dem älteren SplitN(s, sep, 2) vorgezogen wird.
Die drei Rückgaben sind klar voneinander getrennt: before enthält alles vor dem ersten Treffer von sep, after alles dahinter, und found zeigt an, ob sep überhaupt gefunden wurde. Wichtig ist das Verhalten bei einem Nicht-Treffer: before ist dann der komplette Quell-String, after ist leer, found ist false — eine Konvention, die das Default-Branching besonders bequem macht.
package main
import (
"fmt"
"strings"
)
func main() {
// Treffer
b, a, ok := strings.Cut("key=value", "=")
fmt.Printf("hit: before=%q after=%q found=%v\n", b, a, ok)
// Kein Treffer
b, a, ok = strings.Cut("nokeyvalue", "=")
fmt.Printf("miss: before=%q after=%q found=%v\n", b, a, ok)
}hit: before="key" after="value" found=true
miss: before="nokeyvalue" after="" found=falseDer zweite Fall illustriert eine bewusste Designentscheidung: Wer den Trenner nicht findet, hat in before weiterhin den vollständigen Input — kein Sonderfall, kein nil, kein Panic. Eine typische Verwendung ist if !found { return s, defaultValue }, die ohne weitere Slice-Logik auskommt.
Cut ist kein Mehrfach-Splitter. Es sucht nur das erste Vorkommen von sep und packt alles Weitere unverändert in after. Wer einen String an allen Vorkommen zerlegen möchte, greift zu strings.Split oder strings.SplitN — Cut hingegen ist die richtige Wahl, wenn der Trenner semantisch genau einmal auftauchen soll, etwa beim ersten = einer Konfigurationszeile.
package main
import (
"fmt"
"strings"
)
func main() {
// Mehrere "=" — nur das erste trennt
b, a, _ := strings.Cut("path=/usr/local/bin=fallback", "=")
fmt.Printf("before=%q\nafter =%q\n", b, a)
}before="path"
after ="/usr/local/bin=fallback"Das Verhalten ist exakt das, was beim Parsen von KEY=VALUE-Zeilen gewünscht ist: ein Gleichheitszeichen im Wert darf den Schlüssel nicht verstümmeln. Wer dagegen ein letztes Vorkommen braucht — etwa für Dateiendungen — kombiniert strings.LastIndex mit manuellem Slicing oder nutzt eine eigene Helper-Funktion.
Zwei Randfälle lohnen sich, einmal explizit zu sehen: ein leerer Quell-String und ein leerer Trenner. Beide Fälle liefern wohldefinierte Werte und werfen keine Fehler — Cut ist dafür ausgelegt, ohne Vorab-Validierung sicher aufgerufen zu werden.
package main
import (
"fmt"
"strings"
)
func main() {
// Leerer Quell-String
b1, a1, f1 := strings.Cut("", "=")
fmt.Printf("empty s: before=%q after=%q found=%v\n", b1, a1, f1)
// Leerer Trenner — matcht an Position 0
b2, a2, f2 := strings.Cut("hallo", "")
fmt.Printf("empty sep: before=%q after=%q found=%v\n", b2, a2, f2)
}empty s: before="" after="" found=false
empty sep: before="" after="hallo" found=trueDer leere Trenner ist der interessante Fall: Da der leere String per Definition an jeder Position vorkommt (auch an Position 0), liefert Cut before="", after=s und found=true. Das ist konsistent mit strings.Index(s, "") == 0, wirkt beim ersten Lesen aber überraschend — bei nutzergesteuerten Trennern lohnt sich eine if sep == ""-Vorprüfung.
Vor Go 1.18 war das übliche Muster entweder strings.SplitN(s, sep, 2) mit anschließendem Längen-Check oder ein manuelles strings.Index plus Slicing. Beide Wege funktionieren weiterhin, sind aber weniger lesbar und in der Index-Variante deutlich fehleranfälliger.
| Aspekt | strings.Cut | SplitN(s, sep, 2) | Index + Slice |
|---|---|---|---|
| Rückgabe | before, after, found | []string (1 oder 2 Elemente) | int + manuelles Slicing |
| Erfolgsprüfung | direkt via found | len(parts) == 2 | i != -1 |
| Allokation | keine Slice-Allokation | []string-Header | keine |
| Lesbarkeit | hoch — Absicht klar | mittel | niedrig |
| Bug-Risiko | minimal | gering | hoch (Offset, len(sep)) |
| Verfügbar seit | Go 1.18 | seit jeher | seit jeher |
In neuem Code ist Cut praktisch immer die richtige Wahl, sobald genau ein Trenner relevant ist. SplitN(2) bleibt nützlich, wenn der Code generisch über n parametrisiert ist; das Index+Slice-Muster sollte man nur noch sehen, wenn zusätzlich der numerische Offset gebraucht wird.
Das Lesen von KEY=VALUE-Zeilen aus .env-Dateien, HTTP-Cookies oder Konfig-Snippets ist der Lehrbuchfall für Cut. Der erste = trennt Schlüssel und Wert, alle weiteren Gleichheitszeichen sind Teil des Werts — exakt die Semantik, die Cut mitbringt.
package main
import (
"fmt"
"strings"
)
func parseEnv(line string) (key, value string, ok bool) {
key, value, ok = strings.Cut(line, "=")
if !ok {
return "", "", false
}
return strings.TrimSpace(key), strings.TrimSpace(value), true
}
func main() {
lines := []string{
"DATABASE_URL=postgres://user:pw@host/db",
"DEBUG=true",
"BROKEN_LINE_OHNE_GLEICH",
"EQUATION=a=b+c",
}
for _, l := range lines {
k, v, ok := parseEnv(l)
fmt.Printf("ok=%-5v key=%-13q value=%q\n", ok, k, v)
}
}ok=true key="DATABASE_URL" value="postgres://user:pw@host/db"
ok=true key="DEBUG" value="true"
ok=false key="" value=""
ok=true key="EQUATION" value="a=b+c"Beachtenswert ist die vierte Zeile: EQUATION=a=b+c wird korrekt aufgeteilt, weil Cut nur am ersten = schneidet und alle weiteren Zeichen im Wert belässt. Genau dieses Verhalten ist beim Parsen von Datenbank-URLs, Base64-Werten oder URL-Encoded-Strings unverzichtbar — naive Split-Aufrufe würden den Wert hier zerreißen.
Eine zweite typische Aufgabe ist das Abspalten des URL-Schemas vom Rest der URL. Der Trenner :// ist klar definiert und kommt im Restpfad nicht vor — ideal für Cut, das hier in einer Zeile erledigt, wofür sonst strings.Index plus Längenrechnung mit len("://") nötig wäre.
package main
import (
"fmt"
"strings"
)
func splitScheme(url string) (scheme, rest string) {
scheme, rest, ok := strings.Cut(url, "://")
if !ok {
return "", url // kein Schema — alles ist "rest"
}
return scheme, rest
}
func main() {
urls := []string{
"https://mibeon.de/docs/go",
"postgres://user@host/db",
"ftp://files.example.org/pub",
"mibeon.de/ohne-schema",
}
for _, u := range urls {
s, r := splitScheme(u)
fmt.Printf("scheme=%-9q rest=%q\n", s, r)
}
}scheme="https" rest="mibeon.de/docs/go"
scheme="postgres" rest="user@host/db"
scheme="ftp" rest="files.example.org/pub"
scheme="" rest="mibeon.de/ohne-schema"Der letzte Fall zeigt, wie sauber der found-Bool das Default-Verhalten steuert: Ohne :// wird die gesamte Eingabe als rest durchgereicht, das scheme bleibt leer. Für vollwertiges URL-Parsing greift man später zu net/url — für den schnellen Schema-Check oder Routing-Entscheidungen reicht Cut aber vollkommen aus.
Seit Go 1.18
strings.Cut wurde mit Go 1.18 eingeführt und ist seitdem fester Bestandteil der Standardbibliothek — Code mit älteren Go-Versionen muss auf Index+Slice oder SplitN zurückgreifen.
Drei Rückgaben: before, after, found
Die Signatur (before, after string, found bool) liefert Ergebnis und Trefferstatus in einem Aufruf — kein separater Index-Aufruf zur Erfolgsprüfung mehr nötig.
Nur erster Treffer
Cut schneidet ausschließlich am ersten Vorkommen von sep; weitere Trenner bleiben unverändert in after enthalten — entscheidend für KEY=VALUE mit = im Wert.
Ersetzt das alte Index+Slice-Muster
Drei fehleranfällige Zeilen (i := Index, if i == -1, zwei Slices mit +len(sep)) werden zu einem klar lesbaren Aufruf — etwa 90 Prozent dieser Muster lassen sich direkt ersetzen.
Idiomatischer als SplitN(s, sep, 2)
SplitN mit n=2 löst dieselbe Aufgabe, alloziert aber einen Slice und braucht eine len(parts) == 2-Prüfung — Cut ist kompakter und drückt die Absicht direkter aus.
Nicht-Treffer: found=false, before=s
Findet Cut den Trenner nicht, ist before der komplette Eingabestring und after leer — bequem für Default-Branching ohne zusätzliche Sonderfall-Logik.
Leerer Trenner ergibt found=true
Cut(s, "") liefert before="", after=s, found=true, weil der leere String an Position 0 matcht — bei nutzergesteuerten Trennern lohnt sich eine explizite if sep == ""-Prüfung.
Threadsafe und allokationsfrei
Wie alle strings-Funktionen ist Cut rein lesend und alloziert keine neuen Strings — die Rückgaben sind Substring-Header auf dem Original-Backing-Array und gefahrlos aus mehreren Goroutinen aufrufbar.