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.

Go signature.go
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"))
}
Output
true
false

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

Go empty.go
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
}
Output
true
true
false
false

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

Go utf8.go
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
}
Output
true
true
true
false

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

Go perf.go
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"))
}
Output
true
true

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

FunktionFrageRückgabeMatch-Einheit
ContainsKommt die Bytefolge substr in s vor?boolfeste Sequenz
IndexAn welcher Byte-Position beginnt der Treffer?intfeste Sequenz
ContainsAnyKommt irgendeine Rune aus chars in s?booleinzelne Codepunkte
ContainsRuneKommt diese eine Rune in s vor?booleinzelner Codepunkt
EqualFoldSind zwei Strings case-insensitiv gleich?boolgesamter 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).

Go log_filter.go
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)
	}
}
Output
2026-05-22 10:00:03 ERROR db connection refused
2026-05-22 10:00:05 FATAL out of memory

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

Go route_dispatch.go
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"))
}
Output
handler: api v2
handler: api v1 (deprecated)
handler: health
handler: static

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

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht