byte ist in Go kein eigener Typ, sondern ein Alias für uint8 — also ein vorzeichenloser 8-Bit-Wert von 0 bis 255. Trotzdem ist byte allgegenwärtig: []byte ist der Standard-Container für Binärdaten, string-Bytes, Netzwerk-Payloads und alles, was über io.Reader und io.Writer fließt. Dieser Artikel zeigt, was byte wirklich ist, wie es sich von rune unterscheidet, wie Konvertierung mit string funktioniert, was das bytes-Paket leistet und wo die Performance-Stolpersteine liegen.
Was byte in Go bedeutet
In der Go-Sprachspezifikation steht es klar:
byteis an alias foruint8.
Das ist wörtlich gemeint. byte und uint8 sind identische Typen — kein eigener Typ, kein Wrapper, keine Definition. Der Compiler behandelt beide vollkommen austauschbar:
package main
import (
"fmt"
"reflect"
)
func main() {
var b byte = 65
var u uint8 = b // keine Konvertierung nötig
fmt.Println(b, u) // 65 65
fmt.Println(reflect.TypeOf(b)) // uint8
}65 65
uint8reflect.TypeOf(b) liefert uint8, nicht byte — der Alias verschwindet beim Type-Check. Warum trotzdem den Namen byte? Weil er Absicht kommuniziert: Wer byte schreibt, denkt an Binärdaten oder Text-Bytes; wer uint8 schreibt, denkt an einen kleinen vorzeichenlosen Integer für Arithmetik.
byte vs. rune — wann was nutzen
Go hat zwei Aliase für „ein Zeichen”: byte (= uint8, 8 Bit) und rune (= int32, 32 Bit). Sie lösen unterschiedliche Probleme:
| Typ | Alias für | Größe | Wertebereich | Zweck |
|---|---|---|---|---|
byte | uint8 | 8 Bit | 0 – 255 | Ein einzelnes Byte (Rohdaten, ASCII) |
rune | int32 | 32 Bit | −2.147.483.648 – 2.147.483.647 | Ein Unicode-Codepoint |
Der Unterschied wird sichtbar, sobald Multibyte-UTF-8 ins Spiel kommt:
s := "Café"
fmt.Println(len(s)) // 5 — 5 Bytes (é = 2 UTF-8-Bytes)
fmt.Println(len([]rune(s))) // 4 — 4 Codepoints
for i, b := range []byte(s) {
fmt.Printf("byte %d = 0x%X\n", i, b)
}
for i, r := range s {
fmt.Printf("rune at %d = %c (U+%04X)\n", i, r, r)
}5
4
byte 0 = 0x43
byte 1 = 0x61
byte 2 = 0x66
byte 3 = 0xC3
byte 4 = 0xA9
rune at 0 = C (U+0043)
rune at 1 = a (U+0061)
rune at 2 = f (U+0066)
rune at 3 = é (U+00E9)Faustregel: byte für rohe Daten und ASCII, rune sobald es um sichtbare Zeichen in beliebigen Sprachen geht.
[]byte als Standard-Container
Praktisch nirgendwo siehst du ein einzelnes byte — fast immer ist es ein []byte, also ein Slice. Das ist der Quasi-Standard für:
- Datei-Inhalte —
os.ReadFileliefert[]byte. - HTTP-Bodies —
io.ReadAll(r.Body)liefert[]byte. - JSON-Encoding —
json.Marshalliefert[]byte. - Crypto — Hashes, Signaturen, AES-Keys: alles
[]byte. - Netzwerk-Pakete — TCP/UDP-Payloads sind Byte-Slices.
- String-Bytes, wenn du sie verändern willst (
stringist immutable).
data, err := os.ReadFile("config.json")
if err != nil {
return err
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return err
}Anders als string ist []byte veränderbar: data[0] = 'X' funktioniert. Genau deshalb arbeiten Buffer und Streams mit Byte-Slices statt mit Strings.
Konvertierung zwischen string und []byte
Beide Richtungen sind Sprache-eingebaute Type-Conversions:
s := "Hallo"
b := []byte(s) // string -> []byte
b[0] = 'M'
fmt.Println(s) // Hallo (string ist immutable)
fmt.Println(string(b)) // MalloHallo
MalloWichtig: Beide Konvertierungen kopieren in der Regel die Bytes. Ein string ist immutable, ein []byte mutable — wenn beide denselben Speicher teilen würden, könnte man durch das Slice den String verändern. Das verbietet die Sprache.
In Heißpfaden lohnt es sich, die Anzahl dieser Conversions zu minimieren — bei einem Megabyte-String spürst du die Allokation. Seit Go 1.20 erkennt der Compiler einige Spezialfälle, in denen die Kopie wegfällt (z. B. bei string([]byte) direkt vor einem map-Lookup), aber als Regel gilt: jede Konvertierung kostet eine Allokation.
Byte-Literale
Ein einzelnes Byte schreibst du in Go auf vier Arten:
var (
dezimal byte = 65
hex byte = 0x41
oktal byte = 0o101
binaer byte = 0b01000001
ausChar byte = 'A' // Char-Literal als untyped constant -> uint8
)
fmt.Println(dezimal, hex, oktal, binaer, ausChar)
// 65 65 65 65 6565 65 65 65 65Achtung beim Char-Literal: 'A' ist eine untypisierte Konstante mit Default-Typ int32 (rune). In einem Kontext, der byte erwartet (z. B. eine byte-Variablen-Deklaration), wird sie zu byte umgedeutet. Aber c := 'A' ohne expliziten Typ ergibt eine Rune, nicht ein Byte:
c := 'A'
fmt.Printf("%T\n", c) // int32 — nicht uint8!int32Das bytes-Paket
Das bytes-Paket aus der Standardbibliothek ist das Pendant zu strings, nur für []byte. Es enthält zwei zentrale Typen und eine Reihe nützlicher Funktionen:
bytes.Buffer— ein wachsender Puffer, derio.Readerundio.Writerzugleich implementiert. Zero-Value ist sofort nutzbar.bytes.Reader— ein read-only-Reader über einem festen[]byte(implementiertio.Reader,io.Seeker,io.ReaderAt).
var buf bytes.Buffer
buf.WriteString("Hallo, ")
fmt.Fprintf(&buf, "%s!", "Welt")
fmt.Println(buf.String()) // Hallo, Welt!Hallo, Welt!Funktionen für direkte Byte-Slice-Operationen:
| Funktion | Zweck |
|---|---|
bytes.Equal | Zwei Slices auf Inhalts-Gleichheit prüfen |
bytes.Compare | Lexikografischer Vergleich (−1, 0, 1) |
bytes.Contains | Enthält das Slice ein Sub-Slice? |
bytes.Index | Position eines Sub-Slice (−1 wenn nicht da) |
bytes.Split | Slice anhand eines Trenners aufteilen |
bytes.Join | Slices mit Trenner zusammenfügen |
bytes.HasPrefix | Beginnt das Slice mit einem Präfix? |
bytes.TrimSpace | Whitespace am Anfang und Ende entfernen |
bytes.ReplaceAll | Alle Vorkommen ersetzen |
io.Reader und io.Writer
Die beiden wichtigsten Interfaces der gesamten Standardbibliothek arbeiten mit []byte:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}Der Caller stellt einen []byte-Puffer bereit, das Interface füllt ihn (Reader) oder konsumiert daraus (Writer). Damit ist alles, was lesen oder schreiben kann — Files, Sockets, HTTP-Bodies, Crypto-Streams, gzip-Decoder, bytes.Buffer, os.Stdout — über genau dieselbe API ansprechbar:
// File -> stdout (Streaming, ohne alles in den Speicher zu laden)
f, _ := os.Open("daten.bin")
defer f.Close()
io.Copy(os.Stdout, f)Warum []byte und nicht string? Weil string immutable ist — der Reader müsste bei jedem Aufruf einen neuen String allokieren. Mit []byte kann derselbe Puffer in einer Schleife wiederverwendet werden. Das ist das Fundament für streaming-fähige, allokationsarme I/O.
Performance-Aspekte
Wenn du viel an Text-Daten arbeitest, ist die Wahl zwischen string und []byte ein Performance-Faktor:
- Konkatenation in einer Schleife —
bytes.Bufferoderstrings.Buildersind drastisch schneller alss = s + ..., weil sie einen wachsenden Puffer wiederverwenden statt jedes Mal neu zu allokieren. - Vermeide unnötige Conversions — jedes
[]byte(s)und jedesstring(b)allokiert (mit ein paar Compiler-Ausnahmen). Wenn du eine API hast, diestringwill, und Daten als[]bytevorliegen, frage dich, ob die API auch ein[]byteannehmen kann. bytes.Equalstatt==— Slices sind in Go nicht vergleichbar (a == bist ein Compile-Fehler bei[]byte);bytes.Equalmacht das richtig und ist auf typische CPU-Pfade optimiert.- Pre-Sizing —
make([]byte, 0, expected)oderbuf.Grow(n)sparen Re-Allokationen, wenn die ungefähre Größe vorher bekannt ist. - Wiederverwendung — in Hot-Pfaden lohnt ein
sync.Poolmit Byte-Buffern, um GC-Druck zu reduzieren.
Interessantes
byte ist seit Go 1 ein Alias, kein eigener Typ.
reflect.TypeOf(byte(‘A’)) liefert uint8 — der Alias verschwindet zur Laufzeit. Du kannst byte und uint8 ohne Konvertierung mischen, weil die Spezifikation sie als identische Typen führt.
[]byte und string sind Cousins.
Beide repräsentieren eine Bytefolge. Der Unterschied: string ist immutable und vergleichbar (==), []byte ist mutable und nicht direkt vergleichbar. Wer Daten verändern will, nimmt das Slice; wer als Map-Key oder für sichere Übergabe-Semantik will, nimmt den String.
Konvertierung []byte und string ist optimiert, aber nicht gratis.
Seit Go 1.20+ erkennt der Compiler einige Spezialfälle (z. B. m[string(b)]-Lookups), in denen kein Kopieren nötig ist. In allgemeinen Fällen wird weiterhin kopiert — du solltest unnötige Conversions in Heißpfaden vermeiden.
bytes.Buffer ist die Empfehlung, wenn du am Ende []byte brauchst.
strings.Builder ist optimal, wenn das Ergebnis ein string ist. Soll am Ende ein []byte herauskommen — etwa für eine HTTP-Response oder einen Hash-Input — vermeidet bytes.Buffer die finale String-zu-Byte-Konvertierung.
Char-Literale sind Runes, nicht Bytes.
c := ‘A’ ergibt int32, nicht uint8. Nur in einem Kontext, der explizit byte erwartet (z. B. var b byte = ‘A’), wird die untypisierte Konstante zu Byte umgedeutet. Mit := bekommst du eine Rune.
io.Reader.Read([]byte) ist der Stdlib-Standard.
Das Interface nimmt ein veränderbares Slice, weil ein string als immutabler Typ pro Read neu allokiert werden müsste. Mit []byte kann derselbe Puffer in einer Schleife wiederverwendet werden — Grundlage allokationsarmer I/O.
bytes.Equal statt ==.
Slices sind in Go nicht comparable. a == b auf zwei []byte-Werten ist ein Compile-Fehler (außer der Vergleich mit nil). bytes.Equal(a, b) ist der korrekte Weg und auf typische CPU-Pfade optimiert.
Weiterführende Ressourcen
Externe Quellen
- Numeric types – Go Specification
- String types – Go Specification
- Paket bytes – pkg.go.dev
- Paket io – pkg.go.dev