strings.IndexAny beantwortet die Frage: An welcher Byte-Position steht das erste Zeichen aus einer Menge möglicher Zeichen? Die Funktion bekommt einen String s und einen sogenannten Cutset chars — und sucht in s nach dem ersten Vorkommen irgendeines Runes, das in chars enthalten ist. Der Rückgabewert ist der Byte-Offset des Treffers oder -1, falls keines der Zeichen in s auftaucht.
Wichtig ist die Cutset-Semantik: chars wird nicht als Substring interpretiert, sondern als ungeordnete Menge einzelner Runes. IndexAny(s, "abc") liefert also die Position des ersten a, b oder c — je nachdem, welches Zeichen früher in s auftritt. Genau dieser Unterschied macht die Funktion zum Werkzeug für Trenner-Erkennung, einfache Tokenizer und für alle Stellen, an denen mehrere alternative Marker abgesucht werden sollen.
Die Signatur ist denkbar schlank: zwei Strings rein, ein int raus. Der erste Parameter ist der Suchstring, der zweite die Menge der erlaubten Zeichen. Die Funktion ist allokationsarm und durchläuft s linear, bis das erste Rune aus chars gefunden wurde.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Hallo Welt"
// Suche das erste Vorkommen von 'a', 'e' oder 'o'
idx := strings.IndexAny(s, "aeo")
fmt.Println(idx) // 1 ('a' an Position 1)
}1Der Treffer ist hier a an Index 1, obwohl im Cutset auch e und o stehen — IndexAny gibt immer den frühesten Fund zurück, unabhängig von der Reihenfolge der Zeichen in chars.
Der zweite Parameter heißt zwar formal chars string, verhält sich aber wie ein set[rune]. Reihenfolge, Wiederholungen und Position der Zeichen im Cutset spielen keine Rolle — Go zerlegt chars intern in Runes und prüft jedes Rune aus s gegen diese Menge. Genau das unterscheidet IndexAny von strings.Index, das nach einem zusammenhängenden Substring sucht.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Brot und Butter"
// IndexAny: erstes Zeichen, das 'x', 'B' oder 'u' ist
fmt.Println(strings.IndexAny(s, "xBu")) // 0 ('B' an 0)
fmt.Println(strings.IndexAny(s, "uBx")) // 0 Reihenfolge egal
fmt.Println(strings.IndexAny(s, "BBB")) // 0 Duplikate egal
// Index: sucht die exakte Sequenz "Bu"
fmt.Println(strings.Index(s, "Bu")) // 9 (Beginn von "Butter")
}0
0
0
9Während Index(s, "Bu") strikt nach dem Zwei-Zeichen-Muster B direkt gefolgt von u sucht, akzeptiert IndexAny(s, "Bu") jedes B oder jedes u — was immer zuerst kommt. Wer einen festen Substring sucht, nimmt Index; wer aus mehreren Alternativen den frühesten Treffer braucht, nimmt IndexAny.
IndexAny ist rune-aware bei der Suche, gibt aber einen Byte-Offset zurück. Das ist konsistent mit allen anderen Index-Funktionen im strings-Paket. Findet IndexAny ein Multi-Byte-Rune (z. B. einen Umlaut in UTF-8 oder ein Emoji), zeigt der Offset auf das erste Byte der UTF-8-Sequenz, nicht auf eine „Zeichen-Position".
package main
import (
"fmt"
"strings"
)
func main() {
s := "Café résumé"
// 'C' = 1 Byte, 'a' = 1, 'f' = 1, 'é' = 2 Bytes (0xC3 0xA9) → "é" beginnt bei Byte 3
idx := strings.IndexAny(s, "é")
fmt.Println(idx) // 3
// Emoji-Beispiel: 🌍 ist 4 Bytes
t := "Hi 🌍 Welt"
fmt.Println(strings.IndexAny(t, "🌍")) // 3
}3
3Wichtig für die Weiterverarbeitung: Der Rückgabewert ist sicher zum Slicen mit s[:idx] und s[idx:], weil er per Definition auf eine Rune-Grenze fällt. Wer eine Rune-Zähl-Position (statt Bytes) braucht, muss utf8.RuneCountInString(s[:idx]) darübersetzen.
Beide Randfälle sind klar definiert und führen nicht zu Panics: Ein leerer Cutset bedeutet „keine Zeichen erlaubt", ein leerer Suchstring „nichts zu durchsuchen". In beiden Fällen ist das Ergebnis -1. Damit lässt sich der Aufruf gefahrlos auch mit dynamisch zusammengesetzten Cutsets verwenden.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.IndexAny("hallo", "")) // -1 leerer Cutset
fmt.Println(strings.IndexAny("", "abc")) // -1 leerer Suchstring
fmt.Println(strings.IndexAny("", "")) // -1 beides leer
}-1
-1
-1Anders als bei strings.Index, wo ein leerer Substring 0 zurückgibt (weil der leere String an Position 0 „gefunden" wird), bleibt IndexAny mit leerem Cutset bei -1. Diese Asymmetrie ist bewusst — eine leere Menge enthält schlicht keine Zeichen, die man finden könnte.
Die vier Funktionen lösen verwandte, aber verschiedene Probleme. Die Tabelle fasst zusammen, wann welche das passende Werkzeug ist:
| Funktion | Zweck | Rückgabe | Cutset / Substring | Prädikat |
|---|---|---|---|---|
IndexAny(s, chars) | erste Position aus Set | int (Byte-Offset / -1) | Cutset (Menge Runes) | nein |
Index(s, substr) | erste Position von Substring | int (Byte-Offset / -1) | Substring (Sequenz) | nein |
ContainsAny(s, chars) | enthält irgendein Zeichen aus Set? | bool | Cutset | nein |
IndexFunc(s, f) | erste Position, an der f(r) wahr ist | int (Byte-Offset / -1) | – | ja (func(rune) bool) |
Faustregel: brauche ich nur ein „ja/nein", reicht ContainsAny. Brauche ich eine Position aus einer festen Menge Zeichen, nehme ich IndexAny. Brauche ich eine Position nach dynamischer Regel (z. B. „erste nicht-druckbare Rune"), nehme ich IndexFunc. Index ist für feste Sequenzen reserviert.
Ein klassischer Anwendungsfall: das erste Whitespace-Zeichen finden, um einen String in „Kopf" und „Rest" zu zerlegen. Das ist die Grundoperation hinter einfachen Tokenizern, Shell-Argument-Parsern oder Header-Zerlegern. Statt mehrerer Index-Aufrufe für Leerzeichen, Tab und Newline reicht ein einziger Cutset.
package main
import (
"fmt"
"strings"
)
// splitFirstWord trennt das erste Wort vom Rest am ersten Whitespace.
func splitFirstWord(s string) (head, rest string) {
idx := strings.IndexAny(s, " \t\n\r")
if idx < 0 {
return s, ""
}
return s[:idx], strings.TrimLeft(s[idx:], " \t\n\r")
}
func main() {
cmd, args := splitFirstWord("git\tcommit -m 'init'")
fmt.Printf("cmd=%q args=%q\n", cmd, args)
cmd2, args2 := splitFirstWord("noargs")
fmt.Printf("cmd=%q args=%q\n", cmd2, args2)
}cmd="git" args="commit -m 'init'"
cmd="noargs" args=""Der Cutset " \t\n\r" deckt die häufigsten ASCII-Whitespaces ab. Wer Unicode-Whitespace (z. B. NBSP, geschütztes Leerzeichen) ebenfalls erfassen muss, sollte auf strings.IndexFunc(s, unicode.IsSpace) umsteigen — das ist die rune-basierte Variante mit Prädikat.
Beim Zerlegen einer URL-Komponente will man häufig wissen, wo der Pfad-Teil endet — also wo das erste Sonderzeichen ? (Query-Start), # (Fragment-Start) oder / (nächstes Pfad-Segment) auftaucht. Statt drei Index-Calls plus min-Logik genügt ein IndexAny-Aufruf mit dem passenden Cutset.
package main
import (
"fmt"
"strings"
)
// splitURLSegment liefert das erste Pfad-Segment und den Rest (inkl. Trenner).
func splitURLSegment(path string) (segment, rest string) {
idx := strings.IndexAny(path, "/?#")
if idx < 0 {
return path, ""
}
return path[:idx], path[idx:]
}
func main() {
seg, rest := splitURLSegment("users/42?fields=name#top")
fmt.Printf("segment=%q rest=%q\n", seg, rest)
seg2, rest2 := splitURLSegment("just-one-segment")
fmt.Printf("segment=%q rest=%q\n", seg2, rest2)
seg3, rest3 := splitURLSegment("api?v=2")
fmt.Printf("segment=%q rest=%q\n", seg3, rest3)
}segment="users" rest="/42?fields=name#top"
segment="just-one-segment" rest=""
segment="api" rest="?v=2"Der Trick: der Trenner selbst bleibt im rest enthalten, sodass der Aufrufer entscheiden kann, ob er ihn als Marker behandelt oder weiter wegschneidet. Für produktive URL-Parser sollte man natürlich net/url verwenden — der Helper hier zeigt nur das Pattern, das auch in Templating-, Routing- und einfachen Konfig-Parsern täglich gebraucht wird.
chars ist ein Cutset, kein Substring
IndexAny(s, "abc") sucht das erste a, b oder c — nicht die Sequenz abc. Reihenfolge und Duplikate im Cutset sind irrelevant.
Rune-aware Suche, Byte-Offset als Rückgabe
Multi-Byte-Runes im Cutset werden korrekt als ganze Zeichen erkannt; der zurückgegebene Offset zeigt aber auf das erste Byte der UTF-8-Sequenz.
Offset fällt immer auf eine Rune-Grenze
s[:idx] und s[idx:] sind sicher zu slicen — der Treffer endet nie mitten in einer Multi-Byte-Folge.
-1 bei jedem Nicht-Treffer
Egal ob s leer, chars leer oder schlicht kein Zeichen aus chars in s enthalten — das Ergebnis ist konsistent -1.
ContainsAny ist die Bool-Variante
Wenn nur die Existenz interessiert, nicht die Position, vermeidet strings.ContainsAny einen >= 0-Vergleich am Aufrufer.
IndexFunc für dynamische Bedingungen
Sobald die Treffermenge nicht statisch ist (Unicode-Kategorien, Whitespace im weiten Sinne, eigene Regeln), ist IndexFunc mit Prädikat die saubere Wahl.
Threadsafe ohne State
IndexAny hat keinen internen Zustand und ist gefahrlos aus mehreren Goroutinen parallel aufrufbar.
Schneller als regex-Zeichenklassen
Für Aufgaben wie [abc] ist IndexAny um Größenordnungen günstiger als regexp — keine Compile-Phase, kein State-Machine-Overhead.