strings.IndexRune beantwortet die Frage „An welcher Stelle steht der Codepoint r in s?" — und zwar UTF-8-korrekt. Im Gegensatz zu IndexByte arbeitet die Funktion auf Rune-Ebene, dekodiert also die UTF-8-Sequenz und landet niemals mitten in einem Multi-Byte-Zeichen. Damit ist sie der richtige Weg, um Umlaute, Akzente, kyrillische Buchstaben oder Emojis in einem String zu lokalisieren.
Wichtig zu verstehen: Die Rückgabe ist ein Byte-Offset, kein Rune-Offset. Das mag zunächst überraschen, ist aber konsistent mit dem Rest des strings-Pakets — schließlich ist ein Go-String eine Byte-Sequenz, und s[i:] slict nach Bytes. IndexRune liefert genau den Index, mit dem man weiterarbeiten kann.
Die Signatur ist minimal — ein String, eine Rune, ein Integer-Ergebnis. Die Rune ist Gos 32-Bit-Integer-Typ für Unicode-Codepoints, nicht ein einzelnes Byte. Genau dieser Typunterschied macht den Aufruf semantisch sauberer als IndexByte für alles jenseits von ASCII.
func IndexRune(s string, r rune) intDer Rückgabetyp ist int, kein uint — das ermöglicht den -1-Sentinel für „nicht gefunden". Diese Konvention zieht sich durch alle Suchfunktionen des strings-Pakets und sollte in eigenen Wrappern beibehalten werden.
IndexRune iteriert intern rune-weise über den String und dekodiert dabei jede UTF-8-Sequenz. Damit ist garantiert, dass ein Treffer immer auf dem Start-Byte einer Rune liegt — niemals mitten in einer Multi-Byte-Sequenz. Genau das ist der entscheidende Sicherheitsgewinn gegenüber IndexByte, das stupide nach einem Byte sucht.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Schöner Tag"
// 'ö' ist in UTF-8 zwei Bytes: 0xC3 0xB6
i := strings.IndexRune(s, 'ö')
fmt.Println("Byte-Offset von 'ö':", i)
fmt.Println("Slice ab dort:", s[i:])
}Byte-Offset von 'ö': 4
Slice ab dort: öner TagDer Offset 4 ergibt sich, weil Sch drei ASCII-Bytes belegt und das ö an Byte-Position 4 beginnt. IndexByte(s, 0xC3) würde zwar denselben Offset liefern, wäre aber semantisch fragil: jedes Mehrbyte-Zeichen, das mit 0xC3 startet (z.B. Ã, é, ü), würde ebenfalls matchen. IndexRune ist hier eindeutig.
Wer aus Python oder JavaScript kommt, erwartet bei Stringsuche oft einen Zeichen-Index. Go macht das anders: der Rückgabewert ist die Position in Bytes, gemessen vom Stringanfang. Für reine ASCII-Strings ist das identisch, für UTF-8-Strings mit Multi-Byte-Zeichen weichen Byte- und Rune-Offset auseinander.
package main
import (
"fmt"
"strings"
"unicode/utf8"
)
func main() {
s := "Café"
byteOff := strings.IndexRune(s, 'é')
fmt.Println("Byte-Offset:", byteOff)
fmt.Println("Länge in Bytes:", len(s))
fmt.Println("Länge in Runen:", utf8.RuneCountInString(s))
// Rune-Offset selbst zählen
runeOff := 0
for _, r := range s {
if r == 'é' {
break
}
runeOff++
}
fmt.Println("Rune-Offset:", runeOff)
}Byte-Offset: 3
Länge in Bytes: 5
Länge in Runen: 4
Rune-Offset: 3Caf belegt drei Bytes, das é startet also an Byte-Position 3 — obwohl es das vierte Zeichen ist. Bei "Café" fallen Rune- und Byte-Offset zufällig zusammen, weil alle vorangehenden Zeichen ASCII sind. Sobald aber ein Multi-Byte-Zeichen davor steht, driften beide Werte auseinander. Wer den Rune-Offset wirklich braucht, muss selbst mit for i, r := range s zählen.
Eine Besonderheit betrifft den Sonderwert utf8.RuneError ('�', „Replacement Character"). Wenn man IndexRune mit diesem Wert aufruft, sucht die Funktion nicht nur nach dem echten U+FFFD-Codepoint, sondern zusätzlich nach invaliden UTF-8-Sequenzen im String. Das ist eine bewusste Designentscheidung, um ungültige Bytes detektieren zu können.
package main
import (
"fmt"
"strings"
"unicode/utf8"
)
func main() {
// Gültiger String mit echtem U+FFFD
a := "abc�xyz"
fmt.Println("echter FFFD:", strings.IndexRune(a, utf8.RuneError))
// Invalides UTF-8: 0xC3 ohne Folge-Byte
b := "abc\xC3xyz"
fmt.Println("invalides Byte:", strings.IndexRune(b, utf8.RuneError))
// Sauberer ASCII-String — kein Match
c := "abcxyz"
fmt.Println("sauber:", strings.IndexRune(c, utf8.RuneError))
}echter FFFD: 3
invalides Byte: 3
sauber: -1Praktisch heißt das: Mit strings.IndexRune(s, utf8.RuneError) >= 0 lässt sich prüfen, ob ein String entweder beschädigte UTF-8-Sequenzen enthält oder bereits einmal durch eine Ersetzung gelaufen ist. Wer beide Fälle unterscheiden muss, sollte stattdessen utf8.ValidString einsetzen.
Für alle Runen unterhalb von utf8.RuneSelf (= 128, also reines ASCII) erkennt IndexRune das und schaltet intern auf den gleichen Pfad wie IndexByte. Damit profitiert man bei ASCII-Suchen automatisch von der hochoptimierten SIMD-Implementierung, ohne den Aufrufcode ändern zu müssen.
package main
import (
"fmt"
"strings"
)
func main() {
s := "https://mibeon.de/docs"
// ASCII-Rune < 128 → interner Fast-Path
fmt.Println(strings.IndexRune(s, '/'))
fmt.Println(strings.IndexRune(s, ':'))
fmt.Println(strings.IndexRune(s, 'd'))
}6
5
17Die Konsequenz: man muss nicht zwischen IndexByte und IndexRune „aus Performance-Gründen" wählen, wenn man sowieso eine Rune-Konstante sucht. Für ASCII ist der Unterschied de facto null, für Nicht-ASCII bekommt man Korrektheit gratis dazu.
Die drei Suchfunktionen unterscheiden sich in Eingabetyp, UTF-8-Bewusstsein und typischem Einsatz. Die folgende Tabelle ordnet sie ein, damit die Wahl im Alltag schnell fällt.
| Funktion | Sucht nach | UTF-8-korrekt | Multi-Byte sicher | Bester Einsatz |
|---|---|---|---|---|
IndexByte(s, b byte) | einzelnes Byte | nein | nein — kann mitten in Rune treffen | ASCII-Separatoren (/, :, \n) |
IndexRune(s, r rune) | einzelner Codepoint | ja | ja | Umlaute, Akzente, Emojis |
Index(s, sub string) | Substring | ja (Byte-Match auf gültigem UTF-8) | ja | mehrzeichige Muster, Wörter |
Faustregel: ist das Gesuchte ein einzelnes ASCII-Zeichen und Performance kritisch, nimm IndexByte. Ist es ein einzelner Codepoint jenseits von ASCII, nimm IndexRune. Ist es ein Wort oder eine längere Sequenz, nimm Index.
In Deutsch-Texten kommt es regelmäßig vor, dass man ab dem ersten Umlaut oder Sonderzeichen slicen will — etwa um Sortierprefixe abzutrennen oder den ersten „interessanten" Teil eines Strings zu isolieren. IndexRune ist hier das richtige Werkzeug, weil es sicher auf den Rune-Anfang trifft.
package main
import (
"fmt"
"strings"
)
func sliceFromUmlaut(s string) string {
for _, r := range []rune{'ä', 'ö', 'ü', 'ß'} {
if i := strings.IndexRune(s, r); i >= 0 {
return s[i:]
}
}
return ""
}
func main() {
fmt.Printf("%q\n", sliceFromUmlaut("Mueller wird Müller"))
fmt.Printf("%q\n", sliceFromUmlaut("nur ASCII hier"))
}"Müller"
""Die Loop prüft mehrere Kandidaten und nimmt den ersten gefundenen — eine kleine Variante des „erste passende Rune"-Patterns. Wer die früheste Position über mehrere Runen hinweg braucht (statt nur die in Listenreihenfolge), müsste alle Treffer sammeln und das Minimum bilden oder zu IndexAny/IndexFunc greifen.
Emojis sind in UTF-8 meist vier Bytes lang. Wer in einem Text das erste Emoji finden und den String dort trennen will, muss zwingend rune-weise arbeiten — ein Byte-basierter Ansatz würde mitten im Codepoint landen und beim Ausgeben Müll produzieren.
package main
import (
"fmt"
"strings"
)
func splitAtEmoji(s string, emoji rune) (head, tail string, found bool) {
i := strings.IndexRune(s, emoji)
if i < 0 {
return s, "", false
}
return s[:i], s[i:], true
}
func main() {
msg := "Build erfolgreich ✅ alle Tests grün"
head, tail, ok := splitAtEmoji(msg, '✅')
if ok {
fmt.Printf("vor: %q\n", head)
fmt.Printf("ab: %q\n", tail)
}
// 4-Byte-Emoji (Rakete, U+1F680)
deploy := "Release v2.1 \U0001F680 ausgerollt"
h, t, _ := splitAtEmoji(deploy, '\U0001F680')
fmt.Printf("vor: %q\n", h)
fmt.Printf("ab: %q\n", t)
}vor: "Build erfolgreich "
ab: "✅ alle Tests grün"
vor: "Release v2.1 "
ab: "🚀 ausgerollt"Die Rakete U+1F680 belegt vier UTF-8-Bytes, das Häkchen U+2705 drei. IndexRune liefert in beiden Fällen den korrekten Start-Byte-Offset, und das anschließende Slicing produziert gültiges UTF-8. Bei IndexByte müsste man einzelne Bytes der UTF-8-Sequenz raten — fehleranfällig und in vielen Fällen schlicht falsch.
UTF-8-korrekt by design
IndexRune dekodiert intern rune-weise und trifft daher niemals mitten in einer Multi-Byte-Sequenz — der zurückgegebene Offset zeigt verlässlich auf das Start-Byte einer Rune.
Rückgabe ist Byte-Offset, nicht Rune-Offset
Der Index lässt sich direkt für s[i:] verwenden; wer den Zeichen-Index in Runen braucht, muss selbst mit for i, r := range s zählen.
ASCII-Fast-Path = IndexByte-Speed
Für Runen unter utf8.RuneSelf (128) schaltet IndexRune intern auf den IndexByte-Pfad mit derselben SIMD-Optimierung.
utf8.RuneError matcht auch invalides UTF-8
Wird utf8.RuneError gesucht, findet IndexRune sowohl den echten Codepoint U+FFFD als auch ungültige UTF-8-Bytes — praktisch zur Defekt-Erkennung.
-1 als Nicht-Treffer-Sentinel
Wie überall im strings-Paket signalisiert ein negativer Rückgabewert „nicht gefunden" — Prüfung über if i := ...; i >= 0 ist Standard-Idiom.
Sicherer als IndexByte für Multi-Byte-Zeichen
IndexByte trifft bei Umlauten oder Emojis Bytewerte, die in mehreren Codepoints vorkommen — IndexRune ist hier eindeutig und kollisionsfrei.
Für reine Existenzprüfung ContainsRune
Wenn nur interessiert ob die Rune vorkommt und nicht wo, ist ContainsRune lesbarer und semantisch passender.
Threadsafe und allokationsfrei
IndexRune liest nur und allokiert nichts — parallele Aufrufe auf demselben (immutable) String sind ohne Synchronisation sicher.