strings.HasPrefix ist das einfachste Prädikat im strings-Paket: es liefert true, wenn der String s mit der Zeichenfolge prefix beginnt, und sonst false. Hinter den Kulissen passiert nichts Magisches — die Funktion vergleicht zunächst, ob s lang genug ist, und führt dann einen byteweisen Vergleich der ersten len(prefix) Bytes durch. Dieser Mini-Algorithmus läuft in O(len(prefix)) und entspricht im Wesentlichen einem memcmp. Trotz des byte-orientierten Vergleichs ist die Funktion UTF-8-sicher: weil gültiges UTF-8 selbstsynchronisierend ist und der Präfix als komplette Byte-Sequenz vorliegt, kann ein Treffer niemals mitten in einer Mehr-Byte-Rune enden. HasPrefix ist damit das Werkzeug der Wahl, wenn man wissen will, ob ein Pfad mit /api/, ein Befehl mit git oder eine URL mit https:// startet — schneller, klarer und sicherer als ein Regex.

Die Signatur ist denkbar schlank: zwei String-Argumente, ein bool zurück. Es gibt keine optionalen Parameter, kein Fehler-Return, keine Encoding-Optionen — das macht den Aufruf vorhersagbar und gut inline-fähig durch den Compiler.

Go signature.go
func HasPrefix(s, prefix string) bool

Reihenfolge merken: erst der zu prüfende String, dann der gesuchte Präfix. Diese Konvention zieht sich konsequent durch das strings-Paket und vermeidet das ständige „in welcher Reihenfolge nochmal?" anderer Sprachen.

Ein häufiger Stolperstein in Testsuites: HasPrefix(s, "") liefert immer true, egal wie s aussieht — selbst der Leerstring startet mit dem Leerstring. Das ist mathematisch korrekt (jeder String hat den Leerstring als triviales Präfix), kann aber überraschen, wenn ein Filter versehentlich mit leerem Pattern aufgerufen wird und plötzlich alles durchlässt.

Go empty_prefix.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.HasPrefix("hello", ""))   // true
	fmt.Println(strings.HasPrefix("", ""))        // true
	fmt.Println(strings.HasPrefix("/api/v1", "")) // true
}
Output
true
true
true

In Konfigurationsschichten lohnt sich daher ein expliziter if prefix == "" { return false }-Guard, wenn ein leeres Filter-Pattern semantisch „kein Match" bedeuten soll. Die Stdlib folgt hier der reinen Mengenlehre, die Anwendung folgt oft anderer Intuition.

HasPrefix ist das Prädikat, TrimPrefix der Schnitt und CutPrefix (ab Go 1.20) die Kombination aus beidem. Wer den Präfix nicht nur erkennen, sondern auch entfernen will, sollte direkt zu CutPrefix greifen — das spart einen redundanten Vergleich, weil die Funktion intern ohnehin schon weiß, ob sie geschnitten hat.

FunktionRückgabeWann nutzen?
HasPrefix(s, p)boolReine Prüfung, kein Schnitt
TrimPrefix(s, p)string (ggf. unverändert)Schnitt, ohne wissen zu müssen ob er griff
CutPrefix(s, p)(rest string, ok bool)Schnitt und Erfolgs-Flag in einem Aufruf

Das gängige Antipattern if strings.HasPrefix(s, p) { s = strings.TrimPrefix(s, p) } lässt sich mit CutPrefix zu rest, ok := strings.CutPrefix(s, p) zusammenziehen — ein Funktionsaufruf statt zwei, gleicher Effekt.

HasPrefix ist intern eine Längen-Prüfung gefolgt von einem Slice-Vergleich s[:len(prefix)] == prefix. Der Go-Compiler bzw. die Runtime nutzen dafür auf den meisten Plattformen eine SIMD- oder memcmp-ähnliche Routine, die Bytes in Blöcken vergleicht statt einzeln. Für typische Präfixe von 5 bis 50 Bytes liegt der Aufruf im sub-Nanosekunden- bis niedrigen Nanosekunden-Bereich.

