Base64-Kodierung in Go: Ein vollständiger Leitfaden
Ab und zu muss Ihr Go-Programm binäre Daten an eine Welt übergeben, die nur Text akzeptiert: ein JSON-Feld, das ein String bleiben muss, eine URL, die ein einzelner Token bleiben muss, ein E-Mail-Anhang, der über Server wandert, die sich an die 7-Bit-Tage erinnern, ein Bild, das im HTML leben will, damit die Seite eine Anfrage spart. Base64 ist der Kurier für genau diesen Job, und die Startseite dieser Site erklärt das Format bereits in der Tiefe, also geht dieser Artikel direkt zur Kunst des Verpackens: Base64-Strings in Go zu erzeugen, die jeder Decoder auf dem Planeten ohne Widerstand öffnen kann.
Die gute Nachricht vorweg: Der Encoder ist die sanfte Hälfte der Geschichte. Eine Methode, kein Fehler-Rückgabewert, kein Fehlschlagmodus, byteidentische Ausgabe in jeder Go-Version seit der ersten stabilen. Das ganze Drama spielt sich um diese Methode ab: das Wählen des richtigen Alphabets für den Kanal, durch den der String reisen wird, der Close-Aufruf, der Ihre letzten zwei Bytes stillschweigend verschluckt, wenn Sie ihn vergessen, die Größensteuer, die das Format mit sich führt, und die Tatsache, dass Go, wie Python, Java und Node, seine Ausgabe nie bei 76 Zeichen umbreicht. Lernen Sie zuerst die Funktion kennen, dann die Fallen.
Verpacken ohne Fehlschlag
Neunzig Prozent des Kodieralltags in Go sind eine Methode auf dem Encoding-Typ, und wie jeder Kodierungseinstiegspunkt im Paket (Encode, AppendEncode) hat sie keinen Fehler-Rückgabewert:
func (enc *Encoding) EncodeToString(src []byte) string
Reichen Sie ihr Bytes, und sie gibt Ihnen einen String, und das ist der gesamte Vertrag:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
packed := base64.StdEncoding.EncodeToString([]byte("Man"))
fmt.Println(packed) // TWFu
}
Es gibt keinen Fehlerwert, denn es kann nichts schiefgehen: Jedes Byte ist erlaubte Eingabe, das Alphabet deckt es immer ab, und die Ausgabe ist immer reines ASCII. Drei Eigenschaften lohnen sich zu merken, denn sie beantworten die Hälfte aller zukünftigen Fragen. Erstens ist die Ausgabelänge eine reine arithmetische Funktion der Eingabelänge, und das Paket gibt Ihnen die Formel sogar als Methode: EncodedLen(n) liefert (n+2)/3*4 für gepaddete Kodierungen, also werden 3 Eingabe-Bytes zu 4 Zeichen, 6 zu 8, und so weiter. Zweitens trägt das Format eine Größensteuer: Aus jeweils drei Bytes Daten kommen vier Zeichen zurück, was die vertraute Vergrößerung von ungefähr 33 Prozent ist, die auf Ihren Bandbreitenrechnungen und Speicherquotas auftaucht. Drittens ist die Methode deterministisch: Dieselben Bytes erzeugen immer denselben String, auf jedem Rechner, in jeder Version von Go, für immer. Dieser Determinismus ist es, was Base64 zu einem Serialisierungsformat macht statt zu einem Rätsel.
Ein Go-spezifischer Hinweis auf der Eingabeseite: Die Methode nimmt []byte, nicht string, und die []byte(...)-Konvertierung ist an jeder Aufrufstelle explizit - Go konvertiert einen String nie für Sie in einen Slice - und sie erzeugt eine unabhängige Kopie der Bytes des Strings. Der Compiler kann diese Kopie weglassen, wenn der Slice nur gelesen wird und nicht entkommt, deshalb sind die Kosten normalerweise unmessbar; aber wenn der Slice gespeichert oder zurückgegeben wird, zahlt die Laufzeit eine echte O(n)-Kopie. Text in einem Go-Programm ist per Konvention UTF-8, was bedeutet, dass Sie, wenn Sie einen String kodieren, dessen UTF-8-Bytes kodieren, und genau das erwartet jeder moderne Decoder auf der anderen Seite. Mehr dazu im Abschnitt Text, Bytes und Unicode.
Wie Go es ausliefert
Wie alles in diesem Artikel kommt der Encoder aus dem Standardbibliothek-Paket encoding/base64, das seit der ersten Version der Sprache ausgeliefert wird und dessen Quelldatei noch ihren 2009-Copyright-Header trägt. Es gibt kein Modul zum Laden, keinen Feature-Flag zum Umlegen und keine Plattform-Macke: Wenn go version funktioniert, druckt go doc encoding/base64 die gesamte API für Sie.
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. Installieren Sie Go von den offiziellen Tarballs auf go.dev/dl, aus dem Paketmanager Ihrer Distribution (sudo apt install golang-go) oder über die golang.org/dl-Wrapper, wenn Sie Versionen jonglieren. Die Base64-API ist auf beiden unterstützten Linien identisch, und die Tabelle unten ist die gesamte Geschichte davon, was sich je geändert hat, was für ein so zentrales Paket eine kurze Liste ist:
| Version | Jahr | Was sich in encoding/base64 geändert hat |
|---|---|---|
| Go 1.0 | 2012 | Paket ab Tag eins stabil; Quell-Copyright 2009 |
| Go 1.5 | 2015 | RawStdEncoding und RawURLEncoding für ungepaddete Ausgabe hinzugefügt |
| Go 1.8 | 2017 | Strict() für kanonisches Dekodieren hinzugefügt (Decoder-Seite) |
| Go 1.22 | 2024 | AppendEncode und AppendDecode hinzugefügt; WithPadding lehnt nun falsche Argumente ab |
| Go 1.27.1 | 2026 | Aktuelle Version; API unverändert, Verhalten byte-stabil durch die Go-1-Zusage |
Die praktische Konsequenz dieser Geschichte: Code, der 2015 gegen diese API geschrieben wurde, kompiliert heute und verhält sich identisch, und die Strings, die Ihr Programm 2026 kodiert, dekodieren sich auf jeder Go-Version korrekt, vergangene oder zukünftige. Für ein Serialisierungsformat ist das die stille Superkraft.
Ein Alphabet für das Ziel wählen
Kodieren hat eine echte Entscheidung, und es ist eine Frage der Reise: Wohin wird dieser String gehen? Go gibt Ihnen vier fertige Encoder, und jeder ist auf einen anderen Kanal abgestimmt:
| Encoder | Alphabet | Padding | Schicken Sie ihn dorthin, wenn der String reist durch |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
JSON-Bodies, E-Mail-MIME-Teile, Data-URLs, HTTP-Basic-Auth, PEM, die meisten APIs |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
URL-Pfade und -Queries, Dateinamen, überall dort, wo + oder / maskiert werden müssten |
RawStdEncoding |
A-Z a-z 0-9 + / |
kein | Kompakte Standardalphabet-Strings, wo Padding nicht erscheinen darf |
RawURLEncoding |
A-Z a-z 0-9 - _ |
kein | JWT-Segmente, kompakte Identifikatoren, in URLs eingebettete Tokens |
Die Logik hinter den Varianten ist die Logik hinter dem Format selbst. Das Standardalphabet ist das, was MIME und die meisten APIs erwarten, also ist es der Standard und die sichere Antwort, wenn Ihnen niemand etwas anderes gesagt hat. Das URL-sichere Alphabet existiert, weil + und / Reservierungszeichen in URLs sind: ein Plus in einem Query-String wird oft als Leerzeichen gelesen, und ein Slash beginnt einen neuen Pfadabschnitt, also bricht Standard-Base64 in einer URL entweder ab oder braucht prozent-Maskierung für die Zeichen, die +, / oder = tragen - ein paar Prozent eines typischen Tokens. Sie durch - und _ zu ersetzen, die in Pfaden, Queries und Dateinamen unmaskiert erlaubt sind, ist die Korrektur, die RFC 4648 standardisiert hat. Die Raw-Varianten streichen die schließenden Gleichheitszeichen ganz, was in Kontexten wichtig ist, in denen Padding entweder verboten ist oder einfach nie verwendet wird, wie bei JWT-Segmenten. Die Regel, die Sie vor dem meisten Debugging rettet: Der Encoder, den Sie wählen, und der Decoder, den die andere Seite verwendet, sind ein Vertrag, und den Vertrag schreibt das Ziel, nicht Sie.
Wenn ein System, mit dem Sie sprechen, ein privates 64-Zeichen-Alphabet definiert hat, baut Ihnen base64.NewEncoding("...64 chars...") einen Encoder dafür, und WithPadding(rune) lässt Sie das Padding-Zeichen wechseln oder es mit NoPadding deaktivieren. Beide Funktionen panicken bei ungültigen Argumenten (falsche Alphabetlänge, doppeltes Zeichen, Zeilenumbruch im Alphabet, Padding-Zeichen, das mit dem Alphabet kollidiert), bauen Sie also Ihre Custom-Encoder einmal, beim Start, nie in einem heißen Pfad.
Die Close-Falle
Hier ist die berühmteste Falle in diesem Paket, und sie taucht nur auf, wenn Sie einen Stream kodieren statt eines Strings. NewEncoder wickelt jeden io.Writer in einen base64-kodierenden Writer, und weil Base64 in Blöcken von drei Eingabe-Bytes arbeitet, die vier Ausgabe-Zeichen produzieren, muss der Encoder Ihre letzten ein oder zwei Bytes puffern und wartet darauf zu sehen, ob noch mehr kommen. Sie werden nur geflusht, wenn Sie ihn schließen:
package main
import (
"bytes"
"encoding/base64"
"fmt"
)
func main() {
var buf bytes.Buffer
enc := base64.NewEncoder(base64.StdEncoding, &buf)
enc.Write([]byte("hello"))
fmt.Println(buf.String()) // aGVs -- wo ist das "lo"?
buf.Reset()
enc = base64.NewEncoder(base64.StdEncoding, &buf)
enc.Write([]byte("hello"))
enc.Close()
fmt.Println(buf.String()) // aGVsbG8= -- die vollständige Kodierung von "hello"
}
Die erste Ausgabe ist die gesamte Lektion: Ohne Close gab der Encoder nur den ersten kompletten Block aus, drei Bytes von "hello" wurden zu "aGVs", und die verbleibenden zwei Bytes verschwanden einfach in den internen Puffer. Die zweite Ausgabe, nach Close, ist der korrekte, vollständige String. Die Lösung ist eine Gewohnheit, keine Technik: In dem Moment, in dem Sie einen Encoder erzeugen, erzeugen Sie auch seine Bereinigung:
enc := base64.NewEncoder(base64.StdEncoding, w)
defer enc.Close() // denken Sie daran, den zurückgegebenen Fehler im Produktionscode zu prüfen
Zwei Details machen diese Falle schärfer, als sie aussieht. Erstens macht Close echte Arbeit: Es flusht den ausstehenden Teilblock und es kann fehlschlagen, denn es schreibt in den zugrunde liegenden Writer, also prüft die idiomatische Version seinen Fehler, besonders wenn das Ziel ein Netzwerk oder eine Festplatte ist. Zweitens sagt die Dokumentation, dass es ein Fehler ist, Write nach Close aufzurufen, aber die Laufzeit erzwingt diesen Satz nicht. Wenn Sie nach dem Schließen wieder schreiben, beginnt der Encoder still einen frischen Block und hängt ihn an, was einen String mit Padding in der Mitte erzeugt, was ungültiges Base64 ist, das die meisten Decoder mit einem verwirrenden Offset ablehnen werden. Der Vertrag ist es, den Sie einhalten müssen.
Zeilenumbruch, der Go-Weg
Jede andere große Base64-Implementierung, die Sie je verwendet haben, bricht ihre Ausgabe um: MIME will Zeilen von höchstens 76 Zeichen, PEM verwendet 64, E-Mail-Clients auf der ganzen Welt setzen immer wieder ein CRLF ein. Der Go-Encoder tut nichts davon. Er gibt eine durchgehende Zeile aus, egal wie groß der Payload ist, und das tut er seit der Geburt des Pakets. Die Ausgabe für ein Megabyte Daten ist eine einzelne Zeile von einem Megabyte und einem Drittel, von Anfang bis Ende, keine Unterbrechungen.
Das ist eine bewusste Wahl, kein Übersehen. Das Format funktioniert identisch mit oder ohne die Zeilenumbrüche, der eigene Decoder von Go überspringt sie überall in der Eingabe, und ein Encoder, der still CRLFs in Ihre Daten einschiebt, würde Programme überraschen, die den String in einer Datenbankspalte speichern oder auf Gleichheit vergleichen. Der Preis ist, dass Sie selbst umbrechen müssen, wenn der Kanal es verlangt, was ein kleiner Helfer ist:
package main
import (
"bytes"
"encoding/base64"
"fmt"
)
func wrapAt(s string, width int) string {
var out bytes.Buffer
for i := 0; i < len(s); {
end := i + width
if end > len(s) {
end = len(s)
}
out.WriteString(s[i:end])
out.WriteByte('\n')
i = end
}
return out.String()
}
func main() {
raw := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x42}, 100))
fmt.Print(wrapAt(raw, 76))
}
Ein Hinweis zur Reisrichtung: Weil der Go-Decoder Zeilenumbrüche überall ignoriert, dekodiert umgebrochene Eingabe auf der Go-Seite jeder Brücke perfekt. Die andere Richtung braucht Vorsicht: Wenn Sie umgebrochene Ausgabe an einen Konsumenten senden, der keine Unterbrechungen erwartet (ein JSON-Feld, eine URL, ein Token), streichen Sie sie zuerst, denn dieser Konsument könnte einen Zeilenumbruch als defektes Zeichen behandeln. Wissen Sie, in welcher Konvention Ihr Kanal lebt, und geben Sie sie absichtlich aus.
Verpacken für E-Mail und MIME
E-Mail ist das älteste Zuhause von Base64. Das ursprüngliche SMTP-Protokoll wurde entwickelt, um 7-Bit-ASCII zu transportieren, daher wurden Anhänge vor dem Senden base64-kodiert und bei der Ankunft dekodiert, und der MIME-Standard (RFC 2045) formalisierte die Praxis: Der Header Content-Transfer-Encoding: base64 markiert einen Teil, und der Body sollte in Zeilen von höchstens 76 Zeichen aufgeteilt werden, mit CRLF dazwischen.
Das net/smtp-Paket von Go sendet die Bytes, die Sie ihm geben, und es baut MIME-Teile nicht für Sie, also sieht das Base64-Stück in einem Programm, das E-Mail erstellt, so aus:
package main
import (
"bytes"
"encoding/base64"
"fmt"
)
func main() {
body := []byte("hi from Go")
var part bytes.Buffer
part.WriteString("Content-Transfer-Encoding: base64\r\n")
part.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n")
encoded := base64.StdEncoding.EncodeToString(body)
for i := 0; i < len(encoded); i += 76 {
end := i + 76
if end > len(encoded) {
end = len(encoded)
}
part.WriteString(encoded[i:end] + "\r\n")
}
fmt.Print(part.String())
}
Drei Dinge zu beachten. Der Standard-Encoder ist hier der richtige, denn MIME ist der ursprüngliche Standardalphabet-Kontext. Die Zeilenumbrüche sind CRLF, nicht das native Zeilenende der Plattform, denn das ist es, was der RFC vorgibt und was E-Mail-Parsers erwarten. Und wenn Ihr Programm E-Mail in großer Menge sendet, wird eine gepflegte MIME-Bibliothek die gesamte Nachricht für Sie bauen; der Punkt dieses Beispiels ist die Base64-Hälfte, der Teil, der zu diesem Paket gehört. Bekommen Sie das Alphabet und die Zeilenkonvention richtig, und der Rest von MIME ist das Problem eines anderen.
Dateien verpacken
Für Dateien, die in den Speicher passen, ist das Muster dieselben zwei Zeilen wie überall anders: einlesen, dann EncodeToString. Für Dateien, die das nicht tun, hält Streaming Ihren Speicher flach, und das Rezept ist eine Datei, ein Encoder, eine Kopie und zwei Schließungen in der richtigen Reihenfolge:
in, err := os.Open("photo.jpg")
if err != nil {
panic(err)
}
defer in.Close()
out, err := os.Create("photo.b64")
if err != nil {
panic(err)
}
enc := base64.NewEncoder(base64.StdEncoding, out)
if _, err := io.Copy(enc, in); err != nil {
panic(err)
}
if err := enc.Close(); err != nil {
panic(err) // flusht den letzten Teilblock
}
if err := out.Close(); err != nil {
panic(err)
}
Die Reihenfolge der Schließungen ist der subtile Teil, und sie ist die Datei-Version der Close-Falle: Der Encoder muss vor der Datei geschlossen werden, denn enc.Close ist es, was den letzten Teilblock in die Datei schreibt, und die Datei zuerst zu schließen würde diesen Block in einen Puffer lassen, der in nichts schreibt. Bei defer denken Sie daran, dass defer-Aufrufe in umgekehrter Reihenfolge ausgeführt werden, also ist es das Registrieren von out.Close zuerst und enc.Close zweitens (oder, wie im obigen Beispiel, das ausdrückliche Schließen des Encoders vor dem Deferieren der Datei), was die Abfolge sicher macht.
Halten Sie die Größensteuer im Kopf, wenn Sie um dieses Muster planen: Ein 10-Megabyte-Foto wird zu ungefähr 13,3 Megabytes Text, und ein 100-Megabyte-Archiv zu einem 133-Megabyte-String auf der Festplatte. Wenn das Ziel eine Quote, ein Limit oder einen Preis pro Byte hat, wird die Base64-Version Ihrer Datei gezählt, nicht das Original.
Verpacken für das Web: Data-URLs
Browser laden gern ein Bild oder einen Font aus einem String, der im HTML oder CSS selbst lebt, und dieser String ist eine Data-URL: der Medientyp, das ;base64-Flag, ein Komma und der Payload, alles in einer einzigen URL. Go hat keinen Data-URL-Helfer, aber einen zu bauen ist String-Konkatenation, denn das Format ist ein Vertrag, den man schriftlich stehen sehen kann:
package main
import (
"fmt"
"os"
"encoding/base64"
)
func main() {
img, err := os.ReadFile("logo.png")
if err != nil {
panic(err)
}
url := "data:image/png;base64," + base64.StdEncoding.EncodeToString(img)
fmt.Println(url)
// data:image/png;base64,iVBORw0KGgo...
}
Zwei Regeln halten Data-URLs aus dem Gestrüpp. Schließen Sie immer den Medientyp ein: Er ist in der Grammatik optional (der Standard ist text/plain;charset=US-ASCII), aber ein Browser, der den Typ Ihres binären Payloads raten muss, ist kein Szenario, das Sie wollen. Und behandeln Sie Data-URLs als Trick für kleine Assets. Der RFC sagt, das Schema sei nur für kurze Werte nützlich, und die 33-prozentige Vergrößerung ist es, was den Unterschied macht zwischen einem 2-Kilobyte-Icon, das eine Anfrage spart, und einem 5-Megabyte-Foto, das jeden Page-Load aufbläht, ohne Cache, der es teilt, und ohne URL, die man jemandem geben kann. Icons, Favicons, kleine Sprites: ja. Produktfotografie: nein.
Verpacken für HTTP
Drei HTTP-Kontexte dominieren Base64 in Go-Services, und zwei davon bringen eingebauten Helfer mit. Der erste ist der JSON-Body, das Arbeitstier: Sie kodieren einen Wert vor dem Marshalen, und das Feld trägt einen gewöhnlichen String über die Leitung:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
)
type avatar struct {
Data string `json:"data"`
}
func main() {
png := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
a := avatar{Data: base64.StdEncoding.EncodeToString(png)}
body, err := json.Marshal(a)
if err != nil {
panic(err)
}
fmt.Println(string(body))
// {"data":"iVBORw0KGgo="}
}
Wenn ein Typ an vielen Orten auftaucht, ist der saubere Go-Zug, MarshalJSON und UnmarshalJSON darauf zu implementieren, damit der Base64-Schritt für jede Aufrufstelle unsichtbar ist. Der zweite Kontext ist HTTP-Basic-Auth, wo die Standardbibliothek den ganzen Job macht: Request.SetBasicAuth(user, pass) baut den Authorization-Header für Sie und lässt den Standard-Encoder über das user:pass-Paar laufen, das RFC 2617 vorgibt. Die eine Regel dort ist, nicht zu improvisieren: Basic-Auth ist Standard-Base64 mit einem Basic -Präfix, und ein URL-sicheres Alphabet oder ein fehlendes Padding-Zeichen wird einen funktionierenden Login in einen 401 verwandeln, den niemand erklären kann.
Der dritte Kontext sind URLs, wo der String der Payload eines Pfadabschnitts oder eines Query-Parameters ist. Hier ist das Standardalphabet eine schlechte Wahl, denn +, / und = kollidieren alle mit der URL-Grammatik, und jedes Vorkommen von ihnen braucht eine prozent-Maskierung. Kodieren Sie stattdessen mit der URL-sicheren Variante, und der Token überlebt die URL intakt. Wenn der Konsument ihn trotzdem prozent-maskiert, ist nichts kaputt, aber wenn er es nicht tut, haben Sie sich eine Klasse von 404s gespart.
URL-sichere Ausgabe
URL-sicheres Base64 verdient in Go seine eigene Sektion, weil es die Variante ist, zu der Sie öfter greifen werden als zum Standard, und weil Go den Wechsel gratis macht. Das alternative Alphabet aus RFC 4648 ersetzt + durch - und / durch _, sodass die Ausgabe in URL-Pfaden, Queries oder Dateinamen keine Maskierung braucht, und sie liest sich als ein einziger sauberer Token in einer Log-Zeile. Die zwei fertigen Encoder sind URLEncoding (gepaddet) und RawURLEncoding (ungepaddet):
raw := []byte{0xfb, 0x0f, 0x67, 0x01}
fmt.Println(base64.StdEncoding.EncodeToString(raw)) // +w9nAQ==
fmt.Println(base64.URLEncoding.EncodeToString(raw)) // -w9nAQ==
fmt.Println(base64.RawURLEncoding.EncodeToString(raw)) // -w9nAQ
Diese eine Eingabe, drei Ausgaben: Die Standardversion braucht eine prozent-Maskierung für ihr Pluszeichen, die URL-sichere Version ist ein Token, und die Raw-Version streicht auch das Padding. Die typischen Go-Jobs für jede: undurchsichtige Identifikatoren, die ein Service erzeugt und dann in URLs, Routen oder Dateinamen speichert; API-Tokens, die Clients in Query-Strings einfügen; alles, was in einer Log-Zeile auftaucht, wo ein Plus oder ein Slash nur ein Zeichen davon entfernt ist, für Syntax gehalten zu werden.
Die Disziplin, die das sauber hält, ist dieselbe wie überall in diesem Artikel: Die Variante ist ein Vertrag mit dem Konsumenten. Wenn die andere Seite Standard-Base64 erwartet und Sie URL-sicher senden, scheitert ihr Decoder beim ersten Bindestrich, und der Fehler wird ein Byte-Offset in der Nähe des Endes eines völlig guten Strings sein, was nicht offensichtlich zu debuggen ist. Im Zweifel fragen Sie, was die andere Seite erwartet, lesen Sie die Spezifikation, auf die sie verweist, und wählen Sie den Encoder vom Ziel, nicht aus Gewohnheit.
JWTs verpacken
JSON Web Tokens sind der sichtbarste Verbraucher von Base64 in modernen APIs, und sie pinnen die genaue Variante fest: JWS-Kompaktseralisierung, laut RFC 7515, besteht aus drei base64url-Segmenten ohne Padding, verbunden durch Punkte. Header, Payload, Signatur. Das bedeutet, dass der Encoder der Wahl für alles, was Sie von Hand bauen, RawURLEncoding ist:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
)
func main() {
secret := []byte("hmac-secret")
header, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
payload, _ := json.Marshal(map[string]any{"sub": "1234567890"})
signingInput := base64.RawURLEncoding.EncodeToString(header) + "." +
base64.RawURLEncoding.EncodeToString(payload)
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(signingInput))
signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
fmt.Println(signingInput + "." + signature)
}
Lesen Sie dieses Beispiel als eine Lektion darüber, was das Format ist, nicht als Empfehlung, es auszuliefern: Es zeigt genau, wo das Base64 sitzt (zweimal vor dem Signieren, einmal danach) und warum die Signatur die kodierten Segmente abdeckt, nicht das rohe JSON. In der Produktion signieren und verifizieren Sie mit einer gepflegten Bibliothek, denn JWT hat einen langen Schwanz an Fehlern (Uhrenabweichung bei der Ablaufzeit, Algorithmus-Verwechslung, fehlende Audience-Prüfungen), die die Base64-Ebene nicht sehen kann. Die de-facto-Go-Bibliothek ist github.com/golang-jwt/jwt/v5, installiert mit go get github.com/golang-jwt/jwt/v5:
package main
import (
"fmt"
"log"
"time"
"github.com/golang-jwt/jwt/v5"
)
func main() {
secret := []byte("hmac-secret")
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "1234567890",
"exp": time.Now().Add(time.Hour).Unix(),
})
signed, err := token.SignedString(secret)
if err != nil {
log.Fatal("signing failed:", err)
}
fmt.Println(signed)
}
Die Bibliothek führt das base64url-Kodieren jedes Segments intern durch, sodass Sie encoding/base64 überhaupt nicht berühren, was das beste Ergebnis ist: ein Ort weniger, an dem ein Padding- oder Alphabetfehler verstecken kann. Und beachten Sie den Schutz, den sie Ihnen gratis gibt: v5 lehnt Tokens ab, die alg=none behaupten, es sei denn, Sie optieren explizit mit deren UnsafeAllowNoneSignatureType-Konstante ein, was der Schutz ist, den Sie wollen, ohne darüber nachdenken zu müssen.
Text, Bytes und Unicode
Die Haltung von Go zu dieser Frage ist die kürzeste aller großen Sprachen, und sie ist der Grund, warum Base64 hier so angenehm ist: Ein string in Go ist eine schreibgeschützte Folge von Bytes, und der Text in Ihrem Programm ist UTF-8. Es gibt keine versteckte Kodierungsebene, keine "der String ist tatsächlich UTF-16"-Überraschung und keinen Zeichensatz-Flag zum Setzen. Wenn Sie EncodeToString([]byte(myText)) schreiben, kodieren Sie die UTF-8-Bytes des Textes, aus ist:
s := "Café ☕"
packed := base64.StdEncoding.EncodeToString([]byte(s))
fmt.Println(packed) // Q2Fmw6kg4piV
Diese eine Zeile ist die ganze Geschichte für modernen Text, einschließlich Emoji und CJK: Base64 arbeitet auf Bytes, UTF-8 ist nur eine Bytes-Folge, und jeder Decoder auf der anderen Seite, der dieselbe Konvention befolgt, gibt Ihnen denselben String zurück. Die []byte(...)-Konvertierung ist eine unabhängige Kopie, die der Compiler weglässt, wenn der Slice nur gelesen wird und nicht entkommt - in der Praxis kostet es also nichts Messbares.
Der eine Fall, in dem die Geschichte länger wird, sind Legacy-Daten: Bytes, die von einem Windows-1252-, Shift-JIS- oder ISO-8859-1-System produziert wurden und kein gültiges UTF-8 sind. Wenn Sie diese Bytes so wie sie sind base64-kodieren, haben Sie kaputten Text treu transportiert, was niemand haben wollte. Die Lösung ist, vor dem Kodieren zu normalisieren, mit golang.org/x/text, damit der Base64-String sauberes UTF-8 trägt, von dem Moment an, in dem er Ihr Programm verlässt:
import (
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
legacy := []byte{0x43, 0x61, 0x66, 0xE9} // "Café" als Windows-1252
utf8, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), legacy)
if err != nil {
panic(err)
}
packed := base64.StdEncoding.EncodeToString(utf8)
// Q2Fmw6k= -- dasselbe "Café", jetzt saubere UTF-8-Bytes, bereit zu reisen
Dasselbe Modul deckt neben charmap auch japanese, korean, simplifiedchinese und traditionalchinese ab. Die praktische Regel: Einmal konvertieren, an der Grenze, an der Legacy-Bytes in Ihr Programm eintreten, und ab dann ist alles, was Sie kodieren, UTF-8. Nicht zweimal konvertieren, nicht raten, und lassen Sie niemals einen Nicht-UTF-8-Payload in einen Base64-String schleichen, den ein moderner Konsument dekodieren und anzeigen wird.
Den Encoder messen
Der Encoder ist ein Tabellen-Lookup ohne Verzweigung auf die Eingabe und ohne Allokation jenseits des Ausgabe-Strings, und es zeigt sich in den Zahlen. Auf einer aktuellen Desktop-CPU, die Go 1.26 ausführt, dauert das Kodieren von 500 Bytes ungefähr drei Zehntel einer Mikrosekunde mit zwei Allokationen, was in der Größenordnung von anderthalb Gigabytes pro Sekunde liegt. Ein Megabyte Daten kodiert in deutlich unter einer Millisekunde; der Encoder wird selten irgendetwas sein, das Sie spüren.
Der eine Hebel, den es zu kennen lohnt, ist das Allokationsprofil in heißen Schleifen. EncodeToString alloziert den Ausgabe-String bei jedem Aufruf, was die richtige Abwägung für den 99-Prozent-Fall ist. Wenn Sie tausende Chunks pro Sekunde in einen wachsenden Puffer kodieren, hängt AppendEncode, hinzugefügt in Go 1.22, die kodierten Bytes an einen Slice, den Sie wiederverwenden, und führt im ruhigen Betrieb keine Allokation durch, sobald der Puffer auf die passende Größe gewachsen ist:
var out []byte
for _, chunk := range chunks {
out = base64.StdEncoding.AppendEncode(out, chunk)
}
Verwenden Sie EncodeToString für Einzelfälle, AppendEncode für enge Schleifen und NewEncoder für Streams und Dateien. Welches Sie auch wählen, denken Sie daran, dass das Netzwerk oder die Festplatte um den Encoder herum fast immer der langsame Teil ist, also profilieren Sie den ganzen Pfad, bevor Sie das Alphabet optimieren.
Sicherheitsüberlegungen
Der wichtigste Sicherheits-Satz in diesem Artikel: Base64 ist keine Verschlüsselung, und "wir Base64 es zuerst" ist keine Sicherheitsmaßnahme. Das Alphabet macht Daten text-sicher, nicht geheim, und jeder mit den Entwickler-Tools eines Browsers kann Ihr Base64 in einem Moment lesen. Vertraulichkeit kommt aus TLS und aus Zugriffskontrolle, und der Job von Base64 ist es, Bytes über einen nur-Text-Kanal zu bringen, ohne sie zu beschädigen. Halten Sie diese zwei Jobs in Ihrem Design und in Ihrer Dokumentation getrennt, und Sie vermeiden den klassischen "das Passwort ist geschützt, schau, es ist Base64"-Review-Kommentar.
Die zweite Überlegung ist die Größe. Weil das Format um ein Drittel expandiert, hat jedes Limit in Ihrem System eine Base64-Version: Eine API, die 4 Megabytes JSON akzeptiert, akzeptiert ungefähr 3 Megabytes Originaldaten, wenn der Payload ein Base64-Feld ist, eine URL mit einem Längen-Budget wird in rohen Bytes kürzer, wenn der Token URL-sicher und ungepaddet ist, und eine Datenbankspalte, die für den rohen Wert dimensioniert ist, kann für den kodierten zu klein sein. Machen Sie die Arithmetik mit EncodedLen, bevor Sie speichern, senden oder limitieren, und denken Sie daran, dass die Vergrößerung auf der Eingabe liegt, mit der Sie anfangen, nicht auf dem String, mit dem Sie enden.
Drittens, denken Sie darüber nach, wo der kodierte String beobachtet werden kann. Base64-Strings sind log-freundlich und bildschirm-freundlich, was eine Eigenschaft ist, bis ein 20-Megabyte-Anhang zu 26 Megabytes Text base64t, die Ihr Access-Log pflichtbewusst bei jeder Anfrage aufzeichnet. Loggen Sie die Länge, die ersten paar Dutzend Zeichen und den Identifikator, nicht den Payload, und Sie halten Ihre Logs lesbar und Ihre Festplatte am Leben. Schließlich, in URLs, bevorzugen Sie die URL-sichere Variante, damit Ihre Tokens keine ihrer Zeichen als prozent-Maskierungen ausgeben, was die URL aufbläht und gelegentlich ein Gateway oder einen Proxy auslöst, der eine strenge Meinung darüber hat, was in einen Query-String gehört.
Spannende Fakten und Go-Eigenheiten
Ein paar Fakten, die spezifisch für dieses Paket sind, für die Momente, in denen Sie in einem Code-Review recht haben wollen:
EncodeToStringist das Arbeitstier, und wie jeder Kodierungseinstiegspunkt im Paket (Encode,AppendEncode) hat es keinen Fehler-Rückgabewert - Kodieren kann in Go nicht fehlschlagen, was eine seltene und stille Art von Freiheit ist: Jedes Byte ist erlaubte Eingabe, und der einzige Weg, einen schlechten String zu bekommen, ist, das falsche Alphabet für den Kanal zu wählen.EncodedLenist reine Arithmetik,(n+2)/3*4für gepaddete Kodierungen, berechnet ohne Allokation und ohne Schleife. Es existiert, damit Sie Puffer und Quotas dimensionieren können, ohne je ein Byte zu kodieren.- Der interne Stream-Encoder versteckt einen 3-Byte-Eingabepuffer und einen 1024-Byte-Ausgabepuffer, deshalb schreibt
NewEncoderin Chunks, und deshalb kann der letzte Teilblock nur durchClosenach draußen. Die Puffer sind der Grund für die Falle. - Die Dokumentation sagt, dass es ein Fehler ist, nach dem Aufruf von
Closezu schreiben, aber die Laufzeit erzwingt den Satz nicht. Ein spätesWritewird akzeptiert, hängt einen frischen Block an und erzeugt einen String mit Padding in der Mitte: ungültiges Base64, höflich erzeugt, ohne einen Fehlerwert in Sichtweite. - Der Go-Encoder hat seine Ausgabe nie bei 76 Zeichen umgebrochen - wie die Encoder von Python, Java und Node produziert er eine Zeile für ein Megabyte Daten. Ihr MIME-Umbruch-Helfer ist ein privates Projekt, was auch ein guter Weg ist, sich zu merken, dass die Zeilenumbrüche in E-Mail-Base64 eine MIME-Konvention sind, keine Base64-Anforderung.
- Stand August 2026 importieren mehr als 244.000 öffentliche Pakete auf pkg.go.dev
encoding/base64. Was immer Ihr Go-Programm ist, es macht fast sicher irgendwo Base64, ob Sie es wissen oder nicht. - Die Go-1-Kompatibilitätszusage gilt für dieses Paket mit besonderer Wucht: Die Ausgabe eines Programms, das 2013 einen String kodiert hat, ist heute auf Go 1.27 byte-identisch. Base64-Strings sind in Go effektiv unsterblich.
Die Fehler, die immer wieder auftauchen
Die Kodierfehler, die immer wieder in Go-Codebasen auftauchen, in etwa der Reihenfolge, in der sie ankommen:
Closeam Stream-Encoder zu vergessen und einen String auszuliefern, dem seine letzten ein oder zwei Bytes fehlen. Der Bug überlebt jeden Test, der Eingaben verwendet, deren Länge ein Vielfaches von drei ist, so erreicht er die Produktion.- Die Datei vor dem Encoder zu schließen, sodass der letzte Teilblock in einen Dateihandle flusht, der schon weg ist. Die Ausgabe ist um exakt denselben Betrag abgeschnitten, und der Fehler zeigt sich nur bei Eingaben mit ungerader Größe.
- 76-Zeichen-Zeilenumbrüche in MIME- oder E-Mail-Ausgabe zu erwarten und verwirrt zu sein, wenn Go Ihnen eine lange Zeile reicht. Der Umbruch ist eine Kanal-Konvention, und in Go ist es die Aufgabe Ihres Codes, ihn anzuwenden.
- Das Standardalphabet in URLs zu verwenden und dann einen Nachmittag damit zu verbringen, 404ern und 400ern nachzujagen, die in Wirklichkeit ein prozent-Kodierungs-Problem sind. Wenn der String in einer URL leben wird, starten Sie mit
URLEncodingoderRawURLEncoding. - Padding auszugeben, wo der Konsument es verbietet: JWT-Segmente, einige Token-Formate, ein paar strenge Parsers. Die Raw-Varianten existieren genau aus diesem Grund, und die Fehlermeldung von der anderen Seite ist oft ein Byte-Offset am allerletzten Ende Ihres Strings.
- Ein Geheimnis zu Base64-ieren und es Schutz zu nennen. Es ist das nicht. Der Header, der Token, das "verschlüsselte" Feld: von jedem in einer halben Sekunde lesbar. Verwenden Sie TLS, verwenden Sie Hashing, wo ein Hash das ist, was das Protokoll will, und lassen Sie Base64 seinen einen ehrlichen Job tun.
- Die 33 Prozent zu vergessen, wenn Sie Limits setzen: Body-Größen, Spaltenbreiten, URL-Budgets, Quota-Prüfungen. Die Arithmetik ist ein Aufruf von
EncodedLen, und der Preis, sie zu überspringen, ist ein 413 oder eine abgeschnittene Spalte in der Produktion. - Text zu kodieren, der kein UTF-8 ist, was die Beschädigung treu transportiert. Normalisieren Sie Legacy-Zeichensätze mit
golang.org/x/text, bevor Sie kodieren, damit der Base64-String saubere Bytes trägt. - An den Encoder zu schreiben, nachdem Sie ihn geschlossen haben, aus Gewohnheit oder aus einer Retry-Schleife. Es wird kein Fehler ausgelöst, und die Ausgabe ist stillschweigend ungültig.
- Anzunehmen, dass der Decoder auf der anderen Seite so nachsichtig ist wie der von Go. Go überspringt Zeilenumbrüche überall, aber andere Sprachen und Parsers sind strenger bei Leerraum und bei Zeilenlänge, also passen Sie sich der Konvention des Kanals an, nicht der Stimmung der Go-Laufzeitumgebung.
Die andere Seite
Das ist die Kodier-Seite der Geschichte: eine Methode, die nicht fehlschlagen kann, vier Encoder, die zu den Kanälen passen, durch die ihre Strings reisen, ein Stream-Encoder mit einem verpflichtenden Close und ein Format, das Ihre Daten um ein Drittel vergrößert und seine Zeilen nie, jemals umbreicht. Wählen Sie das Alphabet vom Ziel, schließen Sie Ihre Encoder, machen Sie die Größenarithmetik im Voraus, und Base64 in Go bleibt die stille, von Abhängigkeiten freie Nutzlichkeit, die es seit 2009 ist.
Und wenn sich der Verkehr umkehrt, wenn Ihr Programm einen dieser Strings empfängt und ihn öffnen muss, deckt der verwandte Artikel über Base64-Dekodierung in Go diese Seite im Detail ab: die Toleranz-Regeln des Decoders, die Fehler-Offsets, die Ihnen das Byte sagen, an dem die Eingabe schiefgeht, der strikte Modus für anspruchsvolle Protokolle und dieselben vier Kodierungen aus der anderen Richtung.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Dekodierung in Go: Ein vollständiger Leitfaden