strings.Index beantwortet die Frage „Wo beginnt substr innerhalb von s?" und liefert den Byte-Offset des ersten Vorkommens. Findet die Funktion keinen Treffer, gibt sie -1 zurück — eine Sentinel-Konvention, die in der gesamten Standardbibliothek konsistent verwendet wird. Intern arbeitet Index mit einer Variante des Two-Way-String-Matching-Algorithmus und schaltet auf amd64 und arm64 für kurze Substrings auf SIMD-Hot-Paths um. Wichtig im Hinterkopf: der Rückgabewert ist ein Byte-Offset, kein Rune-Offset — sobald Multi-Byte-Zeichen im Spiel sind, sind das zwei verschiedene Dinge.

Die Signatur ist denkbar schlank: zwei Strings rein, ein int raus. Der zurückgegebene Index zeigt auf das erste Byte des Treffers in s; ist der Substring nicht enthalten, kommt -1.

Go signatur.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Index("Hallo Welt", "Welt")) // Treffer ab Byte 6
	fmt.Println(strings.Index("Hallo Welt", "xyz"))  // nicht enthalten
	fmt.Println(strings.Index("abcabc", "bc"))       // erstes Vorkommen
}
Output
6
-1
1

Der dritte Aufruf zeigt das „erstes Vorkommen"-Verhalten: obwohl "bc" zweimal in "abcabc" steckt, liefert Index den ersten Treffer bei Position 1. Für das letzte Vorkommen gibt es LastIndex.

Go-Strings sind UTF-8-kodierte Byte-Sequenzen. Ein ASCII-Zeichen belegt ein Byte, ein Umlaut wie ä belegt zwei, ein Emoji vier. strings.Index zählt Bytes, nicht Code-Points — wer Zeichen-Positionen für Cursor, Spaltenzahlen oder Anzeigelogik braucht, muss umrechnen.

Go bytes_vs_runen.go
package main

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

func main() {
	s := "Schöne Grüße"

	fmt.Println(len(s))                    // Bytes insgesamt
	fmt.Println(utf8.RuneCountInString(s)) // Runen insgesamt

	byteIdx := strings.Index(s, "Grüße")
	fmt.Println("Byte-Offset:", byteIdx)

	// Byte-Offset in Rune-Offset umrechnen
	runeIdx := utf8.RuneCountInString(s[:byteIdx])
	fmt.Println("Rune-Offset:", runeIdx)
}
Output
15
12
Byte-Offset: 8
Rune-Offset: 7

"Schöne " belegt acht Bytes (das ö zählt doppelt), aber nur sieben Runen — daher die Differenz. Solange man den Offset wieder zum Slicen verwendet (s[:byteIdx], s[byteIdx:]), ist der Byte-Offset genau richtig, weil Go-Slicing ebenfalls byte-basiert ist.

Statt eines zweiten Rückgabewertes (ok bool) oder eines Errors nutzt Index den Wert -1, um „nicht gefunden" zu signalisieren. Das ist eine alte Unix/C-Tradition, die Go für die Index-Familie übernommen hat. Vor dem Slicen mit dem Ergebnis ist eine >= 0-Prüfung Pflicht, sonst läuft man bei -1 in einen Out-of-Range-Panic.

Go sentinel.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "user@example.com"

	if i := strings.Index(s, "@"); i >= 0 {
		user := s[:i]
		host := s[i+1:]
		fmt.Printf("user=%q host=%q\n", user, host)
	} else {
		fmt.Println("kein @ gefunden")
	}
}
Output
user="user" host="example.com"

Das Pattern „Index holen, auf >= 0 prüfen, links/rechts slicen" zieht sich durch viele Parser. Seit Go 1.18 lässt sich derselbe Effekt mit strings.Cut oft eleganter ausdrücken — dazu unten mehr.

Ein häufig übersehener Spezialfall: der leere String gilt als an jeder Position vorhanden — Index(s, "") liefert daher 0. Umgekehrt ist in einem leeren s nichts Nicht-Leeres zu finden, also kommt -1.

Go leer.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Index("abc", "")) // leerer Substring -> 0
	fmt.Println(strings.Index("", "x"))   // leeres s        -> -1
	fmt.Println(strings.Index("", ""))    // beides leer     -> 0
}
Output
0
-1
0

Wer Nutzereingaben durchreicht und dabei nicht explizit auf Leerstrings prüft, bekommt mit Index(s, userInput) einen scheinbar harmlosen Treffer an Position 0 — eine Quelle für subtile Bugs in Such- und Filter-Pipelines.

Index ist eine der performance-sensitivsten Funktionen der Standardbibliothek und entsprechend stark optimiert. Für kurze Substrings (insbesondere ein einzelnes Byte) gibt es auf amd64 und arm64 handgeschriebene SIMD-Assembly, die ganze Register-Breiten parallel scannt. Für längere Pattern fällt Go auf einen Two-Way-Algorithmus (Crochemore-Perrin) zurück — linear in der Eingabelänge und ohne den schlimmsten Fall des naiven Matchings.

In Microbenchmarks bewegt sich Index für typische Eingaben im Bereich weniger Nanosekunden pro Aufruf. Für reine Existenzprüfungen empfiehlt sich trotzdem Contains, weil der Code dort die Intention klarer macht — performance-seitig sind beide identisch (Contains ruft intern Index >= 0).

Die drei Funktionen teilen sich die innere Suchmaschine und unterscheiden sich nur in Rückgabe und Suchrichtung. Die Wahl richtet sich nach der Frage, die man eigentlich beantworten will.

