Base64-decodering in Go: een complete gids
Ergens in een API-respons zit een lange string die doet alsof hij een waarde is, terwijl hij eigenlijk een bestand, een token, een afbeelding of een bericht is van een systeem dat drie jaar ouder is dan het jouwe. Irgens in je logregels, database-rijen en JSON-payloads verschijnen base64-strings constant: het standaardalfabet van 64 letters, soms met een plus en een slash, soms met een koppelteken en een underscore, en af en toe twee gelijke tekens die achteraan geparkeerd staan als een handtekening.
De startpagina van deze site legt het formaat zelf al uit: 64 afdrukbare tekens die elk 6 bits dragen, vier tekens per drie invoerbytes, en vulling om het werk af te maken. Dus deze gids gaat direct naar de helft van het werk waar de interessante beslissingen thuishoren: die strings openen in Go. Het goede nieuws is dat Go een geweldig plek is om dat te doen. Eén pakket uit de standaardbibliotheek, nul dependencies, een decoder die standaard strikt is maar toleraat voor regeleindes, en foutmeldingen die naar de exacte byte wijzen die het mis deed.
Wat Go meegeeft
Alles wat je nodig hebt zit al in de standaardbibliotheek. Het pakket heet encoding/base64, het bronbestand draagt nog altijd een copyright-opschrift uit 2009, het jaar waarin de taal werd geboren, en er is geen extensie om in te schakelen, geen module om op te halen en geen instelling om om te zetten. Als go version op je machine iets toont, bezit je al het complete gereedschap.
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. De base64-API is op beide identiek, en dankzij de compatibiliteitsbelofte van Go 1 zal een programma dat vandaag base64 decodeert dat exact hetzelfde blijven doen in elke toekomstige release. Haal Go zelf op via de officiële tarballs op go.dev/dl (zoiets als go1.27.1.linux-amd64.tar.gz, uitgepakt in /usr/local), via de pakketbeheerder van je distributie (sudo apt install golang-go op Ubuntu-gebaseerde systemen), of via de golang.org/dl-wrapper als je graag meerdere Go-versies naast elkaar wilt.
Zodra Go geïnstalleerd is, toont go doc encoding/base64 de hele API in een leesbare kolom, wat de snelste manier is om je geheugen op te frissen. De enige extra die deze gids ergens gebruikt, is golang.org/x/text voor verouderde tekensets, geïnstalleerd met go get golang.org/x/text. Die verschijnt één keer, in zijn eigen sectie, en de rest is pure standaardbibliotheek.
Je eerste decode
Negentig procent van het decoderen in Go is één methode op het Encoding-type:
func (enc *Encoding) DecodeString(s string) ([]byte, error)
Geef hem een base64-string, dan geeft hij de bytes die hij vertegenwoordigt terug, plus een fout als de invoer niet meewerkt:
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
}
Twee dingen aan die signatuur zijn de moeite waard om te onthouden. Ten eerste is het resultaat een []byte, geen string, omdat de bytes die je ontwrapen perfect geldige base64 en tegelijk perfect afschuwelijke tekst kunnen zijn: een PNG-header, een gecomprimeerd archief, een binair protocol. Pak het alleen in met string(...) als je weet dat de payload tekst is. Ten tweede geeft de methode altijd twee waarden terug. Een nil-fout betekent dat de string schone base64 was; een niet-nil fout betekent dat de invoer ergens kapot was, en de byte-slice die je ontving kan dan een gedeeltelijk resultaat zijn in plaats van een lege. Je zult beide kanten van dat gedrag tegenkomen in de sectie over fouten hieronder.
Vier decoders, één vraag: welk alfabet?
Go levert vier kant-en-klare Encoding-waarden mee, en de juiste kiezen is de eerste echte beslissing in elke decode. De tabel hieronder is de zitkaart:
| Variabele | Alfabet | Vulling | Waar je het tegenkomt |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
MIME-e-mail, data-URLs, HTTP Basic auth, PEM-bestanden, algemeen JSON |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
URL-paden en queries, bestandsnamen |
RawStdEncoding |
A-Z a-z 0-9 + / |
geen | Niet-gevulde standaard-base64 van compacte bronnen |
RawURLEncoding |
A-Z a-z 0-9 - _ |
geen | JWT-segmenten, compacte API-identifiers |
De snelste manier om te kiezen is om naar de data zelf te kijken. Een string met een + of / kan alleen maar een standaardalfabet-string zijn, dus die heeft een van de twee Std-decoders nodig. Een string met een - of _ is de URL-veilige variant uit RFC 4648, dus die heeft een van de twee URL-decoders nodig. Kijk daarna naar de staart: =-tekens achteraan betekenen de gevulde variant, en hun afwezigheid de Raw-variant. Zo voelt de verkeerde keuze aan:
decoded, err := base64.StdEncoding.DecodeString("P29_")
// err: illegal base64 data at input byte 3
// de underscore zit niet in het standaardalfabet, dus stopt de
// decoder bij het laatste teken dat hij niet herkent
decoded, err = base64.URLEncoding.DecodeString("P29_")
// decoded zijn de drie bytes 0x3f 0x6f 0x7f, err is nil
Decodeer je data van een bron die een eigen alfabet van 64 tekens heeft gedefinieerd, dan bouwt base64.NewEncoding("...64 chars...") je een decoder voor. Het alfabet moet exact 64 unieke byte-waarden bevatten en mag geen regeleinde bevatten - anders paniceert de functie - en hoewel de documentatie vereist dat het alfabet het vulteken uitsluit, afdwingt de functie dat niet: een alfabet met een '=' wordt zonder paniek geaccepteerd. In het dagelijkse werk heb je het zelden nodig, maar het is er wel, en het is de enige manier om een eigen schema te decoderen.
Het tolerantieprobleem: welke invoer accepteert Go?
Elke base64-decoder moet één ongemakkelijke beslissing maken: hoeveel rommel is hij bereid te slikken? Het antwoord van Go is een zorgvuldig getrokken lijn. Aan de vergevingsgezinde kant slaat de decoder retour- en regeleindetekens over, waar dan ook in de invoer, zodat een string die door een e-mailclient of een PEM-gereedschap over veel regels werd omgebroken zonder voorafgaande verwerking decodeert:
decoded, err := base64.StdEncoding.DecodeString("T\nW\nF\r\nu")
// decoded is "Man", err is nil
// elke \r en \n in de string werd simpelweg genegeerd
Aan de strikte kant mag alles andere niet. Een spatie, een tab, een nulbreedte-teken gekopieerd uit een PDF, een verdwaald dubbele punt uit een header: het moment dat de decoder een teken tegenkomt dat niet in het alfabet staat en geen regeleinde is, stopt hij en rapporteert de offset. En hij behoudt alles wat hij al had gedecodeerd:
decoded, err := base64.StdEncoding.DecodeString("TWFu junk")
// decoded is "Man" (het deel vóór de spatie),
// err is: illegal base64 data at input byte 4
Die combinatie verrast mensen: een mislukte decode kan je toch een bruikbaar halfresultaat aanreiken. Of dat een feature is of een gevaar, dat bepaal je zelf; het punt is dat err == nil de enige voorwaarde is waaronder de data compleet is.
Vulling heeft zijn eigen regels, en die verschillen tussen de gevulde en de raw-varianten. De gevulde decoders werken in groepen: een groep is ofwel vier echte tekens ofwel twee echte tekens gevolgd door ==. Eén teken op zichzelf is nooit een complete groep, dus "T" faalt, en "TWF" faalt ook, omdat drie tekens één vulteken nodig hebben dat ontbreekt. De raw-decoders schrappen de vullingseis, maar ze kunnen een lengte niet accepteren waarbij een groep drie van zijn vier tekens zou missen, dus "T" faalt daar eveneens, terwijl "TW" vrolijk decodeert naar één byte.
base64.StdEncoding.DecodeString("T") // fout op invoerbyte 0
base64.StdEncoding.DecodeString("TWF") // fout op invoerbyte 0
base64.RawStdEncoding.DecodeString("TW") // 1 byte, geen fout
base64.StdEncoding.DecodeString("TWFu====") // "Man" plus fout op byte 4
Er is nog één wisselstand: Strict(), toegevoegd in Go 1.8. In de strikte modus handhaaft de decoder de canonieke vorm uit RFC 4648, sectie 3.5: de ongebruikte afsluitende bits van de laatste groep moeten nul zijn. De gewone modus maakt zich er niets van, omdat die bits simpelweg nooit gebruikt worden, dus "Qm==" decodeert naar de byte B zonder murren. De strikte modus weigert het:
decoded, err := base64.StdEncoding.DecodeString("Qm==")
// decoded is "B", err is nil (de afsluitende bits werden weggelaten)
decoded, err = base64.StdEncoding.Strict().DecodeString("Qm==")
// err is: illegal base64 data at input byte 2
Let erop dat zelfs in de strikte modus regeleinden nog steeds worden overgeslagen, zoals de documentatie opmerkt. Gebruik Strict() als je een protocol spreekt dat om canonieke codering geeft, of als je slordige bronnen liever weigert dan hun bits stilletjes te absorberen.
Fouten die je zeggen waar
Elke mislukking in dit pakket arriveert als een concrete, inspecteerbare waarde. Als de invoer iets bevat wat het alfabet niet kent, of de vulling verkeerd is, geeft de decoder een base64.CorruptInputError terug, en de melding bevat de byte-offset van het probleem:
type CorruptInputError int64
func (e CorruptInputError) Error() string {
return "illegal base64 data at input byte " + strconv.FormatInt(int64(e), 10)
}
Die offset is het verschil tussen "er ging iets mis" en "het 4.102e teken van deze string van 900 kilobyte is een tab die van het klembord is verdwaald". Pak hem met het gebruikelijke Go-idiom:
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 is het symptoomenoverzicht voor de invoer die mensen het meest in de war brengt:
| Invoer (StdEncoding) | Resultaat | Waarom |
|---|---|---|
TWF$ |
fout op byte 3 | $ staat niet in het alfabet |
T |
fout op byte 0 | één teken is nooit een complete groep |
TWF |
fout op byte 0 | drie tekens hebben één = nodig dat ontbreekt |
TWFu junk |
Man plus fout op byte 4 |
een spatie is geen regeleinde, dus stopt het decoderen daar |
TWFu\t |
Man plus fout op byte 4 |
tabs worden niet overgeslagen, alleen \r en \n |
T\nW\nF\nu |
Man, geen fout |
regeleinden worden overal genegeerd |
==== |
fout op byte 0 | vulling aan het begin van een groep is niet geldig |
(lege string) |
leeg resultaat, geen fout | nul bytes base64 decoderen naar nul bytes |
Eén praktische tip: als een decode in productie faalt, log de offset en een kort venster eromheen. In negentig procent van de gevallen is de "corrupte" byte witruimte die het transport, het klembord of een PDF-lezer de string in sloop, en is de oplossing een trim of een strip, geen herontwerp.
Bestanden openen
Base64-bestanden zijn gewoon tekstbestanden die base64 bevatten, dus het gangbare bestandsgereedschap van Go is van toepassing. Voor een bestand dat comfortabel in het geheugen past, lees je het volledig in en decodeer je de 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")
}
Voor grote bestanden is de betere aanpak streaming, en die gebruikt de andere helft van de pakket-API: NewDecoder wrapt elke io.Reader in een base64-decoderende reader, zodat je van bestand naar bestand kunt pipen zonder de hele payload ooit in het geheugen te houden:
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")
Er is een middenoptie als je de extra allocatie wilt vermijden die DecodeString maakt: Decode schrijft naar een bestemmingsbuffer die jij beheert. Bepaal de grootte met DecodedLen, die het maximale aantal uitvoerbytes retourneert voor een gegeven invoerlengte:
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] // de daadwerkelijk gedecodeerde grootte
fmt.Println(len(data), "bytes")
Wees met die laatste wel voorzichtig: Decode vertrouwt erop dat jij de buffer de juiste grootte geeft. Is hij te klein, dan geeft de methode geen fout terug; hij paniceert met index out of range. DecodedLen is het getal dat je moet gebruiken, niet len(raw).
Een base64-commandoregel voor Go
Unix-systemen leveren een base64-utility mee met coreutils, en Go levert geen equivalente binary mee. Het idiomatische antwoord in de Go-wereld is niet een pakket dat je installeert, maar een programma dat je zelf bezit: een klein commandoregeltool gebouwd rond encoding/base64, het flag-pakket en standaardinvoer. Hier is een complete, van zo'n veertig regels, die decodeert wat er in gepipt wordt en de ruwe bytes naar buiten schrijft:
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)
}
Bouw hem één keer met go build -o b64 . en hij wordt een cross-platform decoder die je in een Makefile, een CI-pipeline of een shellfunctie kunt plaatsen: printf 'TWFu' | ./b64 toont Man, en ./b64 -url < token.b64 > token.bin ontwrapt een URL-veilig token naar een bestand. Twee eigenschappen van het ontwerp verdienen de moeite om te noemen. Omdat het eerst al de stdin leest voordat het decodeert, decodeert omgebroken invoer met regeleinden zonder problemen, dankzij de tolerantie voor regeleinden van de decoder. En omdat het bij slechte invoer afsluit met status 1 en de klacht naar stderr schrijft, gedraagt het zich als een tool in een pipeline in plaats van een script dat zich verontschuldigt. Dat is de hele kunst van een Go-CLI: één pakket, één vlag, standaardinvoer, standaarduitvoer en een exit code.
URL-veilig decoderen
De URL-veilige variant bestaat omdat het standaardalfabet bots met de grammatica van URLs: een + wordt in query-strings vaak als spatie gelezen, en een / begint een nieuw padsegment, dus een standaard-base64-string die in een URL zit, moet teken voor teken percent-escaped worden, wat langzaam om te parsen en lelijk om te lezen is. Het alternatieve alfabet van RFC 4648 vervangt + en / door - en _, beide wettig ongeëscaped in URL-paden, queries en bestandsnamen.
In Go is de wissel gewoon een andere decoder-variabele. Is je data URL-veilig en gevuld, gebruik dan URLEncoding; is ze URL-veilig en niet gevuld, gebruik dan RawURLEncoding. Het klassieke geval is een identifier die in een URL of een bestandsnaam leeft:
decoded, err := base64.RawURLEncoding.DecodeString("-w9n")
// decoded zijn de drie bytes 0xfb 0x0f 0x67
// het koppelteken en de underscore zijn onderdeel van het URL-veilige alfabet,
// dus RawURLEncoding verwerkt ze waar StdEncoding zou falen
Waar je het in echte Go-code tegenkomt: JWT-segmenten (in de volgende sectie behandeld), ondoorzichtige identifiers die systemen genereren en in URLs opslaan, bestandsnamen die geen webserver of cloud-objectopslag mogen kapotmaken, en elke API die in zijn documentatie "base64url" belooft. Eén waarschuwing: URL-veilig is een contract tussen bron en consument, geen eigenschap van de data. Bevat de string een + of een /, dan is hij niet URL-veilig, punt, en helpt geen enkel opnieuw proberen met de URL-decoder. Kijk eerst naar de tekens, kies daarna de decoder.
Kijken binnenin JWT's
Een JSON Web Token is drie base64url-segmenten gescheiden door punten: een header, een payload met claims en een signature, zonder vulling op enig segment. Dat maakt een JWT een van de meest voorkomende dingen die je in Go zult decoderen, en de header en payload zijn leesbaar zonder enige sleutel, wat de moeite waard is om te onthouden voor zowel debuggen als beveiligingsreviews:
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"}
}
Let op de keus van decoder: RawURLEncoding, niet StdEncoding. JWT-segmenten gebruiken het URL-veilige alfabet en dragen geen vulling, en een segment waarvan de lengte een of twee tekens kort is van een veelvoud van vier, faalt bij een gevulde decoder op het allerlaatste moment, wat een verwarrende fout is om achterna te jagen. Het signature-segment kun je niet lezen zonder de sleutel, en je zou niets alleen op basis van de payload moeten proberen te vertrouwen, omdat niets een client ervan weerhoudt de eerste twee segmenten te vervalsen. Heb je verificatie nodig, gebruik dan een onderhouden bibliotheek. De de facto is github.com/golang-jwt/jwt/v5 (installeer hem met 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"])
}
Twee details van de bibliotheek zijn de moeite waard om te weten. Ten eerste wordt het base64url-coderen en -decoderen van de drie segmenten intern afgehandeld, dus raak je encoding/base64 nooit direct aan wanneer je signeert of verifieert. Ten tweede wijst de v5-bibliotheek tokens met alg=none af, tenzij je expliciet zijn UnsafeAllowNoneSignatureType-constant doorgeeft, wat je beschermt tegen de klassieke "ongetekende token geaccepteerd"-fout.
Data-URLs
Een data-URL is een URL waarvan de payload de data zelf is. De syntaxis, uit RFC 2397, is data:[mediatype][;base64],data: een optionele mediatype, een optionele ;base64-vlag, een komma en daarna de inhoud. Is de ;base64-vlag aanwezig, dan is de inhoud standaard-base64, en daarom delen data-URLs en deze gids een sectie. Browsers gebruiken ze om afbeeldingen en lettertypen direct in HTML en CSS te embedden, zodat de pagina één request minder nodig heeft:
<img src="data:image/png;base64,iVBORw0KGgo=" alt="pixel">
De standaardbibliotheek van Go heeft geen data-URL-helper, maar het formaat is eenvoudig genoeg om met strings met de hand te parsen, wat de meeste Go-programma's doen:
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")
}
Drie valkuilen om in gedachten te houden. Ten eerste is de ;base64-vlag optioneel, en zonder die vlag is de payload percent-geëscapeerde ASCII in plaats van base64, dus controleer de suffix voordat je een decoder aanroept. Ten tweede geldt: wordt de mediatype weggelaten, dan is de standaard text/plain;charset=US-ASCII, wat zelden uitmaakt voor afbeeldingen maar mensen verrast die andere inhoud parsen. Ten derde zijn data-URLs een truc voor kleine payloads: de RFC zelf zegt dat het schema alleen nuttig is voor korte waarden, en de 33 procent grotere omvang van base64 maakt van een logo van 500 kilobyte een string van 666 kilobyte die in je HTML is geplakt, niet cachebaar en niet deelbaar. Gebruik ze voor iconen en miniaturen, niet voor video's.
HTTP- en API-werk
De met afstand meest voorkomende decode in Go-webservices is het JSON-bodyveld: een uploadformulier, een API-respons of een webhook geeft je een string die eigenlijk een bestand is. Unmarshal naar een struct, en decodeer daarna het veld:
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")
}
Accepteert je API zowel standaard- als URL-veilige strings, dan is het pragmatische patroon om één decoder te proberen en, als hij faalt met een CorruptInputError vlak bij het einde, de andere te proberen voordat je opgeeft. Doe deze dans niet meer dan één keer, en ga nooit teruggrijpen naar "de gelijke tekens eraf halen en dan maar hopen" als algemene strategie.
Voor HTTP Basic-authenticatie decodeer je helemaal niets, omdat Go het voor je doet. Request.BasicAuth, beschikbaar sinds Go 1.4, splitst de Authorization-header voor je en geeft de gebruikersnaam en het wachtwoord terug, nadat hij de standaard-base64-decoder al heeft laten draaien op het user:pass-paar dat RFC 2617 definieert:
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)
}
Onthoud dat Basic auth authenticatie is, geen bescherming: de header is base64, niet versleuteld, dus hij moet alleen via HTTPS reizen. Ben je de client, dan is de spiegeloproep req.SetBasicAuth(user, pass), die dezelfde header voor je bouwt met de standaard-encoder.
Eén verdedigende gewoonte voor API-handlers: beperk het body voordat je het decodeert, met http.MaxBytesReader of een gelijkwaardige lengtecheck. Een base64-string decodeert naar ongeveer driekwart van zijn eigen lengte, dus een bodylimiet van N bytes houdt het gedecodeerde resultaat onder N bytes, en het geheugen blijft begrensd, wat een kwaadwillende client ook post. Het decoderen van een onbeperkt body is een klassieke vector voor geheugenuitputting, omdat de aanvaller bepaalt hoeveel megabytes tekst hij in binair kan omzetten.
Verouderde tekensets
Base64 decoderen geeft je bytes, en in moderne systemen zijn die bytes vrijwel altijd UTF-8, in welk geval string(decoded) het hele verhaal is. Maar base64 is een oud formaat, en veel van wat er rondgaat, werd geproduceerd door systemen die Windows-1252, ISO-8859-1, Shift JIS of een andere verouderde single-byte of double-byte tekenset gebruikten. Heeft de bron dat gedaan, dan zijn de bytes die je decodeert geen geldige UTF-8, en doet Go er niet alsof ze dat wel zijn: hij toont je vervangingstekens op elke plek waar een sequentie kapot is.
Het antwoord van Go is de golang.org/x/text-module, die bytes met een verouderde codering naar UTF-8 omzet (en terug) voor de gangbare tekensets. De omzettingsplek is direct na de decode, en het kost één functieaanroep:
package main
import (
"encoding/base64"
"fmt"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
func main() {
// "Café" opgeslagen als Windows-1252 door een verouderd gereedschap,
// daarna base64-gecodeerd voor transport
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é
}
De module heeft één subpakket per tekensetfamilie: charmap voor de Windows- en ISO-single-bytetabellen, japanese voor Shift JIS en EUC-JP, korean voor EUC-KR, simplifiedchinese voor GB18030 en traditionalchinese voor Big5. De vuistregel is om alleen te converteren als je echt de tekenset van de bron kent, omdat UTF-8 bytes een tweede keer converteren niet met lawaai faalt; het verstoort de tekst gewoon. Twijfel je, dan behandel je de payload als bytes en laat je de downstream-consument beslissen.
Streaming en chunk-gebaseerde decodes
Je zag NewDecoder in de bestanden-sectie; hier is wat hem een sectie op zich waard maakt. Het is een échte streaming-adapter: hij trekt uit de onderliggende reader alleen zo veel als nodig, decodeert op zijn plaats, en geeft een CorruptInputError terug het moment dat de stroom slecht wordt. De hele stroom kan terabytes aan data zijn; het geheugen dat je belegt, is je buffer en de uitvoer die je schrijft. De twee gangbare consumentenpatronen zijn io.ReadAll voor kleine stromen en io.Copy voor alles andere:
small, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, r))
// prima voor een config-blob of een kleine bijlage
w, err := io.Copy(out, base64.NewDecoder(base64.StdEncoding, r))
// prima voor een video, een tarball of een restore-job
Sinds Go 1.22 heeft het pakket ook AppendDecode, die decodeert naar een buffer die je hergebruikt in plaats van per aanroep een verse slice te alloceren. Het is het gereedschap voor hete paden die in een loop veel chunks decoderen, zoals een regelprocessor of een protocoldecoder:
var buf []byte
for _, chunk := range chunks {
buf, err = base64.StdEncoding.AppendDecode(buf, chunk)
if err != nil {
return err
}
process(buf)
}
De methode voegt de gedecodeerde chunk toe aan wat buf al bevat en geeft de uitgebreide slice terug, waarbij de onderliggende array groeit zoals nodig. In de stabiele staat, waar de buffer al is gegroeid tot de juiste grootte, voert hij nul allocaties uit per chunk, wat helder naar voren komt in een benchmark. Is je werkload "één keer decoderen, zelden", dan is DecodeString de eenvoudigere keuze; is het "duizenden keren decoderen in een strakke loop", dan is AppendDecode degene waar je naar moet grijpen.
Houd het veilig
Enkele beveiligingsnotities die specifiek zijn voor hoe Go-programma's dit pakket daadwerkelijk gebruiken. Ten eerste is base64 codering, geen versleuteling. Een base64-string is leesbaar voor iedereen met de developer tools van een webbrowser, dus "we base64 het wachtwoord voordat we het versturen" is geen beveiligingsmaatregel; het is een transportgemak. De vertrouwelijkheid moet komen van TLS, niet van het alfabet.
Ten tweede, beperk je invoer. De gedecodeerde grootte van een base64-string is hooguit DecodedLen van zijn lengte, dus check dat getal tegen een limiet voordat je alloceert, en wikkel request-bodies in met een groottecap voordat iets een decoder raakt. Beide checks kosten één regel, en samen zetten ze een onbeperkte decode om in een beperkte.
Ten derde, bepaal je houding ten opzichte van slordige invoer. De gewone modus gooit de ongebruikte afsluitende bits van de laatste groep stilletjes weg, wat betekent dat twee verschillende strings naar dezelfde bytes kunnen decoderen. Voor de meeste data maakt dat niet uit. Voor alles wat deel uitmaakt van een protocol, een gesigneerd bericht of een waarde die wordt vergeleken of opgeslagen, is Strict() de conservatieve keuze, omdat het de canonieke vorm de enige geaccepteerde vorm maakt.
Ten vierde, let op waar gedecodeerde bytes eindigen. Wordt een gedecodeerde waarde een bestandsnaam, een pad, een SQL-fragment of een commandoargument, dan heeft de base64-laag je niet tegen iets beschermd: de bytes zijn nu de niet-te-betrouwen invoer van je programma, en de gebruikelijke sanitatieregels gelden precies zoals voor elke andere gebruikersdata.
Hoe snel is de decoder?
Base64 in Go is snel, en blijft dat ook op grote data, omdat de implementatie een eenvoudige loop over een opzoektabel is, zonder reflectie en zonder allocatie per teken. Op een recente desktop-CPU met Go 1.26 decodeert een string van 500 bytes in ongeveer een kwart microseconde met één allocatie, wat neerkomt op de orde van twee gigabytes per seconde. Een megabyte base64 decodeert in ruim minder dan een milliseconde; een gigabyte in ruim minder dan een seconde. De getallen bewegen mee met de hardware, maar de vorm niet: base64-decode is vrijwel nooit de bottleneck; meestal is dat het netwerk of het schijfstation eromheen.
Zit je in een hete loop, dan is het allocatieprofiel wat je moet bekijken. DecodeString alloceert de resulteerslice bij elke aanroep. Decode met een vooraf op maat gemaakte bestemming en AppendDecode met een hergebruikte buffer vermijden die allocatie in de stabiele staat helemaal. Voor een decode die een paar keer per request plaatsvindt, maakt het niets uit; voor een decode die een paar miljoen keer per seconde plaatsvindt, is het het verschil tussen een vlak geheugenprofiel en een rammelende garbage collector.
Een korte geschiedenis van het pakket
Het base64-pakket is een van de oudste delen van de Go-standaardbibliotheek. Het copyright-opschrift van het bronbestand zegt 2009, het jaar waarin de taal werd gemaakt, en het pakket maakt sinds de allererste stabiele release, Go 1.0 in maart 2012, deel uit van de standaardbibliotheek. Dat betekent dat de DecodeString die je vandaag aanroept dezelfde API is, met hetzelfde gedrag, die Go-programma's al meer dan een decennium lang aanroepen.
De groei sindsdien is bescheiden en nuttig geweest. Go 1.5 in augustus 2015 voegde de niet-gevulde waarden RawStdEncoding en RawURLEncoding toe, waarmee de deur openging voor compacte strings in JWT-stijl. Go 1.8 in februari 2017 voegde Strict() toe, waardoor protocollen een manier kregen om canonieke invoer te eisen. Go 1.22 in februari 2024 voegde AppendDecode en AppendEncode toe aan het hele gezin van base-coderingen, en maakte WithPadding strenger om nonsens-argumenten af te wijzen. En per september 2026, met Go 1.27.1 als nieuwste release en Go 1.26 als andere ondersteunde lijn, is de API precies die in deze gids wordt beschreven: vier kant-en-klare coderingen, een stroomdecoder, een strikte modus en een append-familie voor prestaties.
Het diepere feit is de compatibiliteitsbelofte. De garantie van Go 1 betekent dat het pakket voor altijd dezelfde invoer zal blijven accepteren en afwijzen, dus een decoder die je dit jaar schrijft voor een data-formaat dat in 2015 is geproduceerd, zal blijven werken. Voor een formaat dat zo oud en zo saai is, is dat het beste nieuws dat er is.
Dingen die je zullen verrassen
Na een tijdje in Go word je niet meer verrast door base64, maar de eerste keren slaan een paar van deze feiten hard in, dus hier zijn ze:
- De decoder slaat
\ren\nover waar dan ook in de invoer, maar geen spatie, geen tab en geen nulbreedte-spatie. De tolerantie is opzettelijk; hij bestaat om omgebroken MIME-invoer te laten werken, en stopt precies waar de spec stopt. - Een mislukte decode kan toch échte data teruggeven. Het gedeeltelijke resultaat is alles wat gedecodeerd is vóór de slechte byte, en de fout arriveert ernaast, niet in plaats daarvan.
CorruptInputErroris letterlijk gewoon eenint64met een methode erbij. De "fout" is de offset, en de melding wordt op verzoek samengesteld.DecodeenEncodevertrouwen allebei erop dat jij de bestemmingsbuffers de juiste grootte geeft. Geef ze een buffer die te klein is en je krijgt geen fout; je krijgt een panic.- Eén enkel teken is geen geldige invoer voor enig van de vier ingebouwde coderingen. Eén base64-teken draagt zes bits, en een byte heeft er acht nodig, dus er zit in één teken geen complete groep, gevuld of niet.
- Per augustus 2026 tonen meer dan 244.000 openbare pakketten op pkg.go.dev
encoding/base64onder hun imports. Het is stilletjes een van de pakketten waarop het meest wordt gerekend in het hele ecosysteem.
Waar decodes mislopen
Dit zijn de decoderingsfouten die steeds opnieuw opduiken in Go-codebases, ongeveer in de volgorde waarin ze in ondersteuningsthreads verschijnen:
StdEncodingkiezen voor URL-veilige data (of omgekeerd). Het symptoom is een fout op de eerste-,_,+of/, en de oplossing is om naar de string te kijken voordat je de decoder kiest.- Een string plakken uit een terminal, een e-mail of een PDF, waardoor er spaties, tabs of artefacten van regeleinden insluipen. Go slaat echte regeleinden over, maar een spatie in het midden van de string is een corrupte byte, en de offset in de fout wijst er recht op.
- Vergeten dat het resultaat een
[]byteis. Het ruw afdrukken geeft je een lijst met getallen, en het voeden aan een functie die een string verwacht heeft eenstring(...)-conversie nodig. - De fout checken en de gedeeltelijke data er toch nog mee gebruiken. De half-gedecodeerde prefix is écht, maar het is niet de payload, en code die het zo behandelt faalt in productie met data die precies de helft van de juiste lengte is.
- De
Decode-buffer dimensioneren metlen(src)in plaats vanDecodedLen(len(src)). De eerste grootte is verkeerd in de andere richting dan je hoopt, en de panic die het veroorzaakt komt alleen voor bij grote invoer, wat het een favoriet maakt van stagingomgevingen. - Aannemen dat JWT-segmenten vulling dragen. Dat doen ze niet, en een gevulde decoder faalt op het laatste teken met een fout die als een raadsel leest. Gebruik
RawURLEncoding. - Geloven dat alle witruimte wordt overgeslagen. Dat is niet zo. Alleen de twee regeleindetekens worden overgeslagen, en "witruimte" op het klembord is een veel groter gezin dan dat.
- Tweemaal decoderen, of niet twee keer decoderen, terwijl de waarde base64 van base64 is (een bestand dat aan een e-mail was bijgevoegd die zelf weer was bijgevoegd). De check is één rondrit: decodeer één keer, bekijk of het resultaat er nog naar base64 uitziet, en decodeer pas daarna opnieuw.
De andere helft van het werk
Dat is de volledige decoderingskant van het verhaal: één pakket, vier kant-en-klare decoders, een stroomdecoder voor grote data, een strikte modus voor knibbige protocollen en foutmeldingen die je de byte vertellen waar het misging. Leer de tolerantieregels, kies je decoder door naar de tekens te kijken, beperk je invoer, en base64 in Go wordt de saaie, voorspelbare, zero-dependency utility waartoe het ontworpen was.
Wanneer het werk omslaat, en je Go-programma base64-strings moet produceren in plaats van ze te openen, dan behandelt het gerelateerde artikel over Base64-coderen in Go die kant in detail: de één-methode-API van de encoder, de Close-aanroep die je laatste twee bytes stilletjes opslurpt, regelomwikkeling voor MIME en hoe de vier coderingen corresponderen met de kanalen waar ze doorheen reizen.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-codering in Go: een complete gids