strings.IndexFunc durchläuft einen String rune-weise und gibt den Byte-Offset der ersten Rune zurück, für die ein übergebenes Prädikat func(rune) bool den Wert true liefert. Findet die Iteration keine solche Rune, liefert die Funktion -1. Damit ist sie die positionssuchende Schwester von ContainsFunc und ergänzt das Cutset-basierte IndexAny um den vollen Ausdruck eines beliebigen Codepoint-Tests.
Der eigentliche Wert von IndexFunc liegt in der Kombination mit dem unicode-Paket: Statt fest verdrahtete Zeichenmengen zu pflegen, beschreibt das Prädikat eine ganze Unicode-Kategorie — Whitespace, Ziffern, Interpunktion, Steuerzeichen. Genauso erlauben Closures eigene, kontextabhängige Regeln, ohne den String mehrfach zu scannen.
Die Signatur ist denkbar schlank: ein String und ein Prädikat. Der Rückgabewert ist ein int — entweder ein Byte-Offset in den Quellstring oder -1 als Marker für „nicht gefunden".
func IndexFunc(s string, f func(rune) bool) intWichtig ist die Asymmetrie zwischen Iteration und Rückgabe: Das Prädikat sieht Runen (Codepoints), die Funktion liefert aber den Byte-Index — exakt den Wert, den man danach für s[:i] und s[i:] braucht.
IndexFunc decodiert den String intern als UTF-8 und ruft das Prädikat einmal pro Rune auf. Multi-Byte-Codepoints werden also korrekt als ein einziger Codepoint betrachtet — das Prädikat muss nie selbst UTF-8 dekodieren.
package main
import (
"fmt"
"strings"
)
func istBuchstabe(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}
func main() {
fmt.Println(strings.IndexFunc("123abc", istBuchstabe))
fmt.Println(strings.IndexFunc("...!?", istBuchstabe))
}3
-1Das Prädikat ist eine ganz normale Go-Funktion — testbar, wiederverwendbar, frei von Side-Effects (idealerweise). Genau diese Trennung von „wie suche ich" und „wonach suche ich" macht IndexFunc so flexibel.
Der zurückgegebene Index ist ein Byte-Offset, kein Rune-Index. Sobald vor der Treffer-Rune Multi-Byte-Zeichen stehen, ist dieser Offset entsprechend größer als die reine „Position" im umgangssprachlichen Sinn.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
s := "Grüße!"
i := strings.IndexFunc(s, unicode.IsPunct)
fmt.Println("Byte-Offset:", i)
fmt.Println("Treffer :", string(s[i]))
fmt.Println("Davor :", s[:i])
}Byte-Offset: 7
Treffer : !
Davor : Grüßeü und ß belegen jeweils zwei Bytes in UTF-8, deshalb landet das Ausrufezeichen bei Byte 7, obwohl es die sechste Rune ist. Wer eine rune-zählende Position braucht, kombiniert IndexFunc mit utf8.RuneCountInString(s[:i]).
Das Standardpaket unicode liefert eine ganze Sammlung fertiger Prädikate — IsSpace, IsDigit, IsLetter, IsPunct, IsControl, IsUpper, IsLower und viele mehr. Diese Funktionen erfüllen exakt die Signatur func(rune) bool und passen direkt in IndexFunc.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
s := "Version v1.2.3-beta"
fmt.Println(strings.IndexFunc(s, unicode.IsSpace))
fmt.Println(strings.IndexFunc(s, unicode.IsDigit))
fmt.Println(strings.IndexFunc(s, unicode.IsPunct))
}7
9
10Der Charme liegt darin, dass diese Tests kategoriell definiert sind — unicode.IsDigit erkennt nicht nur 0–9, sondern alle Unicode-Codepoints der Kategorie Nd (Decimal Number). Wer das nicht möchte, schreibt ein eigenes, engeres ASCII-Prädikat.
Wird die Bedingung kontextabhängig — etwa abhängig von einer Konfiguration oder einem Set verbotener Zeichen — bietet sich eine Closure als Prädikat an. Sie kann Variablen aus dem umgebenden Scope einfangen und vermeidet globale Hilfsfunktionen.
package main
import (
"fmt"
"strings"
)
func main() {
verboten := map[rune]bool{'<': true, '>': true, '&': true}
istVerboten := func(r rune) bool {
return verboten[r]
}
fmt.Println(strings.IndexFunc("safe text", istVerboten))
fmt.Println(strings.IndexFunc("a<b>c", istVerboten))
}-1
1Das Set lebt im äußeren Scope, das Prädikat fängt es per Closure ein. So bleibt der Code lokal, und das Set lässt sich problemlos aus Konfiguration oder Funktionsparametern befüllen, ohne IndexFunc selbst anzupassen.
Die drei Funktionen wirken auf den ersten Blick ähnlich, decken aber unterschiedliche Bedürfnisse ab. Die folgende Tabelle stellt sie nebeneinander.
| Funktion | Auswahl-Mechanismus | Rückgabe | Wann nutzen |
|---|---|---|---|
strings.IndexFunc | Prädikat func(rune) bool | Byte-Offset / -1 | Position der ersten passenden Rune gesucht |
strings.ContainsFunc | Prädikat func(rune) bool | bool | Nur Existenz interessiert, keine Position |
strings.IndexAny | Cutset-String | Byte-Offset / -1 | Feste, statische Zeichenmenge ohne Logik |
IndexFunc ist also die richtige Wahl, sobald die Bedingung Logik oder Unicode-Kategorien braucht und man die genaue Stelle weiterverarbeiten will. ContainsFunc reicht für reine Ja/Nein-Tests, IndexAny für statische Cutsets wie ",;|".
Ein klassisches Muster: Den String am ersten Whitespace in das erste Wort und den Rest aufteilen — etwa um Kommandos zu parsen, bei denen befehl rest... typisch ist. IndexFunc mit unicode.IsSpace liefert exakt den nötigen Byte-Offset.
package main
import (
"fmt"
"strings"
"unicode"
)
func wortUndRest(s string) (string, string) {
i := strings.IndexFunc(s, unicode.IsSpace)
if i < 0 {
return s, ""
}
return s[:i], strings.TrimLeftFunc(s[i:], unicode.IsSpace)
}
func main() {
w, r := wortUndRest("git commit -m hallo")
fmt.Printf("wort=%q rest=%q\n", w, r)
w, r = wortUndRest("solo")
fmt.Printf("wort=%q rest=%q\n", w, r)
}wort="git" rest="commit -m hallo"
wort="solo" rest=""TrimLeftFunc räumt zusätzliche Whitespace-Bytes nach dem Treffer weg, sodass rest direkt mit dem nächsten Token beginnt. Für einen vollständigen Tokenizer ist strings.Fields zwar bequemer, aber für „Kopf vs. Rest" ist das IndexFunc-Muster sehr direkt und ohne Allokation für die Felder-Slice.
Ein zweites typisches Szenario: In einem Identifier wie app-server-2025 oder release-v3 soll der numerische Suffix gefunden werden. IndexFunc mit unicode.IsDigit liefert den Byte-Offset, ab dem die Zahl beginnt.
package main
import (
"fmt"
"strings"
"unicode"
)
func numSuffix(s string) (prefix, num string) {
i := strings.IndexFunc(s, unicode.IsDigit)
if i < 0 {
return s, ""
}
return s[:i], s[i:]
}
func main() {
p, n := numSuffix("release-v3.1.2")
fmt.Printf("prefix=%q num=%q\n", p, n)
p, n = numSuffix("app-server-2025")
fmt.Printf("prefix=%q num=%q\n", p, n)
p, n = numSuffix("daemon")
fmt.Printf("prefix=%q num=%q\n", p, n)
}prefix="release-v" num="3.1.2"
prefix="app-server-" num="2025"
prefix="daemon" num=""Das Muster ist robust gegen jede Trennzeichen-Variation — -, _, ., sogar Buchstaben dazwischen — weil das Prädikat exakt eine Eigenschaft beschreibt: „erste Ziffer". Für striktere Formate (z. B. nur SemVer hinten dran) lässt sich das Prädikat zu einer Closure mit Kontext erweitern, ohne den Aufrufstil zu ändern.
Rune-weise Iteration
IndexFunc decodiert UTF-8 intern und ruft das Prädikat einmal pro Codepoint auf — Multi-Byte-Zeichen werden korrekt als eine Rune gesehen.
Byte-Offset als Rückgabe
Der zurückgegebene Index zählt Bytes, nicht Runen — direkt brauchbar für s[:i] / s[i:], aber nicht identisch mit der Rune-Position.
-1 bei Nicht-Treffer
Findet keine Rune das Prädikat, liefert IndexFunc den Wert -1; das ist der eindeutige Marker für „nicht gefunden".
Ideal mit unicode-Paket
unicode.IsSpace, IsDigit, IsLetter, IsPunct u. a. erfüllen die Prädikat-Signatur direkt — keine Adapter nötig.
Closures erlauben Capture
Eine Closure kann Sets, Konfigurationen oder Schwellen aus dem umgebenden Scope einfangen und in die Suche tragen.
ContainsFunc für nur-Bool
Wenn nur die Existenz, nicht die Position interessiert, ist ContainsFunc ausdrucksstärker und braucht weniger Begleitcode.
IndexAny für statisches Cutset
Für eine feste Zeichenmenge ohne Logik ist IndexAny mit Cutset-String einfacher und kommt ohne Prädikat-Allokation aus.
Threadsafe wenn Prädikat es ist
IndexFunc selbst hat keinen verborgenen Zustand — die Nebenläufigkeitssicherheit ergibt sich aus dem übergebenen Prädikat.