strings.Contains ist das einfachste Prädikat im strings-Paket: Es beantwortet die Frage „Kommt substr irgendwo in s als zusammenhängende Bytefolge vor?" mit true oder false. Die Funktion ist ein dünner Wrapper über strings.Index(s, substr) >= 0 und damit semantisch identisch — nur lesbarer, wenn die Position selbst nicht benötigt wird.
Intern arbeitet Contains byte-orientiert und ist trotzdem UTF-8-sicher: Weil die Bytes einer Mehrbyte-Sequenz in gültigem UTF-8 niemals mit Anfangsbytes anderer Codepunkte kollidieren, kann ein Byte-Match nur dort entstehen, wo auch ein echter Codepunkt-Match liegt. Damit eignet sich Contains als Standardwerkzeug für Substring-Checks in Filtern, Routing, Log-Auswertung und Konfigurationsparsing.
Die Signatur ist minimal: zwei Strings rein, ein Bool raus. Es gibt keine Optionen, keine Flags und keinen Fehlerwert — die Antwort ist immer eindeutig, weil ein Substring entweder vorhanden ist oder nicht.
package main
import (
"fmt"
"strings"
)
// func Contains(s, substr string) bool
func main() {
fmt.Println(strings.Contains("mibeon.de/go/stdlib", "stdlib"))
fmt.Println(strings.Contains("mibeon.de/go/stdlib", "rust"))
}true
falseDer erste Aufruf findet "stdlib" als zusammenhängende Bytefolge im Pfad, der zweite scheitert. Beide Aufrufe laufen in derselben Zeitkomplexität wie Index, da Contains nur dessen Rückgabe mit >= 0 vergleicht.
Das Verhalten bei leeren Eingaben folgt der mathematischen Konvention: Der leere String ist Teilstring jedes Strings — auch seiner selbst. Wer also ungeprüft Nutzereingaben in substr durchreicht, bekommt für leere Eingaben immer ein true und filtert nichts mehr aus.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.Contains("hallo", "")) // leerer substr immer drin
fmt.Println(strings.Contains("", "")) // beide leer
fmt.Println(strings.Contains("", "x")) // s leer, substr nicht
fmt.Println(strings.Contains("a", "ab")) // substr länger als s
}true
true
false
falseDie letzten beiden Fälle zeigen die zweite Symmetrie: Sobald substr länger ist als s, kann kein Match existieren. In Filterpipelines lohnt sich daher ein expliziter len(substr) == 0-Check, bevor der Nutzer-Suchbegriff in Contains wandert.
Contains vergleicht Bytes, keine Runen — und trotzdem liefert es für gültiges UTF-8 das semantisch richtige Ergebnis. Der Grund liegt in der Konstruktion von UTF-8: Fortsetzungsbytes (10xxxxxx) treten nur nach einem entsprechenden Startbyte auf, und Startbytes mehrbytiger Sequenzen unterscheiden sich vom ASCII-Bereich. Eine zufällige Byte-Übereinstimmung mitten in einer Codepunkt-Sequenz ist damit ausgeschlossen.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Schöne Grüße aus München"
fmt.Println(strings.Contains(s, "ö")) // 2 Bytes, gefunden
fmt.Println(strings.Contains(s, "Grüße")) // ganzes Wort mit ß
fmt.Println(strings.Contains(s, "ü")) // 2 Bytes
fmt.Println(strings.Contains(s, "Berlin")) // nicht enthalten
}true
true
true
falseDie Bytefolge von ö (0xC3 0xB6) erscheint im Quelltext exakt dort, wo das ö rune-mäßig steht — und nirgends sonst. Bei korrupten Byte-Strings, die keine valide UTF-8-Darstellung mehr sind, bleibt der Vergleich rein bytebasiert; das ist meist gewünscht, weil die Funktion damit auch auf Binärpuffern definiert ist.
Die Standardbibliothek wählt für Index (und damit für Contains) je nach Substring-Länge unterschiedliche Strategien: Für sehr kurze Pattern greift ein optimierter Brute-Force-Scan, für längere Pattern ein Two-Way-Algorithmus mit linearer Laufzeit ohne Backtracking. Auf amd64 und arm64 kommen SIMD-beschleunigte Suchschleifen für das erste Byte hinzu, die in der Praxis selten zu schlagen sind.
package main
import (
"fmt"
"strings"
)
// Ein manueller Loop ist fast immer langsamer und immer fehleranfälliger.
func containsManual(s, sub string) bool {
if len(sub) == 0 {
return true
}
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
func main() {
s := strings.Repeat("ab", 1_000_000) + "needle"
fmt.Println(strings.Contains(s, "needle"))
fmt.Println(containsManual(s, "needle"))
}true
trueDie Faustregel ist eindeutig: Wer keine sehr spezielle Suchsemantik braucht (etwa Regex oder Aho-Corasick für viele Pattern gleichzeitig), greift zu Contains. Eigene Schleifen verlieren nicht nur Geschwindigkeit, sondern auch die Architektur-spezifischen Mikrooptimierungen, die mit jedem Go-Release nachgepflegt werden.
Die drei Funktionen klingen ähnlich, beantworten aber unterschiedliche Fragen. Die Tabelle zeigt, wann welche Variante zum Problem passt — und warum die falsche Wahl meist daran liegt, dass „Zeichensatz" und „feste Sequenz" verwechselt werden.
| Funktion | Frage | Rückgabe | Match-Einheit |
|---|---|---|---|
Contains | Kommt die Bytefolge substr in s vor? | bool | feste Sequenz |
Index | An welcher Byte-Position beginnt der Treffer? | int | feste Sequenz |
ContainsAny | Kommt irgendeine Rune aus chars in s? | bool | einzelne Codepunkte |
ContainsRune | Kommt diese eine Rune in s vor? | bool | einzelner Codepunkt |
EqualFold | Sind zwei Strings case-insensitiv gleich? | bool | gesamter String |
Contains("hallo", "lo") findet die Sequenz l gefolgt von o. ContainsAny("hallo", "lo") ist hingegen schon bei l zufrieden, weil es jeden Buchstaben einzeln prüft. Diese Unterscheidung ist die häufigste Fehlerquelle bei der ersten Begegnung mit dem strings-Paket.
In der Log-Auswertung will man oft nur Zeilen sehen, die ein bestimmtes Level oder eine Fehlersignatur tragen. Contains reicht hier vollständig aus, solange das Log nicht strukturiert (JSON) ist — und ist deutlich schneller als ein Regex-Match auf (ERROR|FATAL).
package main
import (
"bufio"
"fmt"
"strings"
)
func filterCritical(input string) []string {
var out []string
scanner := bufio.NewScanner(strings.NewReader(input))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "ERROR") || strings.Contains(line, "FATAL") {
out = append(out, line)
}
}
return out
}
func main() {
log := `2026-05-22 10:00:01 INFO app started
2026-05-22 10:00:03 ERROR db connection refused
2026-05-22 10:00:04 WARN retry in 1s
2026-05-22 10:00:05 FATAL out of memory`
for _, l := range filterCritical(log) {
fmt.Println(l)
}
}2026-05-22 10:00:03 ERROR db connection refused
2026-05-22 10:00:05 FATAL out of memoryZwei Contains-Aufrufe sind hier billiger als ein vorkompiliertes regexp.MustCompile("ERROR|FATAL"), weil jeder Aufruf nur einen optimierten Byte-Scan auslöst. Bei mehr als einer Handvoll Pattern lohnt sich ein Wechsel auf einen Aho-Corasick-Matcher oder, falls Reihenfolge egal ist, eine map[string]struct{} mit Fields-Tokenisierung.
Ganz simple Dispatcher (Migrations-Skripte, Reverse-Proxy-Filter, Debug-Endpunkte) lassen sich mit Contains umsetzen, ohne einen kompletten Router einzubinden. Für produktive HTTP-Server ist net/http.ServeMux oder ein Router wie chi natürlich vorzuziehen — der folgende Code zeigt das Muster bewusst reduziert.
package main
import (
"fmt"
"strings"
)
func dispatch(path string) string {
switch {
case strings.Contains(path, "/api/v2/"):
return "handler: api v2"
case strings.Contains(path, "/api/v1/"):
return "handler: api v1 (deprecated)"
case strings.Contains(path, "/healthz"):
return "handler: health"
default:
return "handler: static"
}
}
func main() {
fmt.Println(dispatch("/api/v2/users/42"))
fmt.Println(dispatch("/api/v1/legacy"))
fmt.Println(dispatch("/healthz"))
fmt.Println(dispatch("/assets/logo.svg"))
}handler: api v2
handler: api v1 (deprecated)
handler: health
handler: staticDer Trick mit dem trailing Slash in /api/v2/ verhindert, dass /api/v20/ versehentlich auf den v2-Handler fällt — ein typischer Bug bei naiven Contains-Routern. Sobald aber Pfad-Parameter, Methoden oder Reihenfolge eine Rolle spielen, sollte das Muster auf strings.HasPrefix plus einen echten Router umziehen.
Wrapper über Index >= 0
Contains(s, substr) ist definiert als Index(s, substr) >= 0 — semantisch identisch, nur ohne Positionsinformation.
Leerer Substring ist immer enthalten
Contains(s, "") liefert für jedes s ein true, auch für s == "". Nutzereingaben vor dem Aufruf prüfen.
Byte-orientiert, aber UTF-8-sicher
Die Suche vergleicht Bytes. Bei gültigem UTF-8 entstehen keine falschen Treffer, weil Fortsetzungsbytes nicht mit ASCII-Bytes kollidieren.
Schneller als manuelle Schleifen
Die Standardbibliothek nutzt Two-Way-Suche und SIMD-Hilfen — eigener Code in Go-Userland kommt selten an die Laufzeit heran.
Für Zeichensätze gibt es ContainsAny
ContainsAny(s, chars) matcht jede einzelne Rune aus chars, nicht die Sequenz. Häufige Verwechslung mit Contains.
Für Positionen gibt es Index
Wer den Treffer-Offset braucht (z. B. zum Splitten), greift direkt zu Index oder LastIndex statt Contains.
Threadsafe durch Read-Only-Strings
Go-Strings sind unveränderlich; Contains mutiert nichts und lässt sich aus beliebig vielen Goroutinen parallel aufrufen.
Case-sensitiv per Default
Contains unterscheidet Groß-/Kleinschreibung. Für case-insensitive Vergleiche bietet sich EqualFold oder ToLower plus Contains an.