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.

Go signatur.go
func IndexByte(s string, c byte) int

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

Go byte_treffer.go
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
}
Output
0
1
0
-1

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

Go falle_multibyte.go
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
}
Output
-1
5
12

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

FunktionSucht nachUTF-8-awareGeschwindigkeitEinsatz
IndexByte(s, c)Einzelnes ByteNeinSIMD, sehr hochASCII-Trenner
IndexRune(s, r)Einzelner CodepointJaHochUnicode-Zeichen
Index(s, sub)Substring beliebig langJa (Bytes)MittelMehrzeichen-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).

Go header_split.go
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)
}
Output
ok=true key="Content-Type" value="text/html; charset=utf-8"
zweiter ok: false

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

Go zeilen_iter.go
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)
    })
}
Output
[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

/ Weiter

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

Zur Übersicht