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:

byte is an alias for uint8.

Das ist wörtlich gemeint. byte und uint8 sind identische Typen — kein eigener Typ, kein Wrapper, keine Definition. Der Compiler behandelt beide vollkommen austauschbar:

Go byte_alias.go
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
}
Output
65 65
uint8

reflect.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:

TypAlias fürGrößeWertebereichZweck
byteuint88 Bit0 – 255Ein einzelnes Byte (Rohdaten, ASCII)
runeint3232 Bit−2.147.483.648 – 2.147.483.647Ein Unicode-Codepoint

Der Unterschied wird sichtbar, sobald Multibyte-UTF-8 ins Spiel kommt:

Go byte_vs_rune.go
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)
}
Output
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-Inhalteos.ReadFile liefert []byte.
  • HTTP-Bodiesio.ReadAll(r.Body) liefert []byte.
  • JSON-Encodingjson.Marshal liefert []byte.
  • Crypto — Hashes, Signaturen, AES-Keys: alles []byte.
  • Netzwerk-Pakete — TCP/UDP-Payloads sind Byte-Slices.
  • String-Bytes, wenn du sie verändern willst (string ist immutable).
Go byte_slice.go
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:

Go string_bytes.go
s := "Hallo"
b := []byte(s) // string -> []byte
b[0] = 'M'

fmt.Println(s)         // Hallo (string ist immutable)
fmt.Println(string(b)) // Mallo
Output
Hallo
Mallo

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

Go byte_literals.go
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 65
Output
65 65 65 65 65

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

Go char_literal.go
c := 'A'
fmt.Printf("%T\n", c) // int32 — nicht uint8!
Output
int32

Das 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, der io.Reader und io.Writer zugleich implementiert. Zero-Value ist sofort nutzbar.
  • bytes.Reader — ein read-only-Reader über einem festen []byte (implementiert io.Reader, io.Seeker, io.ReaderAt).
Go bytes_buffer.go
var buf bytes.Buffer
buf.WriteString("Hallo, ")
fmt.Fprintf(&buf, "%s!", "Welt")

fmt.Println(buf.String()) // Hallo, Welt!
Output
Hallo, Welt!

Funktionen für direkte Byte-Slice-Operationen:

FunktionZweck
bytes.EqualZwei Slices auf Inhalts-Gleichheit prüfen
bytes.CompareLexikografischer Vergleich (−1, 0, 1)
bytes.ContainsEnthält das Slice ein Sub-Slice?
bytes.IndexPosition eines Sub-Slice (−1 wenn nicht da)
bytes.SplitSlice anhand eines Trenners aufteilen
bytes.JoinSlices mit Trenner zusammenfügen
bytes.HasPrefixBeginnt das Slice mit einem Präfix?
bytes.TrimSpaceWhitespace am Anfang und Ende entfernen
bytes.ReplaceAllAlle Vorkommen ersetzen

io.Reader und io.Writer

Die beiden wichtigsten Interfaces der gesamten Standardbibliothek arbeiten mit []byte:

Go io_interfaces.go
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:

Go copy_example.go
// 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 Schleifebytes.Buffer oder strings.Builder sind drastisch schneller als s = s + ..., weil sie einen wachsenden Puffer wiederverwenden statt jedes Mal neu zu allokieren.
  • Vermeide unnötige Conversions — jedes []byte(s) und jedes string(b) allokiert (mit ein paar Compiler-Ausnahmen). Wenn du eine API hast, die string will, und Daten als []byte vorliegen, frage dich, ob die API auch ein []byte annehmen kann.
  • bytes.Equal statt == — Slices sind in Go nicht vergleichbar (a == b ist ein Compile-Fehler bei []byte); bytes.Equal macht das richtig und ist auf typische CPU-Pfade optimiert.
  • Pre-Sizingmake([]byte, 0, expected) oder buf.Grow(n) sparen Re-Allokationen, wenn die ungefähre Größe vorher bekannt ist.
  • Wiederverwendung — in Hot-Pfaden lohnt ein sync.Pool mit 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

/ Weiter

Zurück zu Datentypen

Zur Übersicht