Decodifica Base64 in Go: una guida completa
C'è una lunga stringa nascosta in una risposta API, e fa finta di essere un valore quando in realtà è un file, un token, un'immagine o un messaggio di un sistema tre anni più vecchio del tuo. Qualche riga di log, qualche tabella del database, qualche payload JSON: le stringhe base64 compaiono in continuazione: l'alfabeto standard da 64 lettere, a volte con un più e una barra, a volte con un trattino e un underscore, e ogni tanto due segni uguale parcheggiati alla fine come una firma.
La home page di questo sito spiega già il formato in sé: 64 caratteri stampabili da 6 bit ciascuno, quattro caratteri per ogni tre byte di input, e il padding per chiudere il lavoro. Questo articolo va quindi dritto alla metà del lavoro dove vivono le decisioni interessanti: aprire quelle stringhe in Go. La buona notizia è che Go è un posto meraviglioso per farlo. Un pacchetto della libreria standard, zero dipendenze, un decoder rigoroso di default ma permissivo con gli a capo, e messaggi di errore che puntano al byte esatto andato storto.
Cosa arriva con Go
Tutto ciò che ti serve è già nella libreria standard. Il pacchetto si chiama encoding/base64, il suo file sorgente indossa ancora l'intestazione di copyright 2009, l'anno di nascita del linguaggio, e non c'è nessuna estensione da attivare, nessun modulo da scaricare, nessuna impostazione da cambiare. Se go version stampa qualcosa sul tuo computer, possiedi già lo strumento intero.
Alla data di questo articolo la release più recente è Go 1.27.1, uscita il 1 settembre 2026, con la linea Go 1.26 (attualmente 1.26.8) come altra linea supportata. L'API base64 è identica su entrambe, e grazie alla promessa di compatibilità di Go 1, un programma che decodifica base64 oggi continuerà a fare esattamente la stessa cosa su ogni release futura. Go si ottiene dai tarball ufficiali su go.dev/dl (qualcosa come go1.27.1.linux-amd64.tar.gz, da estrarre in /usr/local), dal gestore di pacchetti della tua distribuzione (sudo apt install golang-go sui sistemi basati su Ubuntu), o tramite il wrapper golang.org/dl se ti piacciono diverse versioni di Go fianco a fianco.
Una volta installato Go, go doc encoding/base64 stampa l'API intera in una colonna leggibile, il modo più rapido per rinfrescarsi la memoria. L'unico extra che questo articolo usa da qualche parte è golang.org/x/text, per i set di caratteri legacy, da installare con go get golang.org/x/text. Compare una volta, nella sua sezione, e il resto è libreria standard pura.
La tua prima decodifica
Il novanta percento della vita della decodifica in Go è un metodo del tipo Encoding:
func (enc *Encoding) DecodeString(s string) ([]byte, error)
Dagli una stringa base64 e ti restituisce i byte che rappresenta, più un errore quando l'input si comporta male:
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
}
Due cose di quella firma meritano di essere memorizzate. La prima: il risultato è un []byte, non una stringa, perché i byte che apri possono essere base64 perfettamente valido e testo perfettamente terribile: l'header di un PNG, un archivio compresso, un protocollo binario. Avvolgilo in string(...) solo quando sai che il payload è testo. La seconda: il metodo restituisce sempre due valori. Un errore nil significa che la stringa era base64 pulito; un errore non-nil significa che l'input era rotto da qualche parte, e la slice di byte che hai ricevuto può essere un risultato parziale invece di uno vuoto. Vedrai entrambi i lati di questo comportamento nella sezione sugli errori qui sotto.
Quattro decoder, una domanda: quale alfabeto?
Go spedisce quattro valori Encoding già pronti, e scegliere quello giusto è la prima vera decisione in ogni decodifica. La tabella qui sotto è la mappa dei posti:
| Variabile | Alfabeto | Padding | Dove lo incontrerai |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
Email MIME, data URL, autenticazione HTTP Basic, file PEM, JSON in generale |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
Percorsi e query degli URL, nomi di file |
RawStdEncoding |
A-Z a-z 0-9 + / |
nessuno | Base64 standard senza padding da produttori compatti |
RawURLEncoding |
A-Z a-z 0-9 - _ |
nessuno | Segmenti JWT, identificatori API compatti |
Il modo più rapido per scegliere è guardare i dati stessi. Una stringa che contiene + o / può essere solo una stringa a alfabeto standard, quindi le serve uno dei due decoder Std. Una stringa che contiene - o _ è la variante URL-safe della RFC 4648, quindi le serve uno dei due decoder URL. Poi controlla la coda: i caratteri = finali indicano la variante con padding, e la loro assenza indica quella Raw. Ecco come si sente la scelta sbagliata:
decoded, err := base64.StdEncoding.DecodeString("P29_")
// err: illegal base64 data at input byte 3
// l'underscore non è nell'alfabeto standard, quindi il
// decoder si ferma all'ultimo carattere che non riconosce
decoded, err = base64.URLEncoding.DecodeString("P29_")
// decoded sono i tre byte 0x3f 0x6f 0x7f, err è nil
Se stai decodificando dati da un produttore che ha definito il proprio alfabeto di 64 caratteri, base64.NewEncoding("...64 chars...") ti costruisce un decoder per esso. L'alfabeto deve essere di esattamente 64 byte univoci e non deve contenere un a capo - altrimenti la funzione va in panico - e mentre la documentazione richiede che l'alfabeto escluda il carattere di padding, la funzione non impone questo - un alfabeto contenente '=' viene accettato senza panici. Nel lavoro di tutti i giorni raramente ti servirà, ma c'è, ed è l'unico modo per decodificare un formato privato.
Il problema della tolleranza: che input accetta Go?
Ogni decoder base64 deve prendere una decisione scomoda: quanta spazzatura è disposto ad ingoiare? La risposta di Go è una linea tracciata con cura. Dal lato permissivo, il decoder salta i carriage return e i line feed ovunque nell'input, quindi una stringa spezzata su molte righe da un client di posta o da uno strumento PEM si decodifica senza alcuna pre-elaborazione:
decoded, err := base64.StdEncoding.DecodeString("T\nW\nF\r\nu")
// decoded è "Man", err è nil
// ogni \r e \n nella stringa è stato semplicemente ignorato
Dal lato rigoroso, tutto il resto è vietato. Uno spazio, una tabulazione, un carattere a larghezza zero copiato da un PDF, un due punti smarrito da un header: nel momento in cui il decoder incontra un carattere che non è nell'alfabeto e non è un a capo, si ferma e riporta l'offset. E conserva tutto ciò che ha già decodificato:
decoded, err := base64.StdEncoding.DecodeString("TWFu junk")
// decoded è "Man" (la parte prima dello spazio),
// err è: illegal base64 data at input byte 4
Quella combinazione sorprende: una decodifica fallita può comunque consegnarti un mezzo risultato utilizzabile. Che sia una funzione o un pericolo dipende da te; il punto è che err == nil è l'unica condizione in cui i dati sono completi.
Il padding ha regole proprie, e differiscono tra le varianti con padding e quelle raw. I decoder con padding lavorano a gruppi: un gruppo è o quattro caratteri veri o due caratteri veri seguiti da ==. Un carattere da solo non è mai un gruppo completo, quindi T fallisce, e anche TWF fallisce, perché tre caratteri hanno bisogno di un segno di padding che manca. I decoder raw abbandonano il requisito del padding, ma non possono comunque accettare una lunghezza in cui a un gruppo manchino tre dei suoi quattro caratteri, quindi anche lì T fallisce, mentre TW si decodifica felicemente in un singolo byte.
base64.StdEncoding.DecodeString("T") // errore al byte 0 di input
base64.StdEncoding.DecodeString("TWF") // errore al byte 0 di input
base64.RawStdEncoding.DecodeString("TW") // 1 byte, nessun errore
base64.StdEncoding.DecodeString("TWFu====") // "Man" più errore al byte 4
C'è un altro cambio di umore: Strict(), aggiunto nel Go 1.8. In modalità rigorosa il decoder applica la forma canonica della sezione 3.5 della RFC 4648: i bit finali non usati dell'ultimo gruppo devono essere zero. La modalità normale non se ne cura, perché quei bit semplicemente non vengono mai usati, quindi Qm== si decodifica nel byte B senza lamentazioni. La modalità rigorosa invece la rifiuta:
decoded, err := base64.StdEncoding.DecodeString("Qm==")
// decoded è "B", err è nil (i bit finali sono stati scartati)
decoded, err = base64.StdEncoding.Strict().DecodeString("Qm==")
// err è: illegal base64 data at input byte 2
Nota che anche in modalità rigorosa gli a capo vengono comunque saltati, come sottolinea la documentazione. Usa Strict() quando parli un protocollo che tiene alla codifica canonica, o quando vuoi rifiutare i produttori disordinati invece di assorbire in silenzio i loro bit.
Errori che ti dicono dove
Ogni fallimento in questo pacchetto arriva come un valore concreto e ispezionabile. Quando l'input contiene qualcosa che l'alfabeto non conosce, o il padding è sbagliato, il decoder restituisce un base64.CorruptInputError, e il suo messaggio include l'offset del byte del problema:
type CorruptInputError int64
func (e CorruptInputError) Error() string {
return "illegal base64 data at input byte " + strconv.FormatInt(int64(e), 10)
}
Quell'offset è la differenza tra "qualcosa è fallito" e "il 4.102° carattere di questa stringa da 900 kilobyte è una tabulazione arrivata dalla clipboard". Catturalo con l'idioma Go abituale:
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)
}
Ecco la mappa dei sintomi per gli input che confondono di più:
| Input (StdEncoding) | Risultato | Perché |
|---|---|---|
TWF$ |
errore al byte 3 | $ non è nell'alfabeto |
T |
errore al byte 0 | un carattere da solo non è mai un gruppo completo |
TWF |
errore al byte 0 | tre caratteri hanno bisogno di un = che manca |
TWFu junk |
Man più errore al byte 4 |
lo spazio non è un a capo, quindi la decodifica si ferma lì |
TWFu\t |
Man più errore al byte 4 |
le tabulazioni non vengono saltate, solo \r e \n |
T\nW\nF\nu |
Man, nessun errore |
gli a capo vengono ignorati ovunque |
==== |
errore al byte 0 | il padding all'inizio di un gruppo non è valido |
(stringa vuota) |
risultato vuoto, nessun errore | zero byte di base64 si decodificano in zero byte |
Un consiglio pratico: quando una decodifica fallisce in produzione, registra nei log l'offset e una piccola finestra intorno a esso. Nel novanta percento dei casi il byte "corrotto" è uno spazio invisibile infilato nella stringa dal trasporto, dalla clipboard o da un visualizzatore PDF, e la correzione è un trim o una pulizia, non un ridisegno.
Aprire i file
I file base64 sono semplici file di testo che contengono base64, quindi gli strumenti file abituali di Go si applicano. Per un file che sta comodamente in memoria, leggilo tutto e decodifica la stringa:
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")
}
Per i file grandi il pattern migliore è lo streaming, e usa l'altra metà dell'API del pacchetto: NewDecoder avvolge qualsiasi io.Reader in un reader che decodifica base64, così puoi fare da pipe da file a file senza tenere mai in memoria l'intero payload:
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")
C'è un'opzione intermedia quando vuoi evitare l'allocazione extra che fa DecodeString: Decode scrive in un buffer di destinazione che controlli tu. Dimensionalo con DecodedLen, che restituisce il numero massimo di byte in uscita per una data lunghezza dell'input:
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] // la dimensione decodificata effettiva
fmt.Println(len(data), "bytes")
Attenzione però a quest'ultimo: Decode si fida che tu dimensioni il buffer. Se è troppo piccolo, il metodo non restituisce un errore; va in panico con un indice fuori range. DecodedLen è il numero da usare, non len(raw).
Una riga di comando base64 per Go
I sistemi Unix spediscono una utility base64 con coreutils, e Go non spedisce un binario equivalente. La risposta idiomatica nel mondo Go non è un pacchetto da installare ma un programma che è tuo: un piccolo strumento a riga di comando costruito attorno a encoding/base64, al pacchetto flag e all'input standard. Ecco uno completo, una quarantina di righe, che decodifica tutto ciò che gli arriva via pipe e scrive i byte grezzi fuori:
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)
}
Compilalo una volta con go build -o b64 . e diventa un decoder multi-piattaforma che puoi inserire in un Makefile, in una pipeline CI o in una funzione shell: printf 'TWFu' | ./b64 stampa Man, e ./b64 -url < token.b64 > token.bin apre un token URL-safe in un file. Due proprietà del design meritano attenzione. Poiché legge tutto lo stdin prima di decodificare, un input avvolto con a capo si decodifica senza problemi, grazie alla tolleranza del decoder per gli a capo. E poiché esce con stato 1 su input sbagliati e scrive il reclamo su stderr, si comporta come uno strumento in una pipeline e non come uno script che si scusa. Questa è tutta l'arte di un CLI Go: un pacchetto, una flag, input standard, output standard, e un codice di uscita.
Decodifica URL-safe
La variante URL-safe esiste perché l'alfabeto standard collide con la grammatica degli URL: + viene spesso letto come spazio nelle stringhe di query, e / inizia un nuovo segmento di percorso, quindi una stringa base64 standard incorporata in un URL deve essere percent-escapata carattere per carattere, il che è lento da analizzare e brutto da leggere. L'alfabeto alternativo della RFC 4648 scambia + e / con - e _, entrambi legali senza escape nei percorsi, nelle query e nei nomi di file.
In Go il cambio è solo una variabile di decoder diversa. Se i tuoi dati sono URL-safe e con padding, usa URLEncoding; se sono URL-safe e senza padding, usa RawURLEncoding. Il caso classico è un identificatore che vive in un URL o in un nome di file:
decoded, err := base64.RawURLEncoding.DecodeString("-w9n")
// decoded sono i tre byte 0xfb 0x0f 0x67
// il trattino e l'underscore fanno parte dell'alfabeto URL-safe,
// quindi RawURLEncoding li gestisce dove StdEncoding fallirebbe
Dove la incontrerai in codice Go reale: segmenti JWT (coperti nel prossimo capitolo), identificatori opachi che i sistemi generano e immagazzinano in URL, nomi di file che non devono rompere un web server o un object store cloud, e qualsiasi API che ha promesso "base64url" nella documentazione. Un avvertimento: URL-safe è un contratto tra produttore e consumatore, non una proprietà dei dati. Se la stringa contiene un + o un /, non è URL-safe, punto e basta, e nessuna quantità di tentativi col decoder URL servirà. Guarda prima i caratteri, poi scegli il decoder.
Sbirciare dentro i JWT
Un JSON Web Token è tre segmenti base64url separati da punti: un header, un payload di claim, e una firma, senza padding su nessuno dei segmenti. Questo rende un JWT una delle cose più comuni che decodificherai in Go, e header e payload sono leggibili senza alcuna chiave, il che vale la pena ricordarlo sia per il debug che per le revisioni di sicurezza:
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"}
}
Nota la scelta del decoder: RawURLEncoding, non StdEncoding. I segmenti JWT usano l'alfabeto URL-safe e non portano padding, e un segmento la cui lunghezza è uno o due in meno di un multiplo di quattro farà fallire un decoder con padding proprio in fondo, un errore confuso da rincorrere. Il segmento di firma non puoi leggerlo senza la chiave, e non dovresti provare a fidarti di qualcosa in base al payload da solo, perché nulla impedisce a un client di falsificare i primi due segmenti. Quando ti serve la verifica, usa una libreria mantenuta. Quella de facto è github.com/golang-jwt/jwt/v5 (installala con 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"])
}
Due dettagli della libreria valgono la pena di conoscerli. Il primo: la codifica e la decodifica base64url dei tre segmenti sono gestite internamente, quindi non tocchi mai encoding/base64 direttamente quando firmi o verifichi. Il secondo: la libreria v5 rifiuta i token con alg=none a meno che tu non passi esplicitamente la sua costante UnsafeAllowNoneSignatureType, che ti protegge dal classico errore del "token senza firma accettato".
Data URL
Una data URL è un URL il cui payload è i dati stessi. La sintassi, dalla RFC 2397, è data:[mediatype][;base64],data: un media type opzionale, una flag ;base64 opzionale, una virgola, e poi il contenuto. Quando la flag ;base64 è presente il contenuto è base64 standard, ed è per questo che le data URL e questo articolo condividono una sezione. I browser le usano per incorporare immagini e font direttamente in HTML e CSS, così la pagina ha bisogno di una richiesta in meno:
<img src="data:image/png;base64,iVBORw0KGgo=" alt="pixel">
La libreria standard di Go non ha un helper per le data URL, ma il formato è abbastanza semplice da analizzare a mano con strings, ed è quello che fa la maggior parte dei programmi Go:
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")
}
Tre insidie da tenere a mente. La prima: la flag ;base64 è opzionale, e senza di essa il payload è ASCII percent-encodato invece di base64, quindi controlla il suffisso prima di chiamare un decoder. La seconda: quando il media type è omesso, il predefinito è text/plain;charset=US-ASCII, che raramente conta per le immagini ma sorprende chi analizza altri contenuti. La terza: le data URL sono un trucco per payload piccoli: la stessa RFC dice che lo scheme è utile solo per valori corti, e l'espansione del 33 percento del base64 trasforma un logo da 500 kilobyte in una stringa da 666 kilobyte incollata nel tuo HTML, non cacheabile e non condivisibile. Usale per icone e miniature, non per video.
Lavoro HTTP e API
La decodifica più comune in assoluto nei servizi web Go è il campo del body JSON: un form di upload, una risposta API o un webhook ti consegna una stringa che è in realtà un file. Fai un unmarshal in una struct, poi decodifica il campo:
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")
}
Se la tua API accetta sia stringhe standard che URL-safe, il pattern pragmatico è provare un decoder, e se fallisce con un CorruptInputError vicino alla fine, provare l'altro prima di arrendersi. Non fare questa danza più di una volta, e non rifugiarti mai in "tolta gli uguali e ci speriamo" come strategia generale.
Per l'autenticazione HTTP Basic, non decodifichi nulla, perché lo fa Go per te. Request.BasicAuth, disponibile dal Go 1.4, spezza l'header Authorization per te e restituisce nome utente e password, avendo già fatto girare il decoder base64 standard sulla coppia user:pass definita dalla RFC 2617:
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)
}
Ricorda che la Basic auth è autenticazione, non protezione: l'header è base64, non cifrato, quindi deve viaggiare solo su HTTPS. Se tu sei il client, la chiamata speculare è req.SetBasicAuth(user, pass), che costruisce lo stesso header per te con l'encoder standard.
Un'abitudine difensiva per gli handler API: limita il body prima di decodificarlo, con http.MaxBytesReader o un controllo di lunghezza equivalente. Una stringa base64 si decodifica a circa tre quarti della propria lunghezza, quindi un limite di N byte sul body mantiene il risultato decodificato sotto i N byte, e la memoria resta vincolata indipendentemente da ciò che un client malevolo invia. Decodificare un body illimitato è un vettore classico di esaurimento della memoria, perché l'attaccante controlla quanti megabyte di testo può trasformare in binario.
Set di caratteri legacy
Decodificare base64 ti dà byte, e nei sistemi moderni quei byte sono quasi sempre UTF-8, nel qual caso string(decoded) è tutta la storia. Ma il base64 è un formato vecchio e molta parte è stata prodotta da sistemi che usavano Windows-1252, ISO-8859-1, Shift JIS o qualche altro charset legacy a byte singolo o doppio. Se il produttore l'ha fatto, i byte che decodifichi non sono UTF-8 valido, e Go non farà finta che lo siano: ti mostrerà caratteri di sostituzione ovunque una sequenza sia rotta.
La risposta di Go è il modulo golang.org/x/text, che trasforma i byte codificati in legacy in UTF-8 (e viceversa) per i charset comuni. Lo slot della conversione è subito dopo la decodifica, e costa una chiamata di funzione:
package main
import (
"encoding/base64"
"fmt"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
func main() {
// "Café" salvato in Windows-1252 da uno strumento legacy,
// poi codificato in base64 per il trasporto
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é
}
Il modulo ha un sotto-pacchetto per famiglia di charset: charmap per le tabelle a byte singolo di Windows e ISO, japanese per Shift JIS e EUC-JP, korean per EUC-KR, simplifiedchinese per GB18030, e traditionalchinese per Big5. La regola pratica è convertire solo quando conosci davvero il charset del produttore, perché convertire per una seconda volta byte UTF-8 non fallisce in modo rumoroso; corrompe solo il testo. Quando hai dubbi, tratta il payload come byte e lascia decidere al consumatore a valle.
Streaming e decodifica a blocchi
Hai visto NewDecoder nella sezione sui file; ecco cosa la rende degna di una sezione propria. È un vero adattatore di streaming: pesca dal reader sottostante solo quanto serve, decodifica sul posto, e restituisce un CorruptInputError nel momento in cui lo stream va male. Lo stream intero può essere di terabyte; la memoria che tieni è il tuo buffer e l'output che scrivi. I due pattern di consumo comuni sono io.ReadAll per gli stream piccoli e io.Copy per tutto il resto:
small, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, r))
// va bene per un blob di configurazione o un allegato piccolo
w, err := io.Copy(out, base64.NewDecoder(base64.StdEncoding, r))
// va bene per un video, un tarball, o un lavoro di ripristino
Dal Go 1.22 il pacchetto ha anche AppendDecode, che decodifica in un buffer che riutilizzi invece di allocare una slice fresca per chiamata. È lo strumento per i percorsi caldi che decodificano molti blocchi in un loop, come un processore di righe o un decoder di protocollo:
var buf []byte
for _, chunk := range chunks {
buf, err = base64.StdEncoding.AppendDecode(buf, chunk)
if err != nil {
return err
}
process(buf)
}
Il metodo aggiunge il blocco decodificato a ciò che buf contiene già e restituisce la slice estesa, facendo crescere l'array sottostante se serve. In regime stazionario, dove il buffer è già cresciuto alla dimensione giusta, esegue zero allocazioni per blocco, il che si vede chiaramente in un benchmark. Se il tuo carico di lavoro è "decodifica una volta, raramente", DecodeString è la scelta più semplice; se è "decodifica migliaia di volte in un loop stretto", AppendDecode è quella da scegliere.
Rimanere al sicuro
Qualche nota di sicurezza specifica di come i programmi Go usano davvero questo pacchetto. La prima: il base64 è codifica, non cifratura. Una stringa base64 è leggibile da chiunque con gli strumenti per sviluppatori di un browser web, quindi "codifichiamo in base64 la password prima di inviarla" non è una misura di sicurezza; è una comodità di trasporto. La riservatezza deve venire dal TLS, non dall'alfabeto.
La seconda: vincola i tuoi input. La dimensione decodificata di una stringa base64 è al massimo DecodedLen della sua lunghezza, quindi controlla quel numero contro un limite prima di allocare, e avvolgi i body delle richieste con un soffitto di dimensione prima che qualcosa tocchi un decoder. Entrambi i controlli sono una riga ciascuno, e insieme trasformano una decodifica illimitata in una limitata.
La terza: decidi la tua posizione sugli input disordinati. La modalità normale scarta in silenzio i bit finali non usati dell'ultimo gruppo, il che significa che due stringhe diverse possono decodificarsi negli stessi byte. Per la maggior parte dei dati non importa. Per qualsiasi cosa che faccia parte di un protocollo, di un messaggio firmato, o di un valore che viene confrontato o immagazzinato, Strict() è la scelta prudente, perché rende la forma canonica l'unica forma accettata.
La quarta: fa' attenzione a dove finiscono i byte decodificati. Se un valore decodificato diventa un nome di file, un percorso, un frammento SQL o un argomento di comando, il livello base64 non ti ha protetto da niente: i byte sono ora l'input non fidato del tuo programma, e le regole abituali di sanificazione si applicano esattamente come per qualsiasi altro dato dell'utente.
Quanto è veloce il decoder?
Il base64 in Go è veloce, e resta veloce sui dati grandi perché l'implementazione è un semplice loop di ricerca in tabella senza reflection e senza allocazione per carattere. Su una CPU desktop recente con Go 1.26, una stringa da 500 byte si decodifica in circa un quarto di microsecondo con un'allocazione, il che fa venire fuori un ordine di grandezza di due gigabyte al secondo. Un megabyte di base64 si decodifica in molto meno di un millisecondo; un gigabyte in molto meno di un secondo. I numeri si muovono con l'hardware, ma la forma no: la decodifica base64 è quasi mai il collo di bottiglia; di solito lo è la rete o il disco intorno.
Se sei in un loop caldo, il profilo di allocazione è la cosa da tenere d'occhio. DecodeString alloca la slice di risultato ad ogni chiamata. Decode con una destinazione pre-dimensionata e AppendDecode con un buffer riutilizzato evitano entrambe quell'allocazione del tutto in regime stazionario. Per una decodifica che succede un paio di volte a richiesta, nessuna di queste cose conta; per una decodifica che succede un paio di milioni di volte al secondo, è la differenza tra un profilo di memoria piatto e un garbage collector che si agita.
Una breve storia del pacchetto
Il pacchetto base64 è una delle parti più antiche della libreria standard Go. L'intestazione di copyright del file sorgente riporta 2009, l'anno in cui il linguaggio è stato creato, e il pacchetto fa parte della libreria standard dalla primissima release stabile, Go 1.0, di marzo 2012. Questo significa che il DecodeString che chiami oggi è la stessa API, con lo stesso comportamento, che i programmi Go chiamano da oltre un decennio.
La crescita da allora è stata modesta e utile. Go 1.5 di agosto 2015 ha aggiunto i valori senza padding RawStdEncoding e RawURLEncoding, aprendo la porta a stringhe compatte in stile JWT. Go 1.8 di febbraio 2017 ha aggiunto Strict(), dando ai protocolli un modo per pretendere input canonici. Go 1.22 di febbraio 2024 ha aggiunto AppendDecode e AppendEncode a tutta la famiglia di codifiche base, e ha inasprito WithPadding per rifiutare argomenti senza senso. E a settembre 2026, con Go 1.27.1 come release più recente e Go 1.26 come altra linea supportata, l'API è esattamente quella descritta in questo articolo: quattro codifiche pronte, un decoder di stream, una modalità rigorosa, e una famiglia append per le prestazioni.
Il fatto più profondo è la promessa di compatibilità. La garanzia di Go 1 significa che il pacchetto continuerà ad accettare e rifiutare gli stessi input per sempre, quindi un decoder che scrivi quest'anno per un formato di dati prodotto nel 2015 continuerà a funzionare. Per un formato così vecchio e così noioso, questa è la notizia migliore che ci sia.
Cose che ti sorprenderanno
Dopo un po' in Go smetti di essere sorpreso dal base64, ma le prime volte, di questi fatti, alcuni colpiscono forte, quindi eccoli:
- Il decoder salta
\re\novunque nell'input, ma non uno spazio, non una tabulazione, non uno spazio a larghezza zero. La permissività è voluta; esiste per far funzionare l'input avvolto in MIME, e si ferma esattamente dove si ferma la specifica. - Una decodifica fallita può comunque restituire dati veri. Il risultato parziale è tutto ciò che è stato decodificato prima del byte cattivo, e l'errore arriva insieme a esso, non al suo posto.
CorruptInputErrorè letteralmente solo unint64con un metodo attaccato. L'"errore" è l'offset, e il messaggio viene costruito su richiesta.- Sia
DecodecheEncodesi fidano che tu dimensioni i loro buffer di destinazione. Dai loro un buffer troppo piccolo e non ricevi un errore; ricevi un panico. - Un singolo carattere non è un input valido per nessuna delle quattro codifiche integrate. Un carattere base64 trasporta sei bit, e un byte ne chiede otto, quindi in un carattere non c'è un gruppo completo, con padding o senza.
- A agosto 2026, più di 244.000 pacchetti pubblici su pkg.go.dev elencano
encoding/base64tra i loro import. È, in silenzio, uno dei pacchetti di cui l'intero ecosistema dipende di più.
Quando la decodifica va storta
Questi sono gli errori di decodifica che continuano a ricomparire nei codebase Go, più o meno nell'ordine in cui appaiono nei thread di supporto:
- Scegliere
StdEncodingper dati URL-safe (o il contrario). Il sintomo è un errore al primo-,_,+o/, e la correzione è guardare la stringa prima di scegliere il decoder. - Incollare una stringa da un terminale, una email o un PDF, che infilano spazi, tabulazioni o residui di fine riga. Go salta gli a capo veri, ma uno spazio a metà stringa è un byte corrotto, e l'offset nell'errore punterà dritto su di esso.
- Dimenticare che il risultato è un
[]byte. Stamparlo grezzo ti dà una lista di numeri, e passarlo a una funzione che si aspetta una stringa richiede una conversionestring(...). - Controllare l'errore ma poi usare comunque i dati parziali. Il prefisso a metà decodificato è vero, ma non è il payload, e il codice che lo tratta così fallisce in produzione con dati che sono esattamente la metà della lunghezza giusta.
- Dimensionare il buffer di
Decodeconlen(src)invece diDecodedLen(len(src)). La prima dimensione è sbagliata nella direzione che non ti speri, e il panico che provoca avviene solo su input grandi, il che lo rende un favorito degli ambienti di staging. - Dare per scontato che i segmenti JWT portino padding. Non lo fanno, e un decoder con padding fallisce all'ultimo carattere con un errore che si legge come un mistero. Usa
RawURLEncoding. - Credere che tutto lo spazio vuoto venga saltato. Non è così. Vengono saltati solo i due caratteri di a capo, e lo "spazio vuoto" nella clipboard è una famiglia molto più ampia di così.
- Decodificare due volte, o non decodificare due volte, quando il valore è base64 di base64 (un file allegato a una email che era a sua volta allegata). Il controllo è un solo giro: decodifica una volta, guarda se il risultato sembra ancora base64, e solo allora decodifica di nuovo.
L'altra metà del lavoro
Questa è l'intera parte decodifica della storia: un pacchetto, quattro decoder pronti, un decoder di stream per i dati grandi, una modalità rigorosa per i protocolli schizzinosi, e messaggi di errore che ti dicono il byte dove le cose sono andate storte. Impara le regole di tolleranza, scegli il decoder guardando i caratteri, vincola i tuoi input, e il base64 in Go diventa l'utility noiosa, prevedibile e a zero dipendenze per cui è stato progettato.
Quando il lavoro si capovolge, e il tuo programma Go deve produrre stringhe base64 invece di aprirle, l'articolo correlato sulla codifica Base64 in Go copre quel lato in dettaglio: l'API a un metodo dell'encoder, la chiamata Close che inghiotte in silenzio i tuoi ultimi due byte, l'avvolgimento di righe per il MIME, e come le quattro codifiche si mappano sui canali attraverso cui viaggiano.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in Go: una guida completa