FunktionFrageRückgabeBei Nicht-Treffer
Index(s, sub)Wo beginnt der erste Treffer?int (Byte-Offset)-1
Contains(s, sub)Ist sub enthalten?boolfalse
LastIndex(s, sub)Wo beginnt der letzte Treffer?int (Byte-Offset)-1

Faustregel: braucht der Code die Position für Slicing, ist Index/LastIndex richtig; geht es nur um Anwesenheit, sagt Contains das deutlicher.

In Config-Zeilen, Query-Strings oder Header-Werten taucht das Muster key=value ständig auf. Klassisch teilt man das mit Index am ersten = auf — der erste Treffer ist entscheidend, weil value selbst weitere = enthalten darf (etwa in base64).

Go kv_parsen.go
package main

import (
	"fmt"
	"strings"
)

// parseKV zerlegt "key=value" am ersten '='.
// Variante mit Index — funktioniert in jeder Go-Version.
func parseKV(line string) (key, value string, ok bool) {
	i := strings.Index(line, "=")
	if i < 0 {
		return "", "", false
	}
	return line[:i], line[i+1:], true
}

func main() {
	lines := []string{
		"host=db.example.com",
		"token=abc=def=",  // value enthaelt '='
		"broken-line",
	}
	for _, l := range lines {
		k, v, ok := parseKV(l)
		fmt.Printf("%-25s -> key=%q value=%q ok=%v\n", l, k, v, ok)
	}

	// Seit Go 1.18 idiomatischer mit strings.Cut:
	k, v, ok := strings.Cut("host=db.example.com", "=")
	fmt.Println("Cut:", k, v, ok)
}
Output
host=db.example.com       -> key="host" value="db.example.com" ok=true
token=abc=def=            -> key="token" value="abc=def=" ok=true
broken-line               -> key="" value="" ok=false
Cut: host db.example.com true

Für neuen Code ist strings.Cut die bessere Wahl: ein Aufruf liefert direkt beide Hälften plus ein ok-Flag, ohne dass man manuell mit Offsets hantiert. Index bleibt sinnvoll, wenn der Code Go 1.17 oder älter unterstützen muss oder wenn der Offset selbst — und nicht die Split-Hälften — gebraucht wird.

Für eine echte URL-Verarbeitung gehört net/url her, keine Frage. Für schnelle Vorab-Checks — etwa „Hat dieser String ein Schema?" oder „Welches Schema steht davor?" — ist Index aber das passende Werkzeug, weil der Separator :// einzigartig und kurz ist.

Go url_schema.go
package main

import (
	"fmt"
	"strings"
)

// splitScheme trennt "scheme://rest" am ersten "://".
func splitScheme(raw string) (scheme, rest string) {
	i := strings.Index(raw, "://")
	if i < 0 {
		return "", raw
	}
	return raw[:i], raw[i+3:] // +3 ueberspringt "://"
}

func main() {
	urls := []string{
		"https://mibeon.de/docs/go",
		"postgres://user:pw@db:5432/app",
		"file:///etc/hosts",
		"einfach-nur-text",
	}
	for _, u := range urls {
		scheme, rest := splitScheme(u)
		fmt.Printf("%-35s scheme=%-10q rest=%q\n", u, scheme, rest)
	}
}
Output
https://mibeon.de/docs/go           scheme="https"     rest="mibeon.de/docs/go"
postgres://user:pw@db:5432/app      scheme="postgres"  rest="user:pw@db:5432/app"
file:///etc/hosts                   scheme="file"      rest="/etc/hosts"
einfach-nur-text                    scheme=""          rest="einfach-nur-text"

Auffällig ist der file:///-Fall: der dritte Slash gehört bereits zum Pfad, der Host ist leer. Genau solche Edge-Cases zeigen, wann man von der eigenen Index-Lösung auf net/url.Parse wechseln sollte — alles, was über die simple Schema-Extraktion hinausgeht, ist dort robuster aufgehoben.

Byte-Offset, kein Rune-Offset

Der Rückgabewert ist eine Byte-Position in der UTF-8-Sequenz, keine Zeichen-Position. Bei reinem ASCII fallen beide zusammen, bei Umlauten und Emojis nicht.

-1 als Sentinel

Index nutzt -1 als Konvention für den Nicht-Treffer; vor jedem Slicing mit dem Ergebnis muss eine >= 0-Prüfung stehen, sonst gibt es einen Out-of-Range-Panic.

Leerer Substring liefert 0

Index(s, "") ist per Definition 0, weil der leere String an jeder Position vorkommt — ein häufiger Stolperstein bei ungefilterten Nutzereingaben.

Two-Way mit SIMD-Hot-Path intern

Die Implementierung kombiniert den Two-Way-Algorithmus mit handgeschriebener SIMD-Assembly für kurze Pattern auf amd64 und arm64 — in der Praxis sehr schnell.

Nur Existenz prüfen? Contains nehmen

Wenn das Ergebnis sowieso nur in einem if als Bool landet, drückt strings.Contains die Absicht klarer aus — Performance ist identisch.

Letztes Vorkommen via LastIndex

Index liefert immer den ersten Treffer; für den letzten Treffer (etwa bei Dateiendungen oder Pfad-Separatoren) gibt es strings.LastIndex.

Seit Go 1.18 ist Cut oft die bessere Wahl

Für das klassische „Index holen, links/rechts slicen"-Muster gibt es seit Go 1.18 strings.Cut, das beide Hälften plus ok-Flag in einem Aufruf liefert.

Threadsafe, weil rein lesend

Index greift nur lesend auf seine String-Argumente zu und hält keinen State — der Aufruf ist aus mehreren Goroutinen heraus gefahrlos parallelisierbar.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht