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.
An Stellen, die sehr häufig durchlaufen werden (etwa in einer engen Schleife oder einem oft aufgerufenen Handler), 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 an häufig durchlaufenen Stellen 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