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