Base64-Dekodierung in Go: Ein vollständiger Leitfaden
In einer API-Antwort versteckt sich ein langer String, der so tut, als wäre er ein Wert, obwohl er in Wirklichkeit eine Datei, ein Token, ein Bild oder eine Nachricht von einem System ist, das drei Jahre älter ist als Ihres. Irgendwo in Ihren Log-Zeilen, Datenbankzeilen und JSON-Payloads tauchen Base64-Strings ständig auf: das Standardalphabet mit seinen 64 Zeichen, mal mit Plus und Slash, mal mit Bindestrich und Unterstrich, und gelegentlich zwei Gleichheitszeichen, die am Ende wie eine Signatur parken.
Die Startseite dieser Site erklärt das Format selbst bereits: 64 druckbare Zeichen, die jeweils 6 Bits tragen, vier Zeichen pro drei Eingabe-Bytes und Padding, um den Job zu vollenden. Also geht dieser Artikel direkt zu der Hälfte der Arbeit, in der die interessanten Entscheidungen stecken: dem Öffnen dieser Strings in Go. Die gute Nachricht ist, dass Go ein wunderbarer Ort dafür ist. Ein Paket der Standardbibliothek, null Abhängigkeiten, ein Decoder, der standardmäßig streng ist, aber Zeilenumbrüche verzeiht, und Fehlermeldungen, die auf das exakte Byte zeigen, bei dem etwas schiefging.
Was Go mitbringt
Alles, was Sie brauchen, steckt bereits in der Standardbibliothek. Das Paket heißt encoding/base64, seine Quelldatei trägt immer noch einen Copyright-Header von 2009, dem Geburtsjahr der Sprache, und es gibt keine Erweiterung, die man aktivieren müsste, kein Modul, das man laden müsste, und keine Einstellung, die man umlegen müsste. Wenn go version auf Ihrem Rechner irgendetwas ausgibt, besitzen Sie bereits das komplette Werkzeug.
Zum Zeitpunkt des Schreibens ist die neueste Version Go 1.27.1, erschienen am 1. September 2026, und die Go-1.26-Linie (derzeit 1.26.8) ist die andere unterstützte Spur. Die Base64-API ist auf beiden identisch, und dank der Go-1-Kompatibilitätszusage wird ein Programm, das heute Base64 dekodiert, in jeder künftigen Version exakt dasselbe tun. Go selbst holen Sie sich von den offiziellen Tarballs auf go.dev/dl (etwa go1.27.1.linux-amd64.tar.gz, entpackt nach /usr/local), aus dem Paketmanager Ihrer Distribution (sudo apt install golang-go auf Ubuntu-basierten Systemen) oder über die golang.org/dl-Wrapper, wenn Sie mehrere Go-Versionen nebeneinander mögen.
Sobald Go installiert ist, druckt go doc encoding/base64 die gesamte API in einer lesbaren Spalte, was der schnellste Weg ist, sich wieder alles ins Gedächtnis zu rufen. Das einzige Add-on, das dieser Artikel irgendwo verwendet, ist golang.org/x/text für Legacy-Zeichensätze, installiert mit go get golang.org/x/text. Es taucht einmal auf, in seiner eigenen Sektion, und der Rest ist reine Standardbibliothek.
Ihr erstes Dekodieren
Neunzig Prozent des Dekodieralltags in Go sind eine Methode auf dem Encoding-Typ:
func (enc *Encoding) DecodeString(s string) ([]byte, error)
Geben Sie ihm einen Base64-String, und er gibt die Bytes zurück, die er darstellt, plus einen Fehler, wenn sich die Eingabe danebenbenimmt:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
decoded, err := base64.StdEncoding.DecodeString("TWFu")
if err != nil {
fmt.Println("decode failed:", err)
return
}
fmt.Println(string(decoded)) // Man
}
Zwei Dinge an dieser Signatur lohnen sich zu merken. Erstens ist das Ergebnis ein []byte, kein String, denn die Bytes, die Sie auspacken, können völlig gültiges Base64 und völlig furchtbarer Text sein: ein PNG-Header, ein komprimiertes Archiv, ein binäres Protokoll. Wickeln Sie es erst in string(...) ein, wenn Sie wissen, dass der Payload Text ist. Zweitens gibt die Methode immer zwei Werte zurück. Ein nil-Fehler bedeutet, der String war sauberes Base64; ein Nicht-nil-Fehler bedeutet, die Eingabe war irgendwo kaputt, und der Byte-Slice, den Sie erhalten haben, ist vielleicht ein Teilresultat und nicht leer. Beide Seiten dieses Verhaltens sehen Sie im Fehler-Abschnitt unten.
Vier Decoder, eine Frage: Welches Alphabet?
Go liefert vier fertige Encoding-Werte mit, und die Wahl des richtigen ist die erste echte Entscheidung bei jedem Decode. Die Tabelle unten ist die Sitzordnung:
| Variable | Alphabet | Padding | Wo Sie es treffen werden |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
MIME-E-Mail, Data-URLs, HTTP-Basic-Auth, PEM-Dateien, allgemeines JSON |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
URL-Pfade und -Queries, Dateinamen |
RawStdEncoding |
A-Z a-z 0-9 + / |
kein | Ungepaddetes Standard-Base64 von kompakten Produzenten |
RawURLEncoding |
A-Z a-z 0-9 - _ |
kein | JWT-Segmente, kompakte API-Identifikatoren |
Der schnellste Weg, auszuwählen, ist, auf die Daten selbst zu schauen. Ein String, der + oder / enthält, kann nur ein String aus dem Standardalphabet sein und braucht deshalb einen der beiden Std-Decoder. Ein String mit - oder _ ist die URL-sichere Variante aus RFC 4648 und braucht einen der beiden URL-Decoder. Dann das Ende prüfen: schließende =-Zeichen bedeuten die gepaddete Variante, ihr Fehlen die Raw-Variante. So fühlt sich die falsche Wahl an:
decoded, err := base64.StdEncoding.DecodeString("P29_")
// err: illegal base64 data at input byte 3
// der Unterstrich ist nicht im Standardalphabet,
// deshalb stoppt der Decoder beim letzten Zeichen, das er nicht erkennt
decoded, err = base64.URLEncoding.DecodeString("P29_")
// decoded sind die drei Bytes 0x3f 0x6f 0x7f, err ist nil
Wenn Sie Daten von einem Produzenten dekodieren, der sein eigenes 64-Zeichen-Alphabet definiert hat, baut Ihnen base64.NewEncoding("...64 chars...") einen Decoder dafür. Das Alphabet muss aus genau 64 eindeutigen Byte-Werten bestehen und darf keinen Zeilenumbruch enthalten - andernfalls panickt die Funktion - und obwohl die Dokumentation verlangt, dass das Alphabet das Padding-Zeichen ausschließt, erzwingt die Funktion das nicht - ein Alphabet mit '=' wird ohne Panic akzeptiert. Im Alltag brauchen Sie es selten, aber es ist da, und es ist der einzige Weg, ein privates Schema zu dekodieren.
Das Toleranz-Problem: Welche Eingabe akzeptiert Go?
Jeder Base64-Decoder muss eine unangenehme Entscheidung treffen: Wie viel Müll ist er bereit, zu verschlucken? Die Antwort von Go ist eine sorgfältig gezogene Linie. Auf der nachsichtigen Seite überspringt der Decoder Wagenrücksetzer und Zeilenumbrüche überall in der Eingabe, sodass ein String, der von einem E-Mail-Client oder einem PEM-Tool über viele Zeilen umgebrochen wurde, ohne jegliche Vorverarbeitung dekodiert wird:
decoded, err := base64.StdEncoding.DecodeString("T\nW\nF\r\nu")
// decoded ist "Man", err ist nil
// jedes \r und \n in dem String wurde schlicht ignoriert
Auf der strengen Seite ist alles andere tabu. Ein Leerzeichen, ein Tab, ein Nullbreitenzeichen, das aus einer PDF kopiert wurde, ein verirrtes Kolon aus einem Header: In dem Moment, in dem der Decoder auf ein Zeichen trifft, das nicht im Alphabet steht und kein Zeilenumbruch ist, stoppt er und meldet den Offset. Und er behält alles, was er bereits dekodiert hat:
decoded, err := base64.StdEncoding.DecodeString("TWFu junk")
// decoded ist "Man" (der Teil vor dem Leerzeichen),
// err ist: illegal base64 data at input byte 4
Diese Kombination überrascht: Ein fehlgeschlagenes Dekodieren kann trotzdem ein brauchbares Halbresultat aushändigen. Ob das eine Eigenschaft oder ein Risiko ist, hängt von Ihnen ab; der Punkt ist, dass err == nil die einzige Bedingung ist, unter der die Daten vollständig sind.
Padding hat eigene Regeln, und sie unterscheiden sich zwischen den gepaddeten und den Raw-Varianten. Die gepaddeten Decoder arbeiten in Gruppen: Eine Gruppe besteht entweder aus vier echten Zeichen oder aus zwei echten Zeichen, gefolgt von ==. Ein einzelnes Zeichen ist niemals eine komplette Gruppe, daher scheitert "T", und "TWF" scheitert ebenfalls, denn drei Zeichen brauchen ein Padding-Zeichen, das fehlt. Die Raw-Decoder streichen die Padding-Pflicht, aber sie können eine Länge, bei der einer Gruppe drei ihrer vier Zeichen fehlen würden, nicht akzeptieren, daher scheitert auch dort "T", während "TW" fröhlich zu einem einzelnen Byte dekodiert wird.
base64.StdEncoding.DecodeString("T") // Fehler bei Eingabe-Byte 0
base64.StdEncoding.DecodeString("TWF") // Fehler bei Eingabe-Byte 0
base64.RawStdEncoding.DecodeString("TW") // 1 Byte, kein Fehler
base64.StdEncoding.DecodeString("TWFu====") // "Man" plus Fehler bei Byte 4
Da gibt es noch einen weiteren Stimmungswechsel: Strict(), hinzugefügt in Go 1.8. Im strikten Modus erzwingt der Decoder die kanonische Form aus Abschnitt 3.5 von RFC 4648: Die ungenutzten Schlussbits der letzten Gruppe müssen null sein. Der normale Modus schert sich nicht darum, denn diese Bits werden schlicht nie verwendet, daher dekodiert "Qm==" ohne Murren zu dem Byte B. Der strikte Modus lehnt es dagegen ab:
decoded, err := base64.StdEncoding.DecodeString("Qm==")
// decoded ist "B", err ist nil (die Schlussbits wurden verworfen)
decoded, err = base64.StdEncoding.Strict().DecodeString("Qm==")
// err ist: illegal base64 data at input byte 2
Beachten Sie, dass auch im strikten Modus Zeilenumbrüche weiterhin übersprungen werden, wie die Dokumentation anmerkt. Verwenden Sie Strict(), wenn Sie ein Protokoll sprechen, dem die kanonische Kodierung wichtig ist, oder wenn Sie schlampige Produzenten ablehnen wollen, statt ihre Bits stillschweigend zu schlucken.
Fehler, die Ihnen sagen, wo
Jeder Fehler in diesem Paket kommt als konkreter, untersuchbarer Wert an. Wenn die Eingabe etwas enthält, das das Alphabet nicht kennt, oder das Padding falsch ist, gibt der Decoder eine base64.CorruptInputError zurück, und ihre Meldung enthält den Byte-Offset des Problems:
type CorruptInputError int64
func (e CorruptInputError) Error() string {
return "illegal base64 data at input byte " + strconv.FormatInt(int64(e), 10)
}
Dieser Offset ist der Unterschied zwischen "etwas ist fehlgeschlagen" und "das 4102. Zeichen dieses 900-Kilobyte-Strings ist ein Tab, das von der Zwischenablage hereingekrabbelt ist". Fangen Sie es mit dem üblichen Go-Idiom ab:
package main
import (
"encoding/base64"
"errors"
"fmt"
)
func main() {
_, err := base64.StdEncoding.DecodeString("TWF$")
var corrupt base64.CorruptInputError
if errors.As(err, &corrupt) {
fmt.Printf("bad byte at offset %d: %v\n", int(corrupt), err)
// bad byte at offset 3: illegal base64 data at input byte 3
return
}
fmt.Println("not a corrupt-input error:", err)
}
Hier ist die Symptomtabelle für die Eingaben, die Menschen am meisten verwirren:
| Eingabe (StdEncoding) | Ergebnis | Warum |
|---|---|---|
TWF$ |
Fehler bei Byte 3 | $ steht nicht im Alphabet |
T |
Fehler bei Byte 0 | ein einzelnes Zeichen ist nie eine komplette Gruppe |
TWF |
Fehler bei Byte 0 | drei Zeichen brauchen ein =, das fehlt |
TWFu junk |
Man plus Fehler bei Byte 4 |
ein Leerzeichen ist kein Zeilenumbruch, daher stoppt das Dekodieren dort |
TWFu\t |
Man plus Fehler bei Byte 4 |
Tabs werden nicht übersprungen, nur \r und \n |
T\nW\nF\nu |
Man, kein Fehler |
Zeilenumbrüche werden überall ignoriert |
==== |
Fehler bei Byte 0 | Padding am Anfang einer Gruppe ist ungültig |
(leerer String) |
leeres Ergebnis, kein Fehler | null Bytes Base64 dekodieren zu null Bytes |
Ein praktischer Tipp: Wenn ein Decode in der Produktion fehlschlägt, loggen Sie den Offset und ein kurzes Fenster drumherum. Neunzig Prozent der Zeit ist das "defekte" Byte ein Leerzeichen, das der Transport, die Zwischenablage oder ein PDF-Betrachter in den String geschmuggelt hat, und die Lösung ist ein Trimmen oder Streichen, kein Neuentwurf.
Dateien öffnen
Base64-Dateien sind einfach nur Textdateien, die Base64 enthalten, also gelten die üblichen Datei-Tools von Go. Für eine Datei, die bequem in den Speicher passt, lesen Sie sie vollständig ein und dekodieren Sie den String:
package main
import (
"encoding/base64"
"fmt"
"io"
"os"
)
func main() {
f, err := os.Open("payload.b64")
if err != nil {
fmt.Println("open failed:", err)
return
}
defer f.Close()
raw, err := io.ReadAll(f)
if err != nil {
fmt.Println("read failed:", err)
return
}
decoded, err := base64.StdEncoding.DecodeString(string(raw))
if err != nil {
fmt.Println("decode failed:", err)
return
}
fmt.Println("decoded", len(decoded), "bytes")
}
Bei großen Dateien ist das bessere Muster Streaming, und es verwendet die andere Hälfte der Paket-API: NewDecoder wickelt jeden io.Reader in einen base64-dekodierenden Reader, sodass Sie Datei zu Datei pipen können, ohne den gesamten Payload jemals im Speicher zu halten:
in, err := os.Open("payload.b64")
if err != nil {
panic(err)
}
defer in.Close()
dec := base64.NewDecoder(base64.StdEncoding, in)
out, err := os.Create("payload.bin")
if err != nil {
panic(err)
}
defer out.Close()
written, err := io.Copy(out, dec)
if err != nil {
panic(err)
}
fmt.Println("wrote", written, "bytes")
Es gibt eine mittlere Option, wenn Sie die zusätzliche Allokation vermeiden wollen, die DecodeString vornimmt: Decode schreibt in einen Zielpuffer, den Sie kontrollieren. Bestimmen Sie seine Größe mit DecodedLen, das die maximale Anzahl an Ausgabe-Bytes für eine gegebene Eingabelänge zurückgibt:
raw, err := os.ReadFile("payload.b64")
if err != nil {
panic(err)
}
buf := make([]byte, base64.StdEncoding.DecodedLen(len(raw)))
n, err := base64.StdEncoding.Decode(buf, raw)
if err != nil {
panic(err)
}
data := buf[:n] // die tatsächliche dekodierte Größe
fmt.Println(len(data), "bytes")
Seien Sie mit diesem letzten aber vorsichtig: Decode vertraut darauf, dass Sie den Puffer richtig groß dimensionieren. Wenn er zu klein ist, gibt die Methode keinen Fehler zurück, sie panickt mit einem Index out of range. DecodedLen ist die Zahl, die Sie verwenden, nicht len(raw).
Eine Base64-Kommandozeile für Go
Unix-Systeme liefern ein base64-Werkzeug mit coreutils, und Go liefert kein gleichwertiges Binary mit. Die idiomatische Antwort in der Go-Welt ist kein Paket, das man installiert, sondern ein Programm, das man selbst besitzt: ein kleines Kommandozeilen-Tool, aufgebaut um encoding/base64, das flag-Paket und die Standardeingabe. Hier ist ein komplettes, etwa vierzig Zeilen, das alles dekodiert, was eingepipet wird, und die rohen Bytes ausgibt:
package main
import (
"encoding/base64"
"flag"
"fmt"
"io"
"os"
)
func main() {
urlSafe := flag.Bool("url", false, "use the URL-safe alphabet")
flag.Parse()
enc := base64.StdEncoding
if *urlSafe {
enc = base64.URLEncoding
}
raw, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintln(os.Stderr, "read failed:", err)
os.Exit(1)
}
decoded, err := enc.DecodeString(string(raw))
if err != nil {
fmt.Fprintln(os.Stderr, "decode failed:", err)
os.Exit(1)
}
os.Stdout.Write(decoded)
}
Bauen Sie es einmal mit go build -o b64 ., und es wird zu einem plattformübergreifenden Decoder, den Sie in eine Makefile, eine CI-Pipeline oder eine Shell-Funktion stecken können: printf 'TWFu' | ./b64 druckt Man, und ./b64 -url < token.b64 > token.bin packt einen URL-sicheren Token in eine Datei aus. Zwei Eigenschaften des Designs lohnen die Aufmerksamkeit. Weil es die gesamte stdin liest, bevor es dekodiert, dekodiert umgebrochene Eingabe mit Zeilenumbrüchen problemlos, dank der Zeilenumbruch-Toleranz des Decoders. Und weil es bei falscher Eingabe mit Status 1 beendet und den Protest auf stderr schreibt, verhält es sich wie ein Werkzeug in einer Pipeline und nicht wie ein Skript, das sich entschuldigt. Das ist die ganze Kunst einer Go-CLI: ein Paket, ein Flag, Standard-Eingabe, Standard-Ausgabe und ein Exit-Code.
URL-sicheres Dekodieren
Die URL-sichere Variante existiert, weil das Standardalphabet mit der Grammatik von URLs kollidiert: + wird in Query-Strings oft als Leerzeichen gelesen, und / beginnt einen neuen Pfadabschnitt, daher muss ein Standard-Base64-String, der in einer URL eingebettet ist, Zeichen für Zeichen prozentmaskiert werden, was langsam zu parsen und hässlich zu lesen ist. Das alternative Alphabet von RFC 4648 tauscht + und / gegen - und _ aus, die beide in URL-Pfaden, Queries und Dateinamen unmaskiert erlaubt sind.
In Go ist der Wechsel einfach eine andere Decoder-Variable. Wenn Ihre Daten URL-sicher und gepaddet sind, verwenden Sie URLEncoding; wenn sie URL-sicher und ungepaddet sind, verwenden Sie RawURLEncoding. Der klassische Fall ist ein Identifikator, der in einer URL oder einem Dateinamen lebt:
decoded, err := base64.RawURLEncoding.DecodeString("-w9n")
// decoded sind die drei Bytes 0xfb 0x0f 0x67
// der Bindestrich und der Unterstrich sind Teil des URL-sicheren Alphabets,
// darum verarbeitet RawURLEncoding sie, wo StdEncoding scheitern würde
Wo Sie es in echtem Go-Code treffen werden: JWT-Segmente (als Nächstes behandelt), undurchsichtige Identifikatoren, die Systeme erzeugen und in URLs speichern, Dateinamen, die keinen Webserver oder keinen Cloud-Objektstore brechen dürfen, und jede API, die in ihrer Dokumentation "base64url" versprochen hat. Eine Warnung: URL-sicher ist ein Vertrag zwischen Produzent und Konsument, keine Eigenschaft der Daten. Wenn der String ein + oder ein / enthält, ist er nicht URL-sicher, aus ist, und egal, wie oft Sie es mit dem URL-Decoder erneut versuchen, wird es nicht besser. Schauen Sie zuerst auf die Zeichen, dann wählen Sie den Decoder.
In JWTs hineinschauen
Ein JSON Web Token besteht aus drei base64url-Segmenten, die durch Punkte getrennt sind: ein Header, ein Payload aus Claims und eine Signatur, und keines der Segmente hat Padding. Das macht ein JWT zu einer der häufigsten Dinge, die Sie in Go dekodieren werden, und der Header und der Payload sind ohne jeden Schlüssel lesbar, was sich sowohl fürs Debugging als auch für Security-Reviews im Gedächtnis zu halten lohnt:
package main
import (
"encoding/base64"
"fmt"
"log"
"strings"
)
func main() {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiR28gRGV2ZWxvcGVyIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.NwJQAKfJpMJQuK0gEECtXtO8cIoFnDp0ovXyl7dY1BQ"
parts := strings.Split(token, ".")
if len(parts) != 3 {
log.Fatal("not a JWT: expected three dot-separated parts")
}
for i, name := range []string{"header", "payload"} {
plain, err := base64.RawURLEncoding.DecodeString(parts[i])
if err != nil {
log.Fatalf("bad %s: %v", name, err)
}
fmt.Printf("%s: %s\n", name, plain)
}
// header: {"alg":"HS256","typ":"JWT"}
// payload: {"name":"Go Developer","sub":"1234567890"}
}
Achten Sie auf die Decoder-Wahl: RawURLEncoding, nicht StdEncoding. JWT-Segmente verwenden das URL-sichere Alphabet und tragen kein Padding, und ein Segment, dessen Länge ein oder zwei unter einem Vielfachen von vier liegt, bringt einen gepaddeten Decoder am allerletzten Ende zum Scheitern, was ein verwirrender Fehler zu verfolgen ist. Das Signatursegment können Sie ohne den Schlüssel nicht lesen, und Sie sollten nichts allein auf Basis des Payloads vertrauen, denn nichts hält einen Client davon ab, die ersten beiden Segmente zu fälschen. Wenn Sie Verifikation brauchen, verwenden Sie eine gepflegte Bibliothek. Die de-facto-Bibliothek ist github.com/golang-jwt/jwt/v5 (installieren Sie sie mit go get github.com/golang-jwt/jwt/v5):
package main
import (
"fmt"
"log"
"github.com/golang-jwt/jwt/v5"
)
func main() {
secret := []byte("hmac-secret")
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiR28gRGV2ZWxvcGVyIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.NwJQAKfJpMJQuK0gEECtXtO8cIoFnDp0ovXyl7dY1BQ"
parsed, err := jwt.Parse(token, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return secret, nil
})
if err != nil {
log.Fatal("token rejected:", err)
}
claims, _ := parsed.Claims.(jwt.MapClaims)
fmt.Println("subject:", claims["sub"])
}
Zwei Details aus der Bibliothek lohnen das Wissen. Erstens werden das base64url-Kodieren und -Dekodieren der drei Segmente intern abgewickelt, sodass Sie encoding/base64 beim Signieren oder Verifizieren nie direkt berühren. Zweitens lehnt die v5-Bibliothek Tokens mit alg=none ab, es sei denn, Sie übergeben deren UnsafeAllowNoneSignatureType-Konstante explizit, was Sie vor dem klassischen Fehler "nicht signiertes Token akzeptiert" schützt.
Data-URLs
Eine Data-URL ist eine URL, deren Payload die Daten selbst sind. Die Syntax, aus RFC 2397, lautet data:[mediatype][;base64],data: ein optionaler Medientyp, ein optionales ;base64-Flag, ein Komma und dann der Inhalt. Wenn das ;base64-Flag vorhanden ist, ist der Inhalt Standard-Base64, und deshalb teilen sich Data-URLs und dieser Artikel eine Sektion. Browser verwenden sie, um Bilder und Fonts direkt in HTML und CSS einzubetten, damit die Seite eine Anfrage weniger braucht:
<img src="data:image/png;base64,iVBORw0KGgo=" alt="pixel">
Die Standardbibliothek von Go hat keinen Data-URL-Helfer, aber das Format ist einfach genug, um es von Hand mit strings zu parsen, was die meisten Go-Programme tun:
package main
import (
"encoding/base64"
"fmt"
"strings"
)
func main() {
url := "data:image/png;base64,iVBORw0KGgo="
if !strings.HasPrefix(url, "data:") {
fmt.Println("not a data URL")
return
}
rest := url[len("data:"):]
comma := strings.Index(rest, ",")
if comma == -1 {
fmt.Println("missing comma")
return
}
meta := rest[:comma] // image/png;base64
encoded := rest[comma+1:] // iVBORw0KGgo=
if !strings.HasSuffix(meta, ";base64") {
fmt.Println("this variant is percent-encoded, not base64")
return
}
mediaType := strings.TrimSuffix(meta, ";base64")
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
fmt.Println("decode failed:", err)
return
}
fmt.Println(mediaType, "carries", len(decoded), "bytes")
}
Drei Fallgruben zu bedenken. Erstens ist das ;base64-Flag optional, und ohne es ist der Payload prozentmaskiertes ASCII statt Base64, prüfen Sie also das Suffix, bevor Sie einen Decoder rufen. Zweitens gilt, wenn der Medientyp weggelassen wird, text/plain;charset=US-ASCII als Standard, was für Bilder selten wichtig ist, aber Menschen überrascht, die andere Inhalte parsen. Drittens sind Data-URLs ein Kniff für kleine Payloads: Der RFC selbst sagt, das Schema sei nur für kurze Werte nützlich, und die 33-prozentige Größenvergrößerung von Base64 macht aus einem 500-Kilobyte-Logo einen 666-Kilobyte-String, der in Ihr HTML geklebt ist, nicht cachtbar und nicht teilbar. Verwenden Sie sie für Icons und Thumbnails, nicht für Videos.
HTTP- und API-Arbeit
Der mit Abstand häufigste Decode in Go-Webservices ist das JSON-Body-Feld: ein Upload-Formular, eine API-Antwort oder ein Webhook übergibt Ihnen einen String, der in Wirklichkeit eine Datei ist. Unmarshalen Sie in einen Struct, dann dekodieren Sie das Feld:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
)
type payload struct {
Avatar string `json:"avatar"`
}
func main() {
body := []byte(`{"avatar": "iVBORw0KGgo="}`)
var p payload
if err := json.Unmarshal(body, &p); err != nil {
fmt.Println("bad JSON:", err)
return
}
img, err := base64.StdEncoding.DecodeString(p.Avatar)
if err != nil {
fmt.Println("bad avatar:", err)
return
}
fmt.Println("avatar is", len(img), "bytes")
}
Wenn Ihre API sowohl Standard- als auch URL-sichere Strings akzeptiert, ist das pragmatische Muster, einen Decoder zu versuchen, und wenn er mit einer CorruptInputError in der Nähe des Endes fehlschlägt, den anderen zu versuchen, bevor Sie aufgeben. Führen Sie diesen Tanz nicht mehr als einmal auf, und fallen Sie niemals auf "Gleichheitszeichen streichen und hoffen" als allgemeine Strategie zurück.
Für HTTP-Basic-Auth dekodieren Sie überhaupt nichts, denn Go macht es für Sie. Request.BasicAuth, verfügbar seit Go 1.4, splittet den Authorization-Header für Sie und gibt den Benutzernamen und das Passwort zurück, nachdem es bereits den Standard-Base64-Decoder auf dem user:pass-Paar laufen gelassen hat, das RFC 2617 definiert:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok || user != "alice" || pass != "s3cret" {
w.Header().Set("WWW-Authenticate", `Basic realm="api"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
fmt.Fprintln(w, "hello", user)
})
http.ListenAndServe(":8080", mux)
}
Denken Sie daran, dass Basic-Auth Authentifizierung ist, kein Schutz: der Header ist Base64, nicht verschlüsselt, daher muss er nur über HTTPS reisen. Wenn Sie der Client sind, ist der Spiegelauf req.SetBasicAuth(user, pass), der denselben Header für Sie mit dem Standard-Encoder baut.
Eine defensive Gewohnheit für API-Handler: Beschränken Sie den Body, bevor Sie ihn dekodieren, mit http.MaxBytesReader oder einer äquivalenten Längenprüfung. Ein Base64-String dekodiert auf ungefähr drei Viertel seiner eigenen Länge, daher hält ein Body-Limit von N Bytes das dekodierte Ergebnis unter N Bytes, und der Speicher bleibt begrenzt, egal was ein bösartiger Client postet. Das Dekodieren eines unbegrenzten Bodies ist ein klassischer Vektor für Speichererschöpfung, denn der Angreifer steuert, wie viele Megabytes Text er in Binär umwandeln kann.
Legacy-Zeichensätze
Das Dekodieren von Base64 gibt Ihnen Bytes, und in modernen Systemen sind diese Bytes so gut wie immer UTF-8, in dem Fall ist string(decoded) die gesamte Geschichte. Aber Base64 ist ein altes Format, und vieles davon wurde von Systemen produziert, die Windows-1252, ISO-8859-1, Shift JIS oder einen anderen Single-Byte- oder Double-Byte-Legacy-Zeichensatz verwendeten. Wenn der Produzent das getan hat, sind die Bytes, die Sie dekodieren, kein gültiges UTF-8, und Go wird nicht so tun, als wären sie es: Es zeigt Ihnen Ersetzungszeichen, überall dort, wo eine Sequenz kaputt ist.
Die Antwort von Go ist das Modul golang.org/x/text, das legacy-kodierte Bytes in UTF-8 umwandelt (und wieder zurück) für die gängigen Zeichensätze. Der Umwandlungsschritt sitzt direkt nach dem Decode, und er braucht einen Funktionsaufruf:
package main
import (
"encoding/base64"
"fmt"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
func main() {
// "Café" von einem Legacy-Tool in Windows-1252 gespeichert,
// dann für den Transport base64-kodiert
encoded := "Q2Fm6Q=="
raw, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
fmt.Println("decode failed:", err)
return
}
utf8, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), raw)
if err != nil {
fmt.Println("charset conversion failed:", err)
return
}
fmt.Println(string(utf8)) // Café
}
Das Modul hat ein Unterpaket pro Zeichensatz-Familie: charmap für die Windows- und ISO-Single-Byte-Tabellen, japanese für Shift JIS und EUC-JP, korean für EUC-KR, simplifiedchinese für GB18030 und traditionalchinese für Big5. Die Faustregel lautet, nur umzuwandeln, wenn Sie den Zeichensatz des Produzenten tatsächlich kennen, denn das zweite Mal Umwandeln von UTF-8-Bytes schlägt nicht laut fehl; es verwurschtelt nur den Text. Im Zweifel behandeln Sie den Payload als Bytes und lassen den nachgelagerten Konsumenten entscheiden.
Streaming und chunkweises Dekodieren
NewDecoder haben Sie im Datei-Abschnitt gesehen; hier ist, warum er eine eigene Sektion verdient. Er ist ein wahrer Streaming-Adapter: Er zieht aus dem zugrunde liegenden Reader nur so viel wie nötig, dekodiert an Ort und Stelle und gibt eine CorruptInputError zurück, in dem Moment, in dem der Stream schlecht wird. Der gesamte Stream kann Terabytes groß sein; der Speicher, den Sie halten, ist Ihr Puffer und die Ausgabe, die Sie schreiben. Die beiden üblichen Konsumenten-Muster sind io.ReadAll für kleine Streams und io.Copy für alles andere:
small, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, r))
// gut für einen Konfig-Blob oder einen kleinen Anhang
w, err := io.Copy(out, base64.NewDecoder(base64.StdEncoding, r))
// gut für ein Video, ein Tarball-Archiv oder einen Restore-Job
Seit Go 1.22 hat das Paket auch AppendDecode, das in einen Puffer dekodiert, den Sie wiederverwenden, statt pro Aufruf einen frischen Slice zu allozieren. Es ist das Werkzeug für heiße Pfade, die in einer Schleife viele Chunks dekodieren, wie einen Zeilen-Processor oder einen Protokoll-Decoder:
var buf []byte
for _, chunk := range chunks {
buf, err = base64.StdEncoding.AppendDecode(buf, chunk)
if err != nil {
return err
}
process(buf)
}
Die Methode hängt den dekodierten Chunk an, was buf bereits hält, und gibt den erweiterten Slice zurück, wobei das zugrunde liegende Array bei Bedarf wächst. Im ruhigen Betrieb, wenn der Puffer bereits auf die richtige Größe gewachsen ist, führt sie null Allokationen pro Chunk aus, was in einem Benchmark deutlich sichtbar wird. Wenn Ihre Arbeitslast "einmal dekodieren, selten" ist, ist DecodeString die einfachere Wahl; wenn es "tausendfach dekodieren in einer engen Schleife" ist, greifen Sie zu AppendDecode.
Es sicher halten
Ein paar Sicherheitshinweise, die spezifisch dafür sind, wie Go-Programme dieses Paket tatsächlich verwenden. Erstens ist Base64 Kodierung, keine Verschlüsselung. Ein Base64-String ist für jeden mit den Entwickler-Tools eines Webbrowsers lesbar, daher ist "wir Base64 das Passwort, bevor wir es senden" keine Sicherheitsmaßnahme, sondern ein Transport-Komfort. Die Vertraulichkeit muss aus TLS kommen, nicht aus dem Alphabet.
Zweitens, beschränken Sie Ihre Eingaben. Die dekodierte Größe eines Base64-Strings ist höchstens DecodedLen seiner Länge, prüfen Sie also diese Zahl gegen ein Limit, bevor Sie allozieren, und wickeln Sie Request-Bodies in eine Größenbeschränkung ein, bevor irgendetwas einen Decoder berührt. Beide Prüfungen sind je eine Zeile, und zusammen verwandeln sie einen unbegrenzten Decode in einen begrenzten.
Drittens, entscheiden Sie Ihre Haltung zu schlampiger Eingabe. Der normale Modus verwirft die ungenutzten Schlussbits der letzten Gruppe stillschweigend, was bedeutet, dass zwei verschiedene Strings zu denselben Bytes dekodiert werden können. Für die meisten Daten ist das egal. Für alles, was Teil eines Protokolls, einer signierten Nachricht oder eines Wertes ist, der verglichen oder gespeichert wird, ist Strict() die konservative Wahl, denn es macht die kanonische Form zur einzigen akzeptierten Form.
Viertens, seien Sie vorsichtig, wohin dekodierte Bytes gehen. Wenn ein dekodierter Wert zu einem Dateinamen, einem Pfad, einem SQL-Fragment oder einem Kommando-Argument wird, hat die Base64-Ebene Sie vor nichts geschützt: Die Bytes sind jetzt die unzuverlässige Eingabe Ihres Programms, und die üblichen Sanitizing-Regeln gelten genauso wie für beliebige andere Nutzerdaten.
Wie schnell ist der Decoder?
Base64 in Go ist schnell, und es bleibt schnell auf großen Daten, denn die Implementierung ist eine einfache Tabellen-Lookup-Schleife ohne Reflexion und ohne Allokation pro Zeichen. Auf einer aktuellen Desktop-CPU, die Go 1.26 ausführt, dekodiert ein 500-Byte-String in etwa einem Viertel einer Mikrosekunde mit einer Allokation, was in der Größenordnung von zwei Gigabytes pro Sekunde liegt. Ein Megabyte Base64 dekodiert in deutlich unter einer Millisekunde; ein Gigabyte in deutlich unter einer Sekunde. Die Zahlen bewegen sich mit der Hardware, aber die Form nicht: Das Base64-Dekodieren ist so gut wie nie der Flaschenhals, meistens ist es das Netzwerk oder die Festplatte drumherum.
Wenn Sie in einer heißen Schleife sind, ist das Allokationsprofil das, was Sie beobachten sollten. DecodeString alloziert den Ergebnis-Slice bei jedem Aufruf. Decode mit einem vorab dimensionierten Ziel und AppendDecode mit einem wiederverwendeten Puffer vermeiden diese Allokation im ruhigen Betrieb ganz. Für ein Dekodieren, das ein paar Mal pro Anfrage passiert, ist das hier alles egal; für ein Dekodieren, das ein paar Millionen Mal pro Sekunde passiert, ist es der Unterschied zwischen einem flachen Speicherprofil und einem quälend arbeitenden Garbage Collector.
Eine kurze Geschichte des Pakets
Das base64-Paket ist eines der ältesten Teile der Go-Standardbibliothek. Der Copyright-Header der Quelldatei lautet 2009, das Jahr, in dem die Sprache geschaffen wurde, und das Paket ist seit der allerersten stabilen Version, Go 1.0, im März 2012, Teil der Standardbibliothek. Das bedeutet, dass das DecodeString, das Sie heute aufrufen, dieselbe API mit demselben Verhalten ist, die Go-Programme seit über einem Jahrzehnt aufrufen.
Das Wachstum seitdem war bescheiden und nützlich. Go 1.5 im August 2015 fügte die ungepaddeten RawStdEncoding- und RawURLEncoding-Werte hinzu, die die Tür für JWT-artige kompakte Strings öffneten. Go 1.8 im Februar 2017 fügte Strict() hinzu, damit Protokolle kanonische Eingabe verlangen können. Go 1.22 im Februar 2024 fügte AppendDecode und AppendEncode zur ganzen Familie der Base-Kodierungen hinzu und verschärfte WithPadding, damit es Unsinn-Argumente ablehnt. Und Stand September 2026, mit Go 1.27.1 als neuester Version und Go 1.26 als der anderen unterstützten Linie, ist die API exakt die, die in diesem Artikel beschrieben wird: vier fertige Kodierungen, ein Stream-Decoder, ein strikter Modus und eine Append-Familie für Performance.
Die tiefere Tatsache ist die Kompatibilitätszusage. Die Garantie von Go 1 bedeutet, dass das Paket für immer dieselben Eingaben akzeptieren und dieselben ablehnen wird, also wird ein Decoder, den Sie dieses Jahr für ein Datenformat schreiben, das 2015 produziert wurde, weiter funktionieren. Für ein Format, das so alt und so langweilig ist, ist das die beste Nachricht, die es gibt.
Dinge, die Sie überraschen werden
Nach einer Weile in Go werden Sie sich von base64 nicht mehr überraschen lassen, aber in den ersten paar Malen treffen einige dieser Fakten hart, also sind sie hier:
- Der Decoder überspringt
\rund\nüberall in der Eingabe, aber kein Leerzeichen, kein Tab, kein Nullbreitenleerzeichen. Die Nachsicht ist absichtlich; sie existiert, damit MIME-umgebundene Eingabe funktioniert, und sie stoppt genau dort, wo die Spezifikation stoppt. - Ein fehlgeschlagenes Dekodieren kann trotzdem echte Daten zurückgeben. Das Teilresultat ist alles, was vor dem defekten Byte dekodiert wurde, und der Fehler kommt zusammen mit ihm, nicht statt ihm.
CorruptInputErrorist wörtlich nur einint64mit einer Methode dran. Der "Fehler" ist der Offset, und die Meldung wird auf Abruf gebaut.- Sowohl
Decodeals auchEncodevertrauen darauf, dass Sie ihre Zielpuffer richtig groß dimensionieren. Geben Sie ihnen einen Puffer, der zu klein ist, und Sie erhalten keinen Fehler, Sie erhalten eine Panic. - Ein einzelnes Zeichen ist für keine der vier eingebauten Kodierungen gültige Eingabe. Ein Base64-Zeichen trägt sechs Bits, und ein Byte braucht acht, daher gibt es in einem Zeichen keine komplette Gruppe, gepaddet oder nicht.
- Stand August 2026 listen mehr als 244.000 öffentliche Pakete auf pkg.go.dev
encoding/base64unter ihren Imports. Es ist still eines der Pakete, von denen das gesamte Ökosystem am meisten abhängt.
Wo Decodes schiefgehen
Das sind die Dekodierfehler, die immer wieder in Go-Codebasen auftauchen, in etwa der Reihenfolge, in der sie in Support-Threads erscheinen:
StdEncodingfür URL-sichere Daten wählen (oder umgekehrt). Das Symptom ist ein Fehler beim ersten-,_,+oder/, und die Lösung ist, auf den String zu schauen, bevor Sie den Decoder wählen.- Einen String aus einem Terminal, einer E-Mail oder einer PDF einfügen, der Leerzeichen, Tabs oder Zeilenende-Artefakte einschleust. Go überspringt echte Zeilenumbrüche, aber ein Leerzeichen mitten im String ist ein defektes Byte, und der Offset im Fehler zeigt genau darauf.
- Zu vergessen, dass das Ergebnis ein
[]byteist. Wenn Sie es roh ausgeben, erhalten Sie eine Liste von Zahlen, und um es an eine Funktion zu füttern, die einen String erwartet, brauchen Sie einestring(...)-Konvertierung. - Den Fehler prüfen und dann trotzdem die Teildaten verwenden. Der halbdekodierte Präfix ist echt, aber er ist nicht der Payload, und Code, der ihn dafür hält, scheitert in der Produktion mit Daten, die exakt halb so lang sind, wie sie sein sollten.
- Den
Decode-Puffer mitlen(src)dimensionieren statt mitDecodedLen(len(src)). Die erste Größe ist in der anderen Richtung falsch, als man hofft, und die Panic, die sie auslöst, passiert nur bei großen Eingaben, was sie zu einem Liebling von Staging-Umgebungen macht. - Anzunehmen, dass JWT-Segmente Padding tragen. Das tun sie nicht, und ein gepaddeter Decoder scheitert beim letzten Zeichen mit einem Fehler, der sich wie ein Rätsel liest. Verwenden Sie
RawURLEncoding. - Zu glauben, dass alle Leerraum-Zeichen übersprungen werden. Das ist nicht der Fall. Nur die zwei Zeilenumbruch-Zeichen werden, und "Leerraum" in der Zwischenablage ist eine viel größere Familie als das.
- Doppel-Dekodieren, oder das Versagen, zweimal zu dekodieren, wenn der Wert Base64 von Base64 ist (eine Datei, die an eine E-Mail angehängt war, die selbst angehängt war). Die Prüfung ist eine Rundreise: einmal dekodieren, sehen, ob das Ergebnis immer noch wie Base64 aussieht, und erst dann erneut dekodieren.
Die andere Hälfte des Jobs
Das ist die gesamte Dekodier-Seite der Geschichte: ein Paket, vier fertige Decoder, ein Stream-Decoder für große Daten, ein strikter Modus für anspruchsvolle Protokolle und Fehlermeldungen, die Ihnen das Byte sagen, an dem etwas schiefging. Lernen Sie die Toleranz-Regeln, wählen Sie Ihren Decoder, indem Sie auf die Zeichen schauen, beschränken Sie Ihre Eingaben, und base64 in Go wird die langweilige, vorhersehbare, von Abhängigkeiten freie Nutzlichkeit, zu der es designed wurde.
Wenn der Job umkippt und Ihr Go-Programm Base64-Strings erzeugen muss, statt sie zu öffnen, deckt der verwandte Artikel über Base64-Kodierung in Go diese Seite im Detail ab: die Ein-Methoden-API des Encoders, der Close-Aufruf, der Ihre letzten zwei Bytes stillschweigend verschluckt, der Zeilenumbruch für MIME und wie die vier Kodierungen auf die Kanäle abgebildet sind, durch die sie reisen.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in Go: Ein vollständiger Leitfaden