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.

Go grundlegend.go
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)
}
Output
1

Der 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.

Go cutset_vs_substring.go
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")
}
Output
0
0
0
9

Wä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".

Go multibyte.go
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
}
Output
3
3

Wichtig 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.

Go leer.go
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
}
Output
-1
-1
-1

Anders 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:

FunktionZweckRückgabeCutset / SubstringPrädikat
IndexAny(s, chars)erste Position aus Setint (Byte-Offset / -1)Cutset (Menge Runes)nein
Index(s, substr)erste Position von Substringint (Byte-Offset / -1)Substring (Sequenz)nein
ContainsAny(s, chars)enthält irgendein Zeichen aus Set?boolCutsetnein
IndexFunc(s, f)erste Position, an der f(r) wahr istint (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.

Go whitespace.go
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)
}
Output
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.

Go url_segment.go
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)
}
Output
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.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht