Base64-codering in Go: een complete gids
Nu en dan moet je Go-programma binaire data aanreiken aan een wereld die alleen tekst accepteert: een JSON-veld dat een string moet blijven, een URL die één enkel token moet blijven, een e-mailbijlage die servers passeert die de 7-bittedagen nog herinneren, een afbeelding die binnen de HTML wil wonen zodat de pagina een request over slaat. Base64 is de koerier voor precies dat werk, en de startpagina van deze site legt het formaat al diep uit, dus deze gids gaat direct naar het vakmanschap van het inpakken: base64-strings produceren in Go die elke decoder op aarde zonder vechtpartij kan openen.
Het goede nieuws vooraf: de encoder is de zachte helft van het verhaal. Eén methode, geen foutretour, geen faalmode, byte-identieke uitvoer in elke Go-release sinds de eerste stabiele. Het hele drama zit rondom die methode: het juiste alfabet kiezen voor het kanaal waar de string doorheen reist, de Close-aanroep die je laatste twee bytes stilletjes opslurpt als je hem vergeet, de groottebelasting die het formaat met zich meedraagt, en het feit dat Go, net als Python, Java en Node, zijn uitvoer nooit op 76 tekens omwikzelt. Maak eerst kennis met de functie, daarna met de valkuilen.
Inpakken zonder te falen
Negentig procent van het coderen in Go is één methode op het Encoding-type, en zoals bij elke coderingsingang in het pakket (Encode, AppendEncode) heeft hij geen foutretour:
func (enc *Encoding) EncodeToString(src []byte) string
Geef hem bytes, hij geeft je een string, en dat is het complete contract:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
packed := base64.StdEncoding.EncodeToString([]byte("Man"))
fmt.Println(packed) // TWFu
}
Er is geen foutwaarde omdat er niets mis kan gaan: elke byte is wettige invoer, het alfabet dekt het altijd, en de uitvoer is altijd pure ASCII. Drie eigenschappen zijn de moeite waard om te onthouden, omdat ze de helft van alle toekomstige vragen beantwoorden. Ten eerste is de uitvoerlengte een pure arithmetische functie van de invoerlengte, en het pakket reikt je de formule zelfs aan als methode: EncodedLen(n) geeft (n+2)/3*4 terug voor gevulde coderingen, dus 3 invoerbytes worden 4 tekens, 6 worden 8, en zo verder. Ten tweede draagt het formaat een groottebelasting: elke drie bytes data komen terug als vier tekens, wat de bekende uitbreiding van ruwweg 33 procent is die zich meldt in je bandbreedtefacturen en opslagquota's. Ten derde is de methode deterministisch: dezelfde bytes produceren altijd dezelfde string, op elke machine, in elke versie van Go, voor altijd. Dat determinisme is wat base64 een serialisatieformaat maakt in plaats van een raadsel.
Eén Go-specifieke opmerking over de invoerkant: de methode neemt []byte aan, geen string, en de []byte(...)-conversie is expliciet op elke aanroeplocatie - Go converteert een string nooit voor je naar een slice - en hij produceert een onafhankelijke kopie van de bytes van de string. De compiler kan die kopie weglaten wanneer de slice alleen wordt gelezen en niet ontsnapt, waardoor de kosten doorgaans onmeetbaar zijn; maar als de slice wordt opgeslagen of teruggegeven, betaalt de runtime een echte O(n)-kopie. Tekst in een Go-programma is per conventie UTF-8, dus wanneer je een string codeert, codeer je zijn UTF-8-bytes, en precies dat is wat elke moderne decoder aan de overkant verwacht. Meer daarover in de sectie Tekst, bytes en Unicode.
Hoe Go het levert
Zoals alles in deze gids komt de encoder uit het standaardbibliotheekpakket encoding/base64, dat er is sinds de eerste release van de taal en waarvan het bronbestand zijn copyright-opschrift uit 2009 nog draagt. Er is geen module om op te halen, geen featureflag om om te zetten en geen platform-eigenaardigheid: werkt go version, dan toont go doc encoding/base64 je de hele API.
Op het moment van schrijven is de nieuwste release Go 1.27.1, uitgekomen op 1 september 2026, met de Go 1.26-lijn (momenteel 1.26.8) als de andere ondersteunde track. Installeer Go via de officiële tarballs op go.dev/dl, via de pakketbeheerder van je distributie (sudo apt install golang-go), of via de golang.org/dl-wrapper als je versies juggle. De base64-API is identiek op beide ondersteunde lijnen, en de tabel hieronder is de complete geschiedenis van wat er ooit veranderde, wat een korte lijst is voor een pakket dat zo centraal staat:
| Release | Jaar | Wat er in encoding/base64 veranderde |
|---|---|---|
| Go 1.0 | 2012 | Pakket stabiel vanaf de eerste dag; copyright van de broncode 2009 |
| Go 1.5 | 2015 | RawStdEncoding en RawURLEncoding toegevoegd voor niet-gevulde uitvoer |
| Go 1.8 | 2017 | Strict() toegevoegd voor canoniek decoderen (decoderkant) |
| Go 1.22 | 2024 | AppendEncode en AppendDecode toegevoegd; WithPadding wijst nu slechte argumenten af |
| Go 1.27.1 | 2026 | Huidige release; API ongewijzigd, gedrag byte-stabiel dankzij de Go 1-belofte |
Het praktische gevolg van die geschiedenis: code die in 2015 tegen deze API is geschreven, compileert en gedraagt zich vandaag identiek, en de strings die je programma in 2026 codeert, decoderen correct in elke Go-release, verstreken of toekomstig. Voor een serialisatieformaat is dat de stille superkracht.
Een alfabet kiezen voor de bestemming
Coderen heeft één echte beslissing, en dat is een reisvraag: waar gaat deze string naartoe? Go geeft je vier kant-en-klare encoders, en elk is afgestemd op een ander kanaal:
| Encoder | Alfabet | Vulling | Stuur het daar wanneer de string doorheen reist |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
JSON-bodies, MIME-delen van e-mail, data-URLs, HTTP Basic auth, PEM, de meeste API's |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
URL-paden en queries, bestandsnamen, overal waar + of / geëscaped zou moeten worden |
RawStdEncoding |
A-Z a-z 0-9 + / |
geen | Compacte standaardalfabet-strings waar vulling niet mag verschijnen |
RawURLEncoding |
A-Z a-z 0-9 - _ |
geen | JWT-segmenten, compacte identifiers, tokens ingebed in URLs |
De redenering achter de varianten is de redenering achter het formaat zelf. Het standaardalfabet is wat MIME en de meeste API's verwachten, dus het is de standaard en het veilige antwoord als niemand je het tegenovergestelde heeft gezegd. Het URL-veilige alfabet bestaat omdat + en / gereserveerde tekens zijn in URLs: een plus in een query-string wordt vaak als spatie gelezen, en een slash begint een nieuw padsegment, dus standaard-base64 in een URL breekt of heeft percent-escaping nodig op de tekens die een +, / of = dragen - enkele procenten van een typisch token. Het wisselen daarvan voor - en _, die wettig ongeëscaped zijn in paden, queries en bestandsnamen, is de oplossing die RFC 4648 heeft gestandaardiseerd. De Raw-varianten gooien de afsluitende gelijkttekens helemaal weg, wat uitmaakt in contexten waar vulling of verboden is of simpelweg nooit gebruikt wordt, zoals JWT-segmenten. De regel die je de meeste debugwerk bespaart: de encoder die je kiest en de decoder die de andere kant gebruikt, zijn één contract, en dat contract wordt geschreven door de bestemming, niet door jou.
Heeft een systeem waarmee je praat een eigen alfabet van 64 tekens gedefinieerd, dan bouwt base64.NewEncoding("...64 chars...") je een encoder voor, en WithPadding(rune) laat je het vulteken wisselen of uitschakelen met NoPadding. Beide functies paniceeren bij ongeldige argumenten (een verkeerde alfabetlengte, een dubbel teken, een regeleinde in het alfabet, een vulteken dat bots met het alfabet), dus bouw je eigen encoders één keer, bij het opstarten, nooit in een heet pad.
De Close-val
Hier is de beroemdste val in dit pakket, en die verschijnt alleen wanneer je een stroom codeert in plaats van een string. NewEncoder wrapt elke io.Writer in een base64-coderende writer, en omdat base64 werkt in blokken van drie invoerbytes die vier uitvoertekens produceren, moet de encoder je laatste een of twee bytes bufferen en afwachten of er nog meer komen. Die worden alleen doorgegeven wanneer je hem sluit:
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 -- waar is de "lo"?
buf.Reset()
enc = base64.NewEncoder(base64.StdEncoding, &buf)
enc.Write([]byte("hello"))
enc.Close()
fmt.Println(buf.String()) // aGVsbG8= -- de volledige codering van "hello"
}
De eerste output is de hele les: zonder Close gaf de encoder alleen het eerste complete blok, werden drie bytes van "hello" tot "aGVs", en de overige twee bytes verdwenen simpelweg in de interne buffer. De tweede output, na Close, is de correcte, complete string. De oplossing is een gewoonte, geen techniek: het moment dat je een encoder aanmaakt, maak dan ook zijn opruiming aan:
enc := base64.NewEncoder(base64.StdEncoding, w)
defer enc.Close() // onthoud om de teruggegeven fout te checken in productiekode
Twee details maken deze val scherper dan hij eruitziet. Ten eerste doet Close écht werk: hij geeft het uitstaande gedeeltelijke blok door en hij kan falen, omdat hij naar de onderliggende writer schrijft, dus de idiomatische versie checkt zijn fout, zeker wanneer de bestemming een netwerk of een schijf is. Ten tweede zegt de documentatie dat het een fout is om Write na Close aan te roepen, maar de runtime afdwingt die zin niet. Schrijf je na het sluiten opnieuw, dan start de encoder stilletjes een vers blok en voegt het toe, wat een string oplevert met vulling in het midden, wat ongeldige base64 is die de meeste decoders zullen afwijzen met een verwarrende offset. Het contract is aan jou om na te komen.
Regelomwikkeling, op Go-manier
Elke andere grote base64-implementatie die je ooit hebt gebruikt, omwikzelt zijn uitvoer: MIME wil regels van hooguit 76 tekens, PEM gebruikt 64, en e-mailclients over de hele wereld voegen nu en dan een CRLF in. De encoder van Go doet niets van al dat. Hij produceert één onafgebroken regel, hoe groot de payload ook is, en dat doet hij sinds de geboorte van het pakket. De output voor een megabyte data is één enkele regel van een megabyte en een derde, van begin tot eind, zonder onderbrekingen.
Dat is een bewuste keuze, geen vergetelheid. Het formaat werkt identiek met of zonder regeleinden, de decoder van Go zelf slaat ze over waar dan ook in de invoer, en een encoder die stilletjes CRLFs in je data zou stoppen, zou programma's verrassen die de string in een databasekolom opslaan of vergelijken op gelijkheid. De prijs is dat je zelf moet omwikken wanneer het kanaal dat vereist, wat één kleine helper is:
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))
}
Eén opmerking over de reisrichting: omdat de decoder van Go regeleinden overal ignoreert, decodeert omgebroken invoer perfect aan de Go-kant van elke brug. De andere richting is waar voorzichtigheid geboden is: stuurt je omgebroken output naar een consument die geen onderbrekingen verwacht (een JSON-veld, een URL, een token), haal ze dan eerst eraf, omdat die consument een regeleinde kan behandelen als een corrupt teken. Weet in welke conventie je kanaal leeft, en pas ze bewust toe.
Inpakken voor e-mail en MIME
E-mail is de oudste thuisbasis van base64. Het oorspronkelijke SMTP-protocol was ontworpen om 7-bit ASCII te vervoeren, dus bijlagen werden vóór verzending base64-gecodeerd en bij aankomst gedecodeerd, en de MIME-standaard (RFC 2045) gaf de praktijk formeel: de header Content-Transfer-Encoding: base64 markeert een deel, en het body moet worden gebroken in regels van hooguit 76 tekens met CRLF ertussen.
Het net/smtp-pakket van Go stuurt de bytes die je geeft, en hij zal geen MIME-delen voor je bouwen, dus in een programma dat mail samenstelt, ziet het base64-deel er zo uit:
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())
}
Drie dingen om te merken. De standaard-encoder is hier de juiste, omdat MIME de oorspronkelijke standaardalfabet-context is. De regeleinden zijn CRLF, niet het platformeigen regeleinde, omdat dat wat de RFC voorschrijft en wat mailparsers verwachten. En als je programma échte e-mail in grote hoeveelheden verstuurt, zal een onderhouden MIME-bibliotheek het hele bericht voor je bouwen; het punt van dit voorbeeld is de base64-helft, en dat is het deel dat bij dit pakket hoort. Krijg het alfabet en de regelconventie goed, dan is de rest van MIME het probleem van iemand anders.
Bestanden inpakken
Voor bestanden die in het geheugen passen, is het patroon dezelfde twee regels als overal elders: lezen, dan EncodeToString. Voor bestanden die dat niet doen, houdt streaming je geheugen vlak, en het recept is een bestand, een encoder, een kopie en twee sluitingen in de juiste volgorde:
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) // geeft het laatste gedeeltelijke blok door
}
if err := out.Close(); err != nil {
panic(err)
}
De volgorde van de sluitingen is het subtiele deel, en het is de versie voor bestanden van de Close-val: de encoder moet vóór het bestand worden gesloten, omdat enc.Close is wat het laatste gedeeltelijke blok naar het bestand schrijft, en het bestand eerst sluiten laat dat blok in een buffer zitten die naar niets schrijft. Bij defer onthoud dan dat uitgestelde aanroepen in omgekeerde volgorde worden uitgevoerd: out.Close eerst registreren en enc.Close daarna (of, zoals in het voorbeeld hierboven, de encoder expliciet sluiten voordat je het bestand defers) maakt de volgorde veilig.
Houd de groottebelasting in gedachten wanneer je rond dit patroon plant: een foto van 10 megabyte wordt ruwweg 13,3 megabyte tekst, en een archief van 100 megabyte wordt een string van 133 megabyte op schijf. Heeft de bestemming een quota, een limiet of een prijs per byte, dan is de base64-versie van je bestand wat wordt geteld, niet het origineel.
Inpakken voor het web: data-URLs
Browsers laden graag een afbeelding of lettertype uit een string die zelf binnen de HTML of CSS leeft, en die string is een data-URL: de mediatype, de ;base64-vlag, een komma en de payload, alles in één URL. Go heeft geen data-URL-helper, maar er één bouwen is string-concatenatie, want het formaat is een contract dat je ziet opgeschreven:
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...
}
Twee regels houden data-URLs uit de problemen. Neem altijd de mediatype mee: die is optioneel in de grammatica (de standaard is text/plain;charset=US-ASCII), maar een browser die het type van je binaire payload gaat gokken, is geen scenario dat je wilt. En behandel data-URLs als een truc voor kleine assets. De RFC zegt dat het schema alleen nuttig is voor korte waarden, en de 33-procentuitbreiding is wat het verschil maakt tussen een icoon van 2 kilobyte dat een request bespaart en een foto van 5 megabyte die elke paginalaad opblaast, zonder cache om te delen en zonder URL om aan iemand te geven. Iconen, favicons, kleine sprites: ja. Productfotografie: nee.
Inpakken voor HTTP
Drie HTTP-contexten domineren base64 in Go-services, en twee daarvan komen met ingebouwde hulp. De eerste is het JSON-body, het werkpaard: je codeert een waarde voordat je die marshaalt, en het veld vervoert een platte string over de draad:
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="}
}
Komt een type op veel plaatsen voor, dan is de nette Go-beweging om MarshalJSON en UnmarshalJSON ervoor te implementeren, zodat de base64-stap onzichtbaar is voor elke aanroeplocatie. De tweede context is HTTP Basic-authenticatie, waar de standaardbibliotheek het hele werk doet: Request.SetBasicAuth(user, pass) bouwt de Authorization-header voor je, door de standaard-encoder over het user:pass-paar te laten draaien dat RFC 2617 voorschrijft. De ene regel daar is: niet improviseren: Basic auth is standaard-base64 met een Basic -prefix, en een URL-veilig alfabet of een ontbrekend vulteken maakt van een werkende login een 401 die niemand kan verklaren.
De derde context zijn URLs, waar de string de payload is van een padsegment of een query-parameter. Hier is het standaardalfabet een slechte keuze, omdat +, / en = allebei botsen met de URL-grammatica, en elke optredende instantie daarvan een percent-escape nodig heeft. Codeer in plaats daarvan met de URL-veilige variant, en het token overleeft de URL ongeschonden. Escapeert de consument het toch nog, dan is er niets kapot, maar doet hij dat niet, dan heb je jezelf een hele categorie 404's bespaard.
URL-veilige output
URL-veilige base64 verdient in Go een sectie op zich, omdat het de variant is waar je vaker naar zult grijpen dan naar de standaard, en omdat Go de wissel gratis maakt. Het alternatieve alfabet van RFC 4648 vervangt + door - en / door _, zodat de output geen escaping nodig heeft in URL-paden, queries of bestandsnamen, en het leest in een logregel als één schoon token. De twee kant-en-klare encoders zijn URLEncoding (gevuld) en RawURLEncoding (niet gevuld):
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
Die ene invoer, drie outputs: de standaardversie heeft een percent-escape nodig voor zijn plusteken, de URL-veilige versie is één token, en de raw-versie gooit de vulling ook weg. De typische Go-jobs voor elk: ondoorzichtige identifiers die een service genereert en daarna opslaat in URLs, routes of bestandsnamen; API-tokens die clients in query-strings plakken; alles wat in een logregel zal verschijnen waar een plus of een slash met één verkeerd teken voor syntaxis kan worden aangezien.
De discipline die dit schoon houdt is dezelfde als overal in deze gids: de variant is een contract met de consument. Verwacht de andere kant standaard-base64 en stuur je URL-veilig, dan faalt zijn decoder op het eerste koppelteken, en de fout zal een byte-offset zijn vlak bij het einde van een prima string, wat niet zo voor de hand ligt om te debuggen. Twijfel je, dan vraag je wat de andere kant verwacht, lees je de spec waarnaar hij verwijst, en kies je de encoder van de bestemming, niet van de gewoonte.
JWT's inpakken
JSON Web Tokens zijn de meest zichtbare consument van base64 in moderne API's, en ze pinnen de exacte variant vast: JWS-compacte serialisatie, volgens RFC 7515, is drie base64url-segmenten zonder vulling, door punten samengevoegd. Header, payload, signature. Dat betekent dat de encoder van keuze voor alles wat je met de hand bouwt RawURLEncoding is:
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)
}
Lees dat voorbeeld als een les in wat het formaat is, niet als een aanbeveling om het in productie te zetten: het toont precies waar de base64 zit (tweemaal vóór het signen, eenmaal daarna) en waarom de signature de gecodeerde segmenten dekt en niet het ruwe JSON. In productie signeer en verifieer je met een onderhouden bibliotheek, omdat JWT een lange staart van fouten heeft (klokafwijking bij de vervaldatum, algoritmeverwarring, ontbrekende audience-checks) die de base64-laag niet kan zien. De de facto Go-bibliotheek is github.com/golang-jwt/jwt/v5, geïnstalleerd met 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)
}
De bibliotheek voert het base64url-coderen van elk segment intern uit, dus je raakt encoding/base64 helemaal niet aan, en dat is het beste resultaat: één plek minder waar een vullings- of alfabetfout zich kan verschuilen. En let op de bescherming die je gratis krijgt: v5 wijst tokens af die alg=none claimen, tenzij je expliciet kiest met de UnsafeAllowNoneSignatureType-constant, en dat is de bescherming die je wilt zonder erover na te denken.
Tekst, bytes en Unicode
Go's houding ten opzichte van deze vraag is de kortste van elke grote taal, en het is de reden waarom base64 hier zo aangenaam is: een string in Go is een read-only reeks bytes, en de tekst in je programma is UTF-8. Er is geen verborgen coderingslaag, geen verrassing van "de string is eigenlijk UTF-16", en er is geen tekensetvlag om in te stellen. Schrijf je EncodeToString([]byte(myText)), dan codeer je de UTF-8-bytes van de tekst, punt:
s := "Café ☕"
packed := base64.StdEncoding.EncodeToString([]byte(s))
fmt.Println(packed) // Q2Fmw6kg4piV
Die ene regel is het hele verhaal voor moderne tekst, inclusief emoji en CJK: base64 werkt op bytes, UTF-8 is gewoon een byte-reeks, en elke decoder aan de andere kant die dezelfde conventie volgt, geeft je dezelfde string terug. De []byte(...)-conversie is een onafhankelijke kopie, die de compiler weglaat wanneer de slice alleen wordt gelezen en niet ontsnapt - dus in de praktijk kost het niets meetbaars.
Het ene geval waar het verhaal langer wordt, is verouderde data: bytes die zijn geproduceerd door een Windows-1252-, Shift JIS- of ISO-8859-1-systeem en geen geldige UTF-8 zijn. Codeer je die bytes zo base64, dan heb je kapotte tekst trouwelijk vervoerd, wat niemand wilde. De oplossing is om te normaliseren voordat je codeert, met golang.org/x/text, zodat de base64-string schone UTF-8 draagt vanaf het moment dat de string je programma verlaat:
import (
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
legacy := []byte{0x43, 0x61, 0x66, 0xE9} // "Café" in Windows-1252
utf8, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), legacy)
if err != nil {
panic(err)
}
packed := base64.StdEncoding.EncodeToString(utf8)
// Q2Fmw6k= -- dezelfde "Café", nu schone UTF-8-bytes klaar voor de reis
Dezelfde module dekt japanese, korean, simplifiedchinese en traditionalchinese naast charmap. De praktische regel: converteer één keer, aan de grens waar verouderde bytes je programma binnenkomen, en vanaf dat moment is alles wat je codeert UTF-8. Converteer niet twee keer, gok niet, en laat nooit een niet-UTF-8-payload insluipen in een base64-string die een moderne consument zal decoderen en tonen.
De encoder meten
De encoder is een tabelopzoeking zonder takken op de invoer en zonder allocatie buiten de outputstring om, en dat zie je aan de getallen. Op een recente desktop-CPU met Go 1.26 kost het coderen van 500 bytes ruwweg drie tiende van een microseconde met twee allocaties, wat neerkomt op de orde van een gigabyte en een halve per seconde. Een megabyte data codeert in ruim minder dan een milliseconde; de encoder zal zelden iets zijn wat je voelt.
De ene trekknop die de moeite waard is om te kennen, is het allocatieprofiel in hete loops. EncodeToString alloceert de outputstring bij elke aanroep, wat de juiste ruil is voor het 99-procentgeval. Codeer je duizenden chunks per seconde naar een groeiende buffer, dan voegt AppendEncode, toegevoegd in Go 1.22, de gecodeerde bytes toe aan een slice die je hergebruikt, en voert hij in de stabiele staat geen enkele allocatie uit zodra de buffer tot de grootte is gegroeid:
var out []byte
for _, chunk := range chunks {
out = base64.StdEncoding.AppendEncode(out, chunk)
}
Gebruik EncodeToString voor eenmalige acties, AppendEncode voor strakke loops en NewEncoder voor stromen en bestanden. Kies je wat dan ook, onthoud dan dat het netwerk of de schijf rondom de encoder vrijwel altijd het trage deel is, dus profileer het hele pad voordat je het alfabet optimaliseert.
Beveiligingsoverwegingen
De belangrijkste beveiligingszin in deze gids: base64 is geen versleuteling, en "we base64 het eerst" is geen beveiligingsmaatregel. Het alfabet maakt data tekstveilig, niet geheim, en iedereen met de developer tools van een browser kan je base64 in een oogwenk lezen. De vertrouwelijkheid komt van TLS en van toegangsbeheer, en het werk van base64 is om bytes over een puur tekstkanaal te krijgen zonder ze te beschadigen. Houd die twee taken gescheiden in je ontwerp en in je documentatie, dan vermijd je de klassieke reviewopmerking "het wachtwoord is beschermd, kijk, het is base64".
De tweede overweging is grootte. Omdat het formaat met een derde uitbreidt, heeft elke limiet in je systeem een base64-versie: een API die 4 megabyte JSON accepteert, accepteert ruwweg 3 megabyte oorspronkelijke data wanneer de payload een base64-veld is, een URL met een lengtebudget wordt in ruwe bytes korter wanneer het token URL-veilig en niet gevuld is, en een databasekolom die is afgemeten op de ruwe waarde kan te klein zijn voor de gecodeerde. Doe de rekensom met EncodedLen voordat je opslaat, verstuurt of limiteert, en onthoud dat de uitbreiding zit in de invoer waarmee je begint, niet in de string waarmee je eindigt.
Ten derde, denk na over waar de gecodeerde string waargenomen kan worden. Base64-strings zijn logvriendelijk en schermvriendelijk, en dat is een feature, totdat een bijlage van 20 megabyte wordt base64'd tot 26 megabyte tekst die je access log plichtsbewust opneemt bij elke request. Log de lengte, de eerste tientallen tekens en de identifier, niet de payload, dan houd je je logs leesbaar en je schijf in leven. En tenslotte: kies in URLs voor de URL-veilige variant, zodat je tokens geen van hun tekens kwijtgaan als percent-escapes, wat de URL opblaast en af en toe een gateway of proxy tot struikelen brengt die een strenge blik heeft op wat in een query-string hoort.
Leuke feiten en eigenaardigheden van Go
Een paar feiten die specifiek zijn voor dit pakket, voor de momenten dat je in een code review gelijk wilt hebben:
EncodeToStringis het werkpaard, en zoals bij elke coderingsingang in het pakket (Encode,AppendEncode) heeft hij geen foutretour - coderen kan niet falen in Go, wat een zeldzame en stille vorm van vrijheid is: elke byte is wettige invoer, en de enige manier om een slechte string te krijgen, is het verkeerde alfabet kiezen voor het kanaal.EncodedLenis pure rekenkunde,(n+2)/3*4voor gevulde coderingen, berekend zonder allocatie en zonder loop. Hij bestaat zodat je buffers en quota's kunt dimensioneren zonder ooit een byte te coderen.- De interne stroomencoder verbergt een invoerbuffer van 3 bytes en een uitvoerbuffer van 1024 bytes, en daarom schrijft
NewEncoderin chunks en kan het laatste gedeeltelijke blok alleen viaCloseweg. De buffers zijn de reden voor de val. - De documentatie zegt dat het een fout is om te schrijven nadat je
Closehebt aangeroepen, maar de runtime afdwingt die zin niet. Een lateWritewordt geaccepteerd, voegt een vers blok toe en produceert een string met vulling in het midden: ongeldige base64, beleefd gegenereerd, zonder foutwaarde in zicht. - De encoder van Go heeft zijn output nooit op 76 tekens omgebroken - net als de encoders van Python, Java en Node produceert de encoder van Go één regel voor een megabyte data. Je MIME-omwikkelhelper is een persoonlijk project, wat ook een goede manier is om te onthouden dat de regeleinden in e-mail-base64 een MIME-conventie zijn en geen base64-eis.
- Per augustus 2026 importeren meer dan 244.000 openbare pakketten op pkg.go.dev
encoding/base64. Wat je Go-programma ook is, het doet vrijwel zeker ergens base64, of je het nu weet of niet. - De compatibiliteitsbelofte van Go 1 geldt voor dit pakket met speciale kracht: de output van een programma dat in 2013 een string codeerde, is vandaag byte-identiek op Go 1.27. Base64-strings zijn in Go effectief onsterfelijk.
De fouten die steeds opnieuw terugkomen
De coderingsfouten die steeds opnieuw opduiken in Go-codebases, ongeveer in de volgorde waarin ze binnenvallen:
Closevergeten op de stroomencoder, en een string uitbrengen die zijn laatste een of twee bytes mist. De bug overleeft elke test die invoer gebruikt waarvan de lengte een veelvoud van drie is, en zo belandt ze in productie.- Het bestand sluiten vóór de encoder, zodat het laatste gedeeltelijke blok wordt doorgeschreven naar een bestandshandle die al weg is. De output is met precies dezelfde hoeveelheid afgekapt, en de fout toont zich alleen bij invoer met een oneven grootte.
- Regeleinden van 76 tekens verwachten in MIME- of e-mailoutput en verward zijn wanneer Go je één lange regel geeft. De omwikkeling is een kanaalconventie, en in Go is het aan je code om die toe te passen.
- Het standaardalfabet gebruiken in URLs, en daarna een middag doorbrengen met het jagen op 404's en 400's die eigenlijk een percent-coderingsprobleem zijn. Zal de string in een URL leven, begin dan bij
URLEncodingofRawURLEncoding. - Vulling produceren waar de consument hem verbiedt: JWT-segmenten, enkele tokenformaten, enkele strenge parsers. De raw-varianten bestaan precies om deze reden, en de foutmelding van de andere kant is vaak een byte-offset op het allerlaatste deel van je string.
- Een geheim base64-coderen en het bescherming noemen. Dat is het niet. De header, het token, het "versleutelde" veld: leesbaar door iedereen in een halve seconde. Gebruik TLS, gebruik hashen waar een hash is wat het protocol wilt, en laat base64 zijn ene eerlijke werk doen.
- De 33 procent vergeten bij het instellen van limieten: bodygroottes, kolombreedtes, URL-budgets, quota-checks. De rekensom is één aanroep van
EncodedLen, en de prijs van het overslaan is een 413 of een afgekapte kolom in productie. - Tekst coderen die geen UTF-8 is, wat de kapotte dingen trouwelijk vervoert. Normaliseer verouderde tekensets met
golang.org/x/textvoordat je codeert, zodat de base64-string schone bytes draagt. - Na het sluiten nog naar de encoder schrijven, uit gewoonte of uit een retry-loop. Er wordt geen fout gegeven, en de output is stilletjes ongeldig.
- Aannemen dat de decoder aan de overkant zo vergevingsgezind is als die van Go. Go slaat regeleinden overal over, maar andere talen en parsers zijn strenger over witruimte en over regellengte, dus match de conventie van het kanaal in plaats van de stemming van de Go-runtime.
De andere kant
Dat is de coderingskant van het verhaal: één methode die niet kan falen, vier encoders afgestemd op de kanalen waar hun strings doorheen reizen, een stroomencoder met één verplichte Close, en een formaat dat je data met een derde uitbreidt en nooit, nooit zijn regels omwikzelt. Kies het alfabet van de bestemming, sluit je encoders, doe de grootte-rekensom vooraf, en base64 in Go blijft de stille, zero-dependency utility die het sinds 2009 is.
En wanneer het verkeer omkeert, wanneer je programma een van deze strings ontvangt en die moet openen, dan behandelt het gerelateerde artikel over Base64-decoderen in Go die kant in detail: de tolerantieregels van de decoder, de foutoffsets die je de byte vertellen waar de invoer het misgaat, de strikte modus voor knibbige protocollen, en dezelfde vier coderingen vanuit de andere richting.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-decodering in Go: een complete gids