Im Vergleich zu einem regexp.MustCompile("^prefix").MatchString(s) ist HasPrefix typischerweise zwei bis drei Größenordnungen schneller — ein Regex muss kompiliert, optimiert und durch eine NFA/DFA-Engine geschickt werden, während HasPrefix direkt Bytes vergleicht. In Hot-Loops (Request-Routing, Log-Parsing) ist das ein spürbarer Unterschied.

Eine eingebaute „starts with one of"-Variante gibt es nicht — man iteriert manuell über einen Slice. Bei wenigen Präfixen ist diese Schleife völlig ausreichend; bei Hunderten von Patterns wäre eine Trie-Datenstruktur die richtige Wahl, aber für typische Anwendungsfälle reicht der lineare Scan.

Go multi_prefix.go
package main

import (
	"fmt"
	"strings"
)

func hasAnyPrefix(s string, prefixes []string) bool {
	for _, p := range prefixes {
		if strings.HasPrefix(s, p) {
			return true
		}
	}
	return false
}

func main() {
	schemes := []string{"http://", "https://", "ftp://", "file://"}
	fmt.Println(hasAnyPrefix("https://mibeon.de", schemes)) // true
	fmt.Println(hasAnyPrefix("mailto:me@x.de", schemes))    // false
}
Output
true
false

Eine kleine Optimierung: Präfixe nach erwarteter Trefferhäufigkeit sortieren, damit der häufigste Fall früh greift. In den meisten Anwendungen ist die Liste klein genug, dass dieser Mikro-Trick irrelevant bleibt, aber bei Logfile-Filtern mit Millionen Zeilen lohnt sich der Blick.

HasPrefix vergleicht strikt byteweise — "Hello" startet nicht mit "hello". Für case-insensitive Präfix-Matches gibt es keine eingebaute Variante; das Idiom kombiniert eine Längen-Prüfung mit strings.EqualFold, das Unicode-korrekte Case-Folding durchführt (inklusive deutscher Umlaute, türkischem dotless-i und griechischem Sigma).

Go case_insensitive.go
package main

import (
	"fmt"
	"strings"
)

func hasPrefixFold(s, prefix string) bool {
	if len(s) < len(prefix) {
		return false
	}
	return strings.EqualFold(s[:len(prefix)], prefix)
}

func main() {
	fmt.Println(hasPrefixFold("HELLO World", "hello")) // true
	fmt.Println(hasPrefixFold("Straße 12", "STRASSE")) // false (1-zu-1-Folding, ß ≠ ss)
	fmt.Println(hasPrefixFold("hi", "hello"))          // false
}
Output
true
false
false

Der Längen-Check vorab ist Pflicht, sonst panikt das Slicing s[:len(prefix)] mit Out-of-Bounds. EqualFold arbeitet strikt 1-zu-1 — ß und ss zählen NICHT als gleich, dafür wäre eine vollständige Normalisierungs-Pipeline nötig.

In handgeschriebenen Routern ohne Framework ist HasPrefix die natürliche Wahl, um eingehende Request-Pfade auf Subtrees zu verteilen. Statt eines Regex-Routings reicht oft eine Kette von Präfix-Checks, die in der Reihenfolge ihrer Spezifität geprüft werden — längere Präfixe vor kürzeren.

Go router.go
package main

import (
	"fmt"
	"strings"
)

func route(path string) string {
	switch {
	case strings.HasPrefix(path, "/api/v2/"):
		return "API v2 Handler"
	case strings.HasPrefix(path, "/api/v1/"):
		return "API v1 Handler (deprecated)"
	case strings.HasPrefix(path, "/static/"):
		return "Static File Handler"
	case strings.HasPrefix(path, "/admin/"):
		return "Admin Panel"
	default:
		return "Default Handler"
	}
}

func main() {
	fmt.Println(route("/api/v2/users"))
	fmt.Println(route("/api/v1/legacy"))
	fmt.Println(route("/static/logo.png"))
	fmt.Println(route("/blog/2026"))
}
Output
API v2 Handler
API v1 Handler (deprecated)
Static File Handler
Default Handler

Wichtig ist der abschließende Slash im Präfix — "/api/v1" würde sonst auch /api/v10/... matchen und zu subtilen Routing-Bugs führen. Bei produktiven Routern lohnt sich trotzdem irgendwann der Umstieg auf http.ServeMux (ab Go 1.22 mit Pattern-Support) oder ein Framework, aber für kleine Tools ist die switch-Variante glasklar und schnell.

Viele Binärformate beginnen mit einer charakteristischen Byte-Sequenz — der „Magic Number". PNG startet mit \x89PNG\r\n\x1a\n, PDF mit %PDF-, ZIP mit PK\x03\x04. Da Go-Strings beliebige Bytes enthalten dürfen, lässt sich HasPrefix direkt auf das eingelesene File-Header anwenden, um den Typ ohne Dateiendungs-Raterei zu bestimmen.

Go magic_bytes.go
package main

import (
	"fmt"
	"strings"
)

func detectType(header string) string {
	switch {
	case strings.HasPrefix(header, "\x89PNG\r\n\x1a\n"):
		return "PNG"
	case strings.HasPrefix(header, "%PDF-"):
		return "PDF"
	case strings.HasPrefix(header, "PK\x03\x04"):
		return "ZIP (oder docx/xlsx/jar)"
	case strings.HasPrefix(header, "\xff\xd8\xff"):
		return "JPEG"
	case strings.HasPrefix(header, "GIF87a"), strings.HasPrefix(header, "GIF89a"):
		return "GIF"
	default:
		return "unbekannt"
	}
}

func main() {
	fmt.Println(detectType("\x89PNG\r\n\x1a\n....rest"))
	fmt.Println(detectType("%PDF-1.7\n%%EOF"))
	fmt.Println(detectType("PK\x03\x04..."))
	fmt.Println(detectType("Hello World"))
}
Output
PNG
PDF
ZIP (oder docx/xlsx/jar)
unbekannt

In echten Anwendungen liest man typischerweise die ersten 512 Bytes mit io.ReadFull und übergibt sie als String oder []bytenet/http.DetectContentType macht intern fast genau das, nur mit erweiterter Heuristik. Für eigene Formate (z. B. interne Snapshot-Dateien mit Custom-Magic) ist HasPrefix das Mittel der Wahl, weil es schneller und expliziter als ein Regex-basiertes Sniffing ist.

O(len(prefix))-Vergleich

Die Laufzeit hängt nur am prefix — interner Slice-Vergleich, byteweise, mit memcmp-Charakteristik.

Leeres Präfix ist immer true

HasPrefix(s, "") liefert für jedes s true — mathematisch korrekt, in Filtern oft Stolperfalle.

Strikt case-sensitiv

"Hello" startet nicht mit "hello" — für case-insensitive Match ergänzt EqualFold mit Längen-Check.

Zum Entfernen TrimPrefix nutzen

Wer den Präfix abschneiden will, ruft TrimPrefix direkt auf — der eingebaute Längen-Check ist quasi gratis.

CutPrefix kombiniert Check und Trim

Ab Go 1.20 liefert CutPrefix (rest, ok) in einem Schritt und ersetzt das HasPrefix-plus-TrimPrefix-Duo.

Mehrere Präfixe per Loop

Keine eingebaute „starts with any of"-Variante — Slice durchiterieren und beim ersten Treffer abbrechen.

Threadsafe by design

Reiner Lesevorgang ohne State — beliebig viele Goroutines können dieselben Strings parallel prüfen.

Byte-orientiert, aber UTF-8-sicher

Vergleich läuft byteweise, dank selbstsynchronisierendem UTF-8 endet ein Match nie in einer halben Rune.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

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

Zur Übersicht