strings.IndexByte sucht das erste Vorkommen eines einzelnen Bytes in einem String und gibt dessen Offset zurück — bei Nichttreffer -1. Es ist die schnellste Suchfunktion im strings-Paket, weil die Go-Runtime für amd64 (AVX2) und arm64 (NEON) handgeschriebene SIMD-Assembler-Implementierungen mitliefert, die 16 bis 64 Bytes parallel mit dem gesuchten Wert vergleichen. Der Klassiker für ASCII-Trenner wie /, :, , oder \n in Protokollparsern, Pfadlogik und URL-Routing.
Die Funktion akzeptiert den Heuhaufen s und das zu suchende Byte c und liefert den Byte-Index. Wichtig: Der Index zählt Bytes, nicht Runen — bei ASCII-Eingaben identisch, bei UTF-8 mit Multi-Byte-Zeichen ein subtiler Unterschied.
func IndexByte(s string, c byte) intDer Rückgabewert ist die Position des ersten Treffers in Bytes ab 0. Findet sich c nicht in s, liefert die Funktion -1. Es gibt keine Variante mit Startoffset; wer ab Position n weitersuchen möchte, sliced den String per s[n:] und addiert n aufs Ergebnis.
IndexByte arbeitet auf der rohen Byte-Ebene und kennt UTF-8 nicht. Multi-Byte-Codepoints wie ä (0xC3 0xA4) werden in ihre Einzelbytes zerlegt — ein Aufruf IndexByte("ä", 0xC3) trifft tatsächlich bei Offset 0, obwohl 0xC3 für sich genommen kein gültiges Zeichen ist. Diese Eigenschaft ist gewollt: Sie macht die Funktion blitzschnell, verschiebt aber die Verantwortung für UTF-8-Semantik auf den Aufrufer.
package main
import (
"fmt"
"strings"
)
func main() {
s := "ä" // 0xC3 0xA4 in UTF-8
// IndexByte trifft das erste Byte der Sequenz
fmt.Println(strings.IndexByte(s, 0xC3)) // 0
fmt.Println(strings.IndexByte(s, 0xA4)) // 1
// IndexRune sucht den Codepoint U+00E4 als logisches Zeichen
fmt.Println(strings.IndexRune(s, 'ä')) // 0
// Nichttreffer
fmt.Println(strings.IndexByte(s, '?')) // -1
}0
1
0
-1Beide Funktionen liefern hier 0, aber aus völlig unterschiedlichen Gründen: IndexByte sieht das erste Byte 0xC3 der UTF-8-Sequenz, IndexRune interpretiert die Sequenz als Codepoint U+00E4. Bei reinem ASCII spielt der Unterschied keine Rolle — und genau dort ist IndexByte zu Hause.
Die Standardbibliothek liefert für die wichtigsten Architekturen handgeschriebene Assembler-Implementierungen. Auf amd64 nutzt der Hot-Path AVX2-Register (PCMPEQB/VPCMPEQB, 32 Bytes parallel), auf arm64 NEON-Vektor-Instruktionen mit 16 Bytes pro Vergleich. In Microbenchmarks erreicht der Lookup auf modernen Server-CPUs Durchsätze von 5 bis 20 GB/s pro Core — eine Größenordnung schneller als eine handgeschriebene Schleife in Go.
Das hat eine angenehme Konsequenz: Selbst auf mehreren Megabyte großen Strings ist IndexByte praktisch frei. Wer ein Logfile mit '\n' zerschneidet oder einen Header-Block per ':' zerlegt, muss sich um die Kosten der Suche keine Gedanken machen. Der Flaschenhals liegt dann meist im I/O oder in der Folgeverarbeitung.
IndexByte ist die richtige Wahl, wenn drei Bedingungen erfüllt sind: Erstens, das gesuchte Byte ist ein ASCII-Wert kleiner 128. Zweitens, der Eingabe-String ist gültiges UTF-8 (oder reines ASCII). Drittens, es wird genau ein Byte gesucht — kein Substring, kein Codepoint mit Multi-Byte-Repräsentation.
Unter diesen Voraussetzungen ist das Ergebnis identisch mit dem von IndexRune und um eine Größenordnung schneller. Das deckt den überwiegenden Teil realer Anwendungsfälle ab: Pfad-Splitter (/), Header-Trenner (:), CSV-Spalten (,), Zeilenenden (\n), Query-String-Separatoren (&, =).
Sobald das gesuchte „Zeichen" als Codepoint ≥ 128 gemeint ist, wird IndexByte zur Falle. Ein deutsches ö ist in UTF-8 zwei Bytes (0xC3 0xB6), beide einzeln als Byte gesucht treffen sie irgendwo mitten in fremden Sequenzen. Der einzig richtige Weg ist hier IndexRune, das die UTF-8-Dekodierung übernimmt.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Straße in München"
// FALSCH: 'ß' ist kein einzelnes Byte — Truncation rune→byte greift still.
var b byte = 'ß' // -> 0xDF, das zweite Byte von 'ß' (0xC3 0x9F)
fmt.Println(strings.IndexByte(s, b)) // -1, weil 0xDF allein nicht vorkommt
// RICHTIG: IndexRune kennt UTF-8
fmt.Println(strings.IndexRune(s, 'ß')) // 5
fmt.Println(strings.IndexRune(s, 'ü')) // 12
}-1
5
12Die stille Truncation von rune zu byte ist tückisch — der Compiler warnt nicht, das Ergebnis ist einfach falsch. Faustregel: Sobald ein Nicht-ASCII-Zeichen ins Spiel kommt, lieber IndexRune nehmen und den Performance-Unterschied in Kauf nehmen.
Die drei Funktionen unterscheiden sich in Granularität und Geschwindigkeit. Eine kompakte Gegenüberstellung hilft bei der Auswahl im Alltag:
| Funktion | Sucht nach | UTF-8-aware | Geschwindigkeit | Einsatz |
|---|---|---|---|---|
IndexByte(s, c) | Einzelnes Byte | Nein | SIMD, sehr hoch | ASCII-Trenner |
IndexRune(s, r) | Einzelner Codepoint | Ja | Hoch | Unicode-Zeichen |
Index(s, sub) | Substring beliebig lang | Ja (Bytes) | Mittel | Mehrzeichen-Muster, Tokens |
Bei rein lateinischem ASCII-Suchwert und ASCII-Eingabe sind alle drei korrekt — dann gewinnt IndexByte. Sobald das Muster länger als ein Byte ist, ist Index Pflicht; sobald es ein Nicht-ASCII-Codepoint ist, IndexRune.
Ein klassisches Muster: eine Zeile der Form "Content-Type: text/html" in Schlüssel und Wert zerlegen. Der Doppelpunkt ist garantiert ASCII, also ist IndexByte das richtige Werkzeug. Die Variante ist deutlich schneller und allokationsfrei im Vergleich zu strings.SplitN(line, ":", 2).
package main
import (
"fmt"
"strings"
)
func splitHeader(line string) (key, value string, ok bool) {
i := strings.IndexByte(line, ':')
if i < 0 {
return "", "", false
}
key = line[:i]
// Wert: alles nach ':' trimmen
value = strings.TrimSpace(line[i+1:])
return key, value, true
}
func main() {
k, v, ok := splitHeader("Content-Type: text/html; charset=utf-8")
fmt.Printf("ok=%v key=%q value=%q\n", ok, k, v)
_, _, ok = splitHeader("kein doppelpunkt hier")
fmt.Println("zweiter ok:", ok)
}ok=true key="Content-Type" value="text/html; charset=utf-8"
zweiter ok: falseStatt zwei Slices wie SplitN zu allokieren, liefert IndexByte nur einen int, mit dem das ursprüngliche String-Header über zwei Slicing-Operationen geteilt wird. Slicing erzeugt in Go kein neues Backing-Array — beide Teilstrings teilen sich den Speicher der Originaleingabe. Bei millionenfach geparsten HTTP-Headern macht das messbar Druck vom Garbage Collector.
Für produktiven Code, der Zeilen aus einem io.Reader ziehen will, ist bufio.Scanner die richtige Adresse — er kümmert sich um Buffergrößen, CRLF und Token-Limits. Wenn aber ein kompletter Block bereits im Speicher liegt (etwa das Ergebnis eines os.ReadFile), ist eine handgerollte Iteration mit IndexByte schneller und allokationsfrei.
package main
import (
"fmt"
"strings"
)
func eachLine(s string, fn func(line string)) {
for len(s) > 0 {
i := strings.IndexByte(s, '\n')
if i < 0 {
fn(s)
return
}
// CRLF-tolerant: \r vor \n abschneiden
line := s[:i]
if len(line) > 0 && line[len(line)-1] == '\r' {
line = line[:len(line)-1]
}
fn(line)
s = s[i+1:]
}
}
func main() {
payload := "ERROR: disk full\r\nWARN: retrying\nINFO: done\n"
eachLine(payload, func(line string) {
fmt.Printf("[%s]\n", line)
})
}[ERROR: disk full]
[WARN: retrying]
[INFO: done]Jede Iteration besteht aus genau einem SIMD-Lookup und zwei String-Slicings — keine Allokation, kein Heap, kein GC-Druck. Für Stream-Eingaben aus Dateien oder Netzwerken bleibt bufio.Scanner trotzdem die erste Wahl, weil er die Buffergrenzen sauber verwaltet; das Snippet hier zielt auf den Fall, dass die Daten bereits vollständig im Speicher sind und maximaler Durchsatz zählt.
Schnellster Suchpfad im Paket
IndexByte nutzt handgeschriebene SIMD-Assembler (AVX2 auf amd64, NEON auf arm64) und vergleicht 16-64 Bytes parallel — typisch 5-20 GB/s pro Core.
Sucht RAW Byte, keinen Codepoint
Multi-Byte UTF-8-Sequenzen werden in ihre Einzelbytes zerlegt; IndexByte("ä", 0xC3) trifft tatsächlich bei Offset 0.
Sicher nur für ASCII-Bytes (< 128)
Bei Bytes unter 128 ist das Resultat identisch mit IndexRune und semantisch korrekt — darüber wird es trickreich.
Für Bytes >= 128 lieber IndexRune
Sobald das gesuchte „Zeichen" als Codepoint gemeint ist und Multi-Byte sein kann, übernimmt IndexRune die UTF-8-Dekodierung.
-1 als Nicht-Treffer-Signal
Das Sentinel -1 muss immer abgefragt werden, bevor das Ergebnis als Slice-Index verwendet wird — sonst Out-of-Bounds-Panic.
Ideal für ASCII-Trenner
Klassische Einsatzfelder: / für Pfade, : für Header, , für CSV, \n für Zeilen, &/= für Query-Strings.
Threadsafe ohne weitere Maßnahmen
Strings sind in Go unveränderlich; IndexByte liest nur und ist damit ohne Mutex aus mehreren Goroutinen aufrufbar.
bufio.Scanner für Stream-APIs
Bei Stream-Eingaben aus io.Reader ist bufio.Scanner die robustere Wahl; IndexByte-Iteration zielt auf bereits geladene Buffer.
Weiterführende Ressourcen
Externe Quellen
strings.IndexBytebytes.IndexByte— gleiche Idee für[]bytebufio.Scanner