strings.ContainsAny beantwortet eine einfache, aber häufig gestellte Frage: Steckt in einem String irgendeine Rune aus einer vorgegebenen Zeichenmenge? Die Funktion liefert ein nacktes bool, ohne Position und ohne Trefferanzahl — sie eignet sich also als reines Prädikat in if-Bedingungen und Validierungspfaden.
Das zweite Argument chars ist ausdrücklich ein Cutset, also eine Menge einzelner Unicode-Codepoints, und kein zu suchender Substring. Reihenfolge und Mehrfachnennungen darin spielen keine Rolle. Intern arbeitet ContainsAny rune-basiert, weshalb auch Multi-Byte-UTF-8-Zeichen wie Umlaute oder Emojis sauber erkannt werden.
Die Funktion ist eines der schlankesten Prädikate im strings-Paket. Sie nimmt zwei Strings entgegen und gibt einen Wahrheitswert zurück — kein Index, kein Fehler, keine Allokation.
func ContainsAny(s, chars string) bools ist der zu durchsuchende Text, chars die Zeichenmenge. Ein true bedeutet: mindestens eine Rune aus chars taucht irgendwo in s auf.
Der häufigste Stolperstein bei ContainsAny ist die Verwechslung mit Contains. Während Contains einen festen, zusammenhängenden Substring sucht, behandelt ContainsAny sein zweites Argument als Menge: jedes Zeichen darin ist ein eigener Suchkandidat. ContainsAny("Hallo", "xyz") ist false, ContainsAny("Hallo", "xya") ist true, sobald die Rune a in s vorkommt.
Genau deshalb sind auch Reihenfolge und Doppelungen im Cutset bedeutungslos. "abc", "cba" und "aabbcc" sind als Cutsets exakt äquivalent.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Hallo Welt"
// Cutset: irgendeine dieser Runes?
fmt.Println(strings.ContainsAny(s, "xyz")) // false
fmt.Println(strings.ContainsAny(s, "xyW")) // true (W trifft)
// Reihenfolge und Doppelungen egal
fmt.Println(strings.ContainsAny(s, "Wxy")) // true
fmt.Println(strings.ContainsAny(s, "WWWy")) // true
// Vergleich mit Contains: dort ist "Wxy" EIN Substring
fmt.Println(strings.Contains(s, "Wxy")) // false
}false
true
true
true
falseWer das Cutset-Konzept einmal verinnerlicht hat, vermeidet eine ganze Klasse von Bugs, in denen ein gemeinter Substring-Test heimlich zur Mengenprüfung wird.
Beide Randfälle leerer Strings liefern dieselbe Antwort, allerdings aus unterschiedlichen Gründen. Ein leeres Cutset enthält keine Runes, die getroffen werden könnten — das Ergebnis ist daher zwingend false. Ein leeres s enthält schlicht keine Zeichen, die einem Cutset angehören könnten, also ebenfalls false.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ContainsAny("Hallo", "")) // false, Cutset leer
fmt.Println(strings.ContainsAny("", "abc")) // false, s leer
fmt.Println(strings.ContainsAny("", "")) // false
}false
false
falseDiese Symmetrie ist praktisch, weil keine Sonderfallbehandlung im aufrufenden Code nötig ist — leere Eingaben fallen einfach durch.
ContainsAny iteriert intern über Runes, nicht über Bytes. Dadurch sind auch Multi-Byte-Codepoints wie deutsche Umlaute oder Emojis vollwertige Mitglieder des Cutsets. Ein ä belegt in UTF-8 zwei Bytes, wird aber als eine einzige Rune behandelt — eine byteweise Suche würde hier falsche Treffer produzieren.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Schöne Grüße aus München 🍻"
// Umlaute als Cutset-Mitglieder
fmt.Println(strings.ContainsAny(s, "äöü")) // true (ö, ü treffen)
fmt.Println(strings.ContainsAny(s, "áéí")) // false
// Emojis als Runes im Cutset
fmt.Println(strings.ContainsAny(s, "🍕🍻🍷")) // true (🍻 trifft)
fmt.Println(strings.ContainsAny(s, "🍕🍷")) // false
}true
false
true
falseFür die meisten internationalen Texte ist dieses Verhalten genau das, was man erwartet — anders als bei naiver Byte-Suche bleiben gemischte Zeichen aus verschiedenen Sprachen korrekt prüfbar.
Die drei Funktionen bilden ein zusammengehöriges Trio, unterscheiden sich aber klar in Rückgabewert und Semantik des zweiten Arguments. Wer den Unterschied im Kopf hat, greift in fast jedem Such-Szenario zum richtigen Werkzeug.
| Funktion | Rückgabe | Zweites Argument | Frage |
|---|---|---|---|
ContainsAny | bool | Cutset (Runes-Menge) | Kommt irgendeine Rune vor? |
Contains | bool | fester Substring | Kommt der genaue Substring vor? |
IndexAny | int (-1 = nein) | Cutset (Runes-Menge) | Wo kommt eine Rune zuerst vor? |
Faustregel: Contains für ganze Substrings, ContainsAny für reine Ja/Nein-Fragen über eine Zeichenmenge, IndexAny wenn die Trefferposition gebraucht wird.
Ein klassisches Einsatzfeld ist die Validierung von Benutzereingaben. Bevor ein Username in eine URL eingebaut oder in HTML eingebettet wird, lohnt sich eine schnelle Vorprüfung auf strukturzerstörende Zeichen wie <, >, ", ' oder &. ContainsAny reduziert diese Prüfung auf eine einzige, gut lesbare Zeile.
package main
import (
"fmt"
"strings"
)
const forbidden = `<>"'&`
func isValidUsername(name string) bool {
if name == "" {
return false
}
return !strings.ContainsAny(name, forbidden)
}
func main() {
candidates := []string{
"michael",
"max_mustermann",
`<script>`,
"alice&bob",
"O'Reilly",
}
for _, c := range candidates {
fmt.Printf("%-20s -> %v\n", c, isValidUsername(c))
}
}michael -> true
max_mustermann -> true
<script> -> false
alice&bob -> false
O'Reilly -> falseWichtig: ContainsAny ersetzt keine vollständige Eingabevalidierung oder ein sauberes Escaping — es ist ein billiger Vorfilter, der offensichtlich riskante Eingaben früh ausschließt.
Beim Einlesen unbekannter Tabellendateien hilft eine schnelle Trennzeichen-Heuristik, bevor ein vollwertiger Parser gewählt wird. Die erste nicht-leere Zeile reicht oft, um zwischen Komma-, Semikolon- und Tab-getrennten Formaten zu unterscheiden.
package main
import (
"fmt"
"strings"
)
func sniffSeparator(line string) string {
switch {
case strings.ContainsAny(line, "\t"):
return "tab"
case strings.ContainsAny(line, ";"):
return "semicolon"
case strings.ContainsAny(line, ","):
return "comma"
default:
return "unknown"
}
}
func main() {
lines := []string{
"id,name,email",
"id;name;email",
"id\tname\temail",
"einzelspalte",
}
for _, l := range lines {
fmt.Printf("%-25q -> %s\n", l, sniffSeparator(l))
}
}"id,name,email" -> comma
"id;name;email" -> semicolon
"id\tname\temail" -> tab
"einzelspalte" -> unknownFür robustere Erkennung würde man mehrere Zeilen sampeln und die Trennzeichen zählen — aber als erste Weichenstellung in einer Importpipeline ist ContainsAny ausreichend und sehr schnell.
chars ist Cutset, kein Substring
Das zweite Argument wird Rune für Rune als Menge gelesen. ContainsAny(s, "abc") fragt nicht nach dem Substring abc, sondern nach mindestens einem der Zeichen a, b oder c.
Rune-aware: Multi-Byte sicher
Iteration läuft über Codepoints, nicht über Bytes. Umlaute wie ä oder Emojis wie 🍻 funktionieren als Cutset-Mitglieder korrekt.
Leeres Cutset liefert immer false
ContainsAny(s, "") ist stets false, weil eine leere Menge keine Treffer enthalten kann — keine Sonderbehandlung nötig.
Ersetzt Ketten aus mehreren Contains
Statt Contains(s, "a") || Contains(s, "b") || Contains(s, "c") reicht ein einziger Aufruf ContainsAny(s, "abc") — kürzer, schneller, lesbarer.
Schneller als Regex für reine Zeichensets
Wenn nur geprüft wird, ob irgendein Zeichen aus einer festen Menge vorkommt, ist ContainsAny einer Regex-Klasse wie [abc] klar überlegen — kein Compile-Schritt, keine NFA-Maschinerie.
Threadsafe — reine Lesefunktion
Die Funktion mutiert keinen geteilten Zustand und kann gefahrlos aus mehreren Goroutinen heraus auf denselben Strings aufgerufen werden.
Case-sensitiv
A und a sind verschiedene Runes. Für case-insensitive Prüfung vorher strings.ToLower auf s und chars anwenden oder ContainsFunc mit eigener Vergleichslogik nutzen.
Für die Position: IndexAny
ContainsAny verwirft den Offset des Treffers. Wer wissen will, wo das erste Set-Zeichen steht, greift zu IndexAny, das -1 oder den Byte-Offset zurückgibt.