Décodage Base64 en Go : un guide complet
Il y a une longue chaîne cachée dans une réponse d'API, et elle fait semblant d'être une valeur alors qu'elle est en réalité un fichier, un jeton, une image ou un message venu d'un système trois ans plus vieux que le vôtre. Quelque part dans vos lignes de log, vos lignes de base de données et vos payloads JSON, les chaînes base64 n'arrêtent pas de se pointer : l'alphabet standard de 64 lettres, parfois avec un plus et un slash, parfois avec un tiret et un tiret bas, et de temps en temps deux signes d'égalité garés à la fin comme une signature.
La page d'accueil de ce site explique déjà le format lui-même : 64 caractères imprimables portant chacun 6 bits, quatre caractères pour trois octets en entrée, et du padding pour finir le travail. Cet article va donc directement à la moitié du travail où se prennent les décisions intéressantes : ouvrir ces chaînes en Go. La bonne nouvelle, c'est que Go est un endroit formidable pour le faire. Un seul paquet de la bibliothèque standard, zéro dépendance, un décodeur strict par défaut mais conciliant avec les retours à la ligne, et des messages d'erreur qui pointent l'octet exact qui a posé problème.
Ce que Go fournit
Tout ce qu'il vous faut est déjà dans la bibliothèque standard. Le paquet s'appelle encoding/base64, son fichier source porte encore un en-tête de copyright 2009, l'année de naissance du langage, et il n'y a ni extension à activer, ni module à récupérer, ni réglage à basculer. Si go version affiche quoi que ce soit sur votre machine, vous possédez déjà l'outil complet.
À l'heure où j'écris ces lignes, la version la plus récente est Go 1.27.1, sortie le 1er septembre 2026, avec la ligne Go 1.26 (actuellement 1.26.8) comme autre ligne supportée. L'API base64 est identique sur les deux, et grâce à la promesse de compatibilité Go 1, un programme qui décode du base64 aujourd'hui fera exactement la même chose sur chaque version future. Récupérez Go lui-même dans les tarballs officiels sur go.dev/dl (quelque chose comme go1.27.1.linux-amd64.tar.gz, décompressé dans /usr/local), dans le gestionnaire de paquets de votre distribution (sudo apt install golang-go sur les systèmes basés sur Ubuntu), ou via le wrapper golang.org/dl si vous aimez avoir plusieurs versions de Go côte à côte.
Une fois Go installé, go doc encoding/base64 affiche toute l'API dans une colonne lisible, ce qui est le moyen le plus rapide de rafraîchir votre mémoire. Le seul add-on que cet article utilise quelque part est golang.org/x/text pour les jeux de caractères hérités, installé avec go get golang.org/x/text. Il apparaît une fois, dans sa propre section, et le reste est de la bibliothèque standard pure.
Votre premier décodage
Neuf fois sur dix, une vie de décodage en Go tient en une seule méthode du type Encoding :
func (enc *Encoding) DecodeString(s string) ([]byte, error)
Donnez-lui une chaîne base64, et elle vous rend les octets qu'elle représente, plus une erreur quand l'entrée se comporte mal :
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
}
Deux choses dans cette signature méritent d'être mémorisées. D'abord, le résultat est un []byte, pas une chaîne, parce que les octets que vous dépaquetez peuvent être du base64 parfaitement valide et un texte parfaitement horrible : un en-tête PNG, une archive compressée, un protocole binaire. Enrobez-les dans string(...) seulement quand vous savez que le payload est du texte. Ensuite, la méthode renvoie toujours deux valeurs. Une erreur nil veut dire que la chaîne était du base64 propre ; une erreur non nil veut dire que l'entrée était cassée quelque part, et la tranche d'octets que vous avez reçue peut être un résultat partiel au lieu d'être vide. Vous verrez les deux faces de ce comportement dans la section sur les erreurs plus bas.
Quatre décodeurs, une question : quel alphabet ?
Go fournit quatre valeurs Encoding prêtes à l'emploi, et choisir la bonne est la première vraie décision de chaque décodage. Le tableau ci-dessous est le plan de table :
| Variable | Alphabet | Padding | Où vous la croiserez |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
e-mails MIME, data URLs, auth HTTP Basic, fichiers PEM, JSON courant |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
chemins et requêtes d'URL, noms de fichiers |
RawStdEncoding |
A-Z a-z 0-9 + / |
aucun | base64 standard sans padding venant de producteurs compacts |
RawURLEncoding |
A-Z a-z 0-9 - _ |
aucun | segments de JWT, identifiants d'API compacts |
Le moyen le plus rapide de choisir est de regarder les données elles-mêmes. Une chaîne qui contient + ou / ne peut qu'être du base64 à alphabet standard, il lui faut donc l'un des deux décodeurs Std. Une chaîne qui contient - ou _ est la variante URL-safe du RFC 4648, il lui faut donc l'un des deux décodeurs URL. Puis vérifiez la fin : des caractères = en queue signifient la variante avec padding, et leur absence la variante Raw. Voici ce que ça fait, un mauvais choix :
decoded, err := base64.StdEncoding.DecodeString("P29_")
// err: illegal base64 data at input byte 3
// le tiret bas n'est pas dans l'alphabet standard, donc le
// décodeur s'arrête au dernier caractère qu'il ne reconnaît pas
decoded, err = base64.URLEncoding.DecodeString("P29_")
// le décodage donne les trois octets 0x3f 0x6f 0x7f, err est nil
Si vous décodez des données venant d'un producteur qui a défini son propre alphabet de 64 caractères, base64.NewEncoding("...64 chars...") vous fabrique un décodeur pour lui. L'alphabet doit contenir exactement 64 valeurs d'octet uniques et ne pas contenir de retour à la ligne - la fonction panique sinon - et même si la documentation exige de l'alphabet qu'il exclue le caractère de padding, la fonction n'applique pas cette exigence - un alphabet contenant '=' est accepté sans panique. Au quotidien, vous en aurez rarement besoin, mais il est là, et c'est le seul moyen de décoder un schéma privé.
Le problème de la tolérance : quelles entrées Go accepte-t-il ?
Chaque décodeur base64 doit prendre une décision inconfortable : quel volume de saletés est-il prêt à avaler ? La réponse de Go est une ligne soigneusement tracée. Côté conciliant, le décodeur saute les retours chariot et les sauts de ligne n'importe où dans l'entrée, alors une chaîne coupée sur de nombreuses lignes par un client e-mail ou un outil PEM se décode sans aucun prétraitement :
decoded, err := base64.StdEncoding.DecodeString("T\nW\nF\r\nu")
// le décodage donne "Man", err est nil
// chaque \r et \n de la chaîne a tout simplement été ignoré
Côté strict, tout le reste est interdit. Un espace, un tabulateur, un caractère de largeur nulle copié depuis un PDF, un deux-points qui traine dans un en-tête : au moment où le décodeur croise un caractère qui n'est pas dans l'alphabet et n'est pas un retour à la ligne, il s'arrête et signale le décalage. Et il conserve tout ce qu'il a déjà décodé :
decoded, err := base64.StdEncoding.DecodeString("TWFu junk")
// le décodage donne "Man" (la partie avant l'espace),
// err est : illegal base64 data at input byte 4
Cette combinaison surprend : un décodage qui échoue peut quand même vous rendre un demi-résultat utilisable. Que ce soit une fonctionnalité ou un danger, ça dépend de vous ; l'essentiel est que err == nil est la seule condition sous laquelle les données sont complètes.
Le padding a ses propres règles, et elles diffèrent entre les variantes avec padding et les variantes brutes. Les décodeurs avec padding travaillent par groupes : un groupe est soit quatre caractères réels, soit deux caractères réels suivis de ==. Un seul caractère ne constitue jamais un groupe complet, alors "T" échoue, et "TWF" échoue aussi, parce que trois caractères exigent un signe de padding qui manque. Les décodeurs brutes abandonnent l'exigence de padding, mais ils ne peuvent toujours pas accepter une longueur où un groupe manquerait trois de ses quatre caractères, alors "T" échoue là aussi, tandis que "TW" se décode sans histoire en un seul octet.
base64.StdEncoding.DecodeString("T") // erreur à l'octet 0 de l'entrée
base64.StdEncoding.DecodeString("TWF") // erreur à l'octet 0 de l'entrée
base64.RawStdEncoding.DecodeString("TW") // 1 octet, pas d'erreur
base64.StdEncoding.DecodeString("TWFu====") // "Man" plus erreur à l'octet 4
Il y a encore un autre interrupteur d'humeur : Strict(), ajouté dans Go 1.8. En mode strict, le décodeur applique la forme canonique de la section 3.5 du RFC 4648 : les bits de fin inutilisés du groupe final doivent être à zéro. Le mode normal s'en fiche, parce que ces bits ne sont tout simplement jamais utilisés, alors "Qm==" se décode en l'octet B sans se plaindre. Le mode strict refuse, lui :
decoded, err := base64.StdEncoding.DecodeString("Qm==")
// le décodage donne "B", err est nil (les bits de fin ont été écartés)
decoded, err = base64.StdEncoding.Strict().DecodeString("Qm==")
// err est : illegal base64 data at input byte 2
Notez que même en mode strict, les retours à la ligne sont toujours sautés, comme le souligne la documentation. Utilisez Strict() quand vous parlez un protocole qui se soucie de l'encodage canonique, ou quand vous voulez rejeter les producteurs distraits au lieu d'absorber silencieusement leurs bits.
Des erreurs qui vous disent où
Chaque échec dans ce paquet arrive sous forme d'une valeur concrète, inspectable. Quand l'entrée contient quelque chose que l'alphabet ne connaît pas, ou que le padding est faux, le décodeur renvoie un base64.CorruptInputError, et son message inclut le décalage de l'octet problématique :
type CorruptInputError int64
func (e CorruptInputError) Error() string {
return "illegal base64 data at input byte " + strconv.FormatInt(int64(e), 10)
}
Ce décalage fait toute la différence entre « quelque chose a échoué » et « le 4102e caractère de cette chaîne de 900 kilo-octets est un tabulateur venu d'un presse-papiers ». Attrapez-le avec l'idiome Go habituel :
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)
}
Voici le tableau des symptômes pour les entrées qui déroutent le plus :
| Entrée (StdEncoding) | Résultat | Pourquoi |
|---|---|---|
TWF$ |
erreur à l'octet 3 | $ n'est pas dans l'alphabet |
T |
erreur à l'octet 0 | un seul caractère n'est jamais un groupe complet |
TWF |
erreur à l'octet 0 | trois caractères exigent un = qui manque |
TWFu junk |
Man plus erreur à l'octet 4 |
l'espace n'est pas un retour à la ligne, donc le décodage s'arrête là |
TWFu\t |
Man plus erreur à l'octet 4 |
les tabulateurs ne sont pas sautés, seuls \r et \n le sont |
T\nW\nF\nu |
Man, pas d'erreur |
les retours à la ligne sont ignorés n'importe où |
==== |
erreur à l'octet 0 | un padding au début d'un groupe n'est pas valide |
(chaîne vide) |
résultat vide, pas d'erreur | zéro octet de base64 se décodent en zéro octet |
Un conseil pratique : quand un décodage échoue en production, journalisez le décalage et une petite fenêtre autour. Dans neuf cas sur dix, l'octet « corrompu » est un caractère blanc que le transport, le presse-papiers ou une visionneuse PDF a glissé dans la chaîne, et la correction est un trim ou un nettoyage, pas une refonte.
Ouvrir des fichiers
Les fichiers base64 ne sont que des fichiers texte qui contiennent du base64, alors les outils habituels de fichiers de Go s'appliquent. Pour un fichier qui rentre confortablement en mémoire, lisez-le tout et décodez la chaîne :
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")
}
Pour les gros fichiers, le meilleur motif est le streaming, et il utilise l'autre moitié de l'API du paquet : NewDecoder enrobe n'importe quel io.Reader dans un lecteur qui décode le base64, alors vous pouvez pomper fichier vers fichier sans jamais garder tout le payload en mémoire :
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")
Il y a une option intermédiaire quand vous voulez éviter l'allocation supplémentaire que fait DecodeString : Decode écrit dans un buffer de destination que vous contrôlez. Dimensionnez-le avec DecodedLen, qui renvoie le nombre maximal d'octets de sortie pour une longueur d'entrée donnée :
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 taille réellement décodée
fmt.Println(len(data), "bytes")
Méfiez-vous de ce dernier, cependant : Decode fait confiance à vous pour dimensionner le buffer. S'il est trop petit, la méthode ne renvoie pas d'erreur ; elle panique avec un index hors de portée. DecodedLen est le nombre à utiliser, pas len(raw).
Une ligne de commande base64 pour Go
Les systèmes Unix fournissent un utilitaire base64 avec coreutils, et Go ne fournit pas d'exécutable équivalent. La réponse idiomatique dans le monde Go n'est pas un paquet à installer mais un programme que vous possédez : un petit outil en ligne de commande construit autour de encoding/base64, du paquet flag et de l'entrée standard. En voici un complet, une quarantaine de lignes, qui décode ce qui arrive par le tuyau et écrit les octets bruts :
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)
}
Compilez-le une fois avec go build -o b64 . et il devient un décodeur multiplateforme que vous glisserez dans un Makefile, un pipeline CI ou une fonction shell : printf 'TWFu' | ./b64 affiche Man, et ./b64 -url < token.b64 > token.bin déplie un jeton URL-safe dans un fichier. Deux propriétés du design méritent d'être notées. Parce qu'il lit toute l'entrée standard avant de décoder, une entrée coupée sur plusieurs lignes se décode sans souci, grâce à la tolérance du décodeur aux retours à la ligne. Et parce qu'il quitte avec le statut 1 sur une entrée invalide et écrit sa réclamation sur stderr, il se comporte comme un outil dans un pipeline plutôt que comme un script qui s'excuse. C'est tout l'art d'un CLI Go : un paquet, un flag, entrée standard, sortie standard, et un code de sortie.
Décodage URL-safe
La variante URL-safe existe parce que l'alphabet standard entre en collision avec la grammaire des URL : + est souvent lu comme un espace dans les chaînes de requête, et / commence un nouveau segment de chemin, alors une chaîne base64 standard embarquée dans une URL doit être échappée en percent caractère par caractère, ce qui est lent à analyser et laid à lire. L'alphabet alternatif du RFC 4648 échange + et / contre - et _, tous les deux légaux sans échappement dans les chemins, les requêtes et les noms de fichiers d'URL.
En Go, le changement tient en une variable de décodeur différente. Si vos données sont URL-safe avec padding, utilisez URLEncoding ; si elles sont URL-safe sans padding, utilisez RawURLEncoding. Le cas classique est un identifiant qui vit dans une URL ou un nom de fichier :
decoded, err := base64.RawURLEncoding.DecodeString("-w9n")
// le décodage donne les trois octets 0xfb 0x0f 0x67
// le tiret et le tiret bas font partie de l'alphabet URL-safe,
// donc RawURLEncoding les gère là où StdEncoding échouerait
Où vous le croiserez dans du vrai code Go : segments de JWT (voir la section suivante), identifiants opaques générés par des systèmes et stockés dans des URL, noms de fichiers qui ne doivent pas casser un serveur web ou un stockage d'objets cloud, et toute API qui a promis « base64url » dans sa documentation. Une mise en garde : URL-safe est un contrat entre producteur et consommateur, pas une propriété des données. Si la chaîne contient un + ou un /, elle n'est pas URL-safe, point final, et aucune quantité de nouvelles tentatives avec le décodeur URL n'y changera rien. Regardez d'abord les caractères, puis choisissez le décodeur.
Jeter un œil à l'intérieur des JWT
Un JSON Web Token est trois segments base64url séparés par des points : un en-tête, un payload de claims, et une signature, sans padding sur aucun des segments. Cela fait du JWT l'une des choses que vous décoderez le plus souvent en Go, et l'en-tête et le payload se lisent sans aucune clé, ce qui vaut le coup d'être retenu aussi bien pour le débogage que pour les revues de sécurité :
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"}
}
Remarquez le choix du décodeur : RawURLEncoding, pas StdEncoding. Les segments de JWT utilisent l'alphabet URL-safe et ne portent aucun padding, et un segment dont la longueur est en deçà d'un multiple de quatre de un ou deux caractères fera échouer un décodeur avec padding tout à la fin, ce qui est une erreur difficile à remonter. Le segment de signature ne se lit pas sans la clé, et vous ne devriez pas faire confiance à quoi que ce soit sur la base du payload seul, parce que rien n'empêche un client de forger les deux premiers segments. Quand vous avez besoin de vérification, utilisez une bibliothèque maintenue. La bibliothèque de facto est github.com/golang-jwt/jwt/v5 (installez-la avec 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"])
}
Deux détails de la bibliothèque valent le coup d'être connus. D'abord, l'encodage et le décodage base64url des trois segments sont gérés à l'intérieur, alors vous ne touchez jamais encoding/base64 directement quand vous signez ou vérifiez. Ensuite, la bibliothèque v5 refuse les jetons avec alg=none à moins que vous ne passiez explicitement sa constante UnsafeAllowNoneSignatureType, ce qui vous protège de la classique erreur du « jeton non signé accepté ».
Data URLs
Une data URL est une URL dont le payload est la donnée elle-même. La syntaxe, d'après le RFC 2397, est data:[mediatype][;base64],data : un type média optionnel, un drapeau ;base64 optionnel, une virgule, puis le contenu. Quand le drapeau ;base64 est présent, le contenu est du base64 standard, voilà pourquoi les data URLs et cet article partagent une section. Les navigateurs les utilisent pour embarquer images et polices directement dans le HTML et le CSS, pour que la page ait une requête de moins :
<img src="data:image/png;base64,iVBORw0KGgo=" alt="pixel">
La bibliothèque standard de Go n'a pas d'assistant pour les data URLs, mais le format est simple à analyser à la main avec strings, et c'est ce que font la plupart des programmes 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")
}
Trois pièges à garder en tête. D'abord, le drapeau ;base64 est optionnel, et sans lui le payload est de l'ASCII en percent-encoding plutôt que du base64, alors vérifiez le suffixe avant d'appeler un décodeur. Ensuite, quand le type média est omis, il vaut par défaut text/plain;charset=US-ASCII, ce qui importe rarement pour les images mais surprend ceux qui analysent d'autres contenus. Enfin, les data URLs sont un tour pour petits payloads : le RFC lui-même dit que le schéma n'est utile que pour de courtes valeurs, et l'augmentation de taille de 33 pour cent du base64 fait d'un logo de 500 kilo-octets une chaîne de 666 kilo-octets collée dans votre HTML, impossible à mettre en cache et à partager. Utilisez-les pour des icônes et des vignettes, pas pour des vidéos.
Travaux HTTP et API
Le décodage le plus courant dans les services web Go est le champ du corps JSON : un formulaire d'upload, une réponse d'API ou un webhook vous remette une chaîne qui est en réalité un fichier. Désérialisez dans une structure, puis décodez le champ :
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")
}
Si votre API accepte à la fois des chaînes standard et URL-safe, le motif pratique est d'essayer un décodeur, et s'il échoue avec un CorruptInputError vers la fin, d'essayer l'autre avant de baisser les bras. Ne faites pas ce duo plus d'une fois, et n'adoptez jamais « enlevez les signes d'égalité et priez » comme stratégie générale.
Pour l'authentification HTTP Basic, vous ne décodez rien du tout, parce que Go le fait pour vous. Request.BasicAuth, disponible depuis Go 1.4, découpe l'en-tête Authorization pour vous et renvoie le nom d'utilisateur et le mot de passe, après avoir déjà passé le décodeur base64 standard sur la paire user:pass que définit le 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)
}
Souvenez-vous que l'auth Basic est une authentification, pas une protection : l'en-tête est du base64, pas du chiffré, alors il ne doit voyager que sur HTTPS. Si vous êtes le client, l'appel miroir est req.SetBasicAuth(user, pass), qui construit le même en-tête pour vous avec l'encodeur standard.
Une habitude défensive pour les gestionnaires d'API : limitez le corps avant de le décoder, avec http.MaxBytesReader ou un contrôle de longueur équivalent. Une chaîne base64 se décode à environ les trois quarts de sa propre longueur, alors une limite de corps de N octets garde le résultat décodé sous N octets, et la mémoire reste bornée quoi que poste un client malveillant. Décoder un corps sans limite est un vecteur classique d'épuisement mémoire, parce que l'attaquant contrôle combien de mégaoctets de texte il peut transformer en binaire.
Jeux de caractères hérités
Décoder du base64 vous donne des octets, et dans les systèmes modernes ces octets sont presque toujours de l'UTF-8, auquel cas string(decoded) est toute l'histoire. Mais le base64 est un format ancien, et une bonne partie de celui qu'on trouve a été produite par des systèmes qui utilisaient Windows-1252, ISO-8859-1, Shift JIS ou un autre jeu de caractères hérité à octet simple ou double. Si le producteur l'a fait, les octets que vous décodez ne sont pas de l'UTF-8 valide, et Go ne fera pas semblant : il vous affichera des caractères de remplacement partout où une séquence est cassée.
La réponse de Go est le module golang.org/x/text, qui transforme les octets encodés en hérité en UTF-8 (et l'inverse) pour les jeux de caractères courants. L'emplacement de la conversion est juste après le décodage, et il tient en un appel de fonction :
package main
import (
"encoding/base64"
"fmt"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
func main() {
// "Café" stocké en Windows-1252 par un outil hérité,
// puis encodé en base64 pour le 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é
}
Le module a un sous-paquet par famille de jeux de caractères : charmap pour les tables à octet simple de Windows et ISO, japanese pour Shift JIS et EUC-JP, korean pour EUC-KR, simplifiedchinese pour GB18030, et traditionalchinese pour Big5. La règle générale est de ne convertir que quand vous connaissez vraiment le jeu de caractères du producteur, parce que convertir une deuxième fois des octets déjà en UTF-8 n'échoue pas à grand bruit ; elle défigure juste le texte. En cas de doute, traitez le payload comme des octets et laissez le consommateur en aval décider.
Streaming et décodage par morceaux
Vous avez vu NewDecoder dans la section fichiers ; voici ce qui lui vaut une section à part entière. C'est un véritable adaptateur de streaming : il tire du lecteur sous-jacent exactement ce qu'il faut, décode sur place, et renvoie un CorruptInputError dès que le flux se gâte. Le flux entier peut faire des téraoctets ; la mémoire que vous tenez est votre buffer et la sortie que vous écrivez. Les deux motifs de consommation courants sont io.ReadAll pour les petits flux et io.Copy pour tout le reste :
small, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, r))
// parfait pour un blob de configuration ou une petite pièce jointe
w, err := io.Copy(out, base64.NewDecoder(base64.StdEncoding, r))
// parfait pour une vidéo, une archive tar, ou un travail de restauration
Depuis Go 1.22, le paquet a aussi AppendDecode, qui décode dans un buffer que vous réutilisez au lieu d'allouer une tranche neuve à chaque appel. C'est l'outil pour les chemins chauds qui décodent beaucoup de morceaux dans une boucle, comme un processeur de lignes ou un décodeur de protocole :
var buf []byte
for _, chunk := range chunks {
buf, err = base64.StdEncoding.AppendDecode(buf, chunk)
if err != nil {
return err
}
process(buf)
}
La méthode ajoute le morceau décodé à ce que buf contient déjà et renvoie la tranche étendue, en grandissant le tableau sous-jacent au besoin. En régime établi, quand le buffer a déjà grandi à la bonne taille, elle effectue zéro allocation par morceau, ce qui saute aux yeux dans un benchmark. Si votre charge est « décoder une fois, rarement », DecodeString est le choix le plus simple ; si elle est « décoder des milliers de fois dans une boucle serrée », c'est AppendDecode qu'il faut viser.
Garder les choses en sécurité
Quelques notes de sécurité spécifiques à la façon dont les programmes Go utilisent vraiment ce paquet. D'abord, le base64 est un encodage, pas un chiffrement. Une chaîne base64 est lisible par n'importe qui muni des outils de développement d'un navigateur web, alors « on base64 le mot de passe avant de l'envoyer » n'est pas une mesure de sécurité ; c'est une commodité de transport. La confidentialité doit venir du TLS, pas de l'alphabet.
Deuxièmement, bornez vos entrées. La taille décodée d'une chaîne base64 est au plus DecodedLen de sa longueur, alors vérifiez ce nombre contre une limite avant d'allouer, et enrobez les corps de requête avec une limite de taille avant que quoi que ce soit ne touche un décodeur. Les deux vérifications tiennent chacune en une ligne, et ensemble elles transforment un décodage sans bornes en un décodage borné.
Troisièmement, décidez de votre position sur les entrées approximatives. Le mode normal écarte silencieusement les bits de fin inutilisés du groupe final, ce qui veut dire que deux chaînes différentes peuvent se décoder vers les mêmes octets. Pour la plupart des données, ça n'a pas d'importance. Pour tout ce qui fait partie d'un protocole, d'un message signé, ou d'une valeur qui sera comparée ou stockée, Strict() est le choix prudent, parce qu'il fait de la forme canonique la seule forme acceptée.
Quatrièmement, faites attention à l'endroit où vont les octets décodés. Si une valeur décodée devient un nom de fichier, un chemin, un fragment SQL ou un argument de commande, la couche base64 ne vous a protégé de rien : les octets sont maintenant l'entrée non fiable de votre programme, et les règles habituelles de nettoyage s'appliquent exactement comme pour n'importe quelle autre donnée utilisateur.
Quelle est la vitesse du décodeur ?
Le base64 en Go est rapide, et il le reste sur les grosses données parce que l'implémentation est une simple boucle de consultation de table, sans réflexion et sans allocation par caractère. Sur un CPU de bureau récent tournant Go 1.26, une chaîne de 500 octets se décode en un peu plus d'un quart de microseconde avec une allocation, ce qui donne de l'ordre de deux gigaoctets par seconde. Un mégaoctet de base64 se décode en moins d'une milliseconde ; un gigaoctet en moins d'une seconde. Les chiffres bougent avec le matériel, mais pas leur forme : le décodage base64 est presque jamais le goulot d'étranglement ; c'est le réseau ou le disque autour qui l'est, en général.
Si vous êtes dans une boucle chaude, c'est le profil d'allocation qu'il faut surveiller. DecodeString alloue la tranche de résultat à chaque appel. Decode avec une destination pré-dimensionnée et AppendDecode avec un buffer réutilisé évitent tous les deux cette allocation entièrement en régime établi. Pour un décodage qui arrive quelques fois par requête, aucune de ces choses n'importe ; pour un décodage qui arrive quelques millions de fois par seconde, c'est la différence entre un profil mémoire plat et un collecteur d'ordures qui s'essouffle.
Une courte histoire du paquet
Le paquet base64 est l'une des plus anciennes parties de la bibliothèque standard de Go. L'en-tête de copyright du fichier source indique 2009, l'année de création du langage, et le paquet fait partie de la bibliothèque standard depuis la toute première version stable, Go 1.0, en mars 2012. Cela veut dire que le DecodeString que vous appelez aujourd'hui est la même API, avec le même comportement, que les programmes Go appellent depuis plus d'une décennie.
La croissance depuis est modeste et utile. Go 1.5 d'août 2015 a ajouté les valeurs RawStdEncoding et RawURLEncoding sans padding, qui ont ouvert la porte aux chaînes compactes à la JWT. Go 1.8 de février 2017 a ajouté Strict(), offrant aux protocoles un moyen d'exiger une entrée canonique. Go 1.22 de février 2024 a ajouté AppendDecode et AppendEncode à toute la famille des encodages de base, et a resserré WithPadding pour rejeter les arguments absurdes. Et en septembre 2026, avec Go 1.27.1 comme version la plus récente et Go 1.26 comme autre ligne supportée, l'API est exactement celle décrite dans cet article : quatre encodages prêts à l'emploi, un décodeur de flux, un mode strict, et une famille append pour la performance.
Le fait plus profond est la promesse de compatibilité. La garantie Go 1 veut dire que le paquet continuera d'accepter et de rejeter les mêmes entrées pour toujours, alors un décodeur que vous écrivez cette année pour un format de données produit en 2015 continuera de marcher. Pour un format aussi vieux et aussi ennuyeux, c'est la meilleure nouvelle qui soit.
Les choses qui vont vous surprendre
Après un moment en Go, vous cessez d'être surpris par le base64, mais les premières fois, certains de ces faits tombent fort, alors les voilà :
- Le décodeur saute
\ret\nn'importe où dans l'entrée, mais pas un espace, pas un tabulateur, pas un espace de largeur nulle. La tolérance est délibérée ; elle existe pour que les entrées coupées à la MIME fonctionnent, et elle s'arrête exactement là où la spécification s'arrête. - Un décodage qui échoue peut quand même renvoyer de vraies données. Le résultat partiel est tout ce qui a été décodé avant l'octet fautif, et l'erreur arrive avec, pas à la place.
CorruptInputErrorest littéralement juste unint64avec une méthode accrochée. L'« erreur » est le décalage, et le message est construit à la demande.DecodeetEncodefont tous les deux confiance à ce que vous dimensionnez leurs buffers de destination. Donnez-leur un buffer trop petit et vous n'avez pas une erreur ; vous avez une panique.- Un seul caractère n'est pas une entrée valide pour aucun des quatre encodages intégrés. Un caractère base64 porte six bits, et un octet en exige huit, alors il n'y a pas de groupe complet dans un seul caractère, avec padding ou sans.
- En août 2026, plus de 244 000 paquets publics sur pkg.go.dev listent
encoding/base64parmi leurs imports. C'est discrètement l'un des paquets dont on dépend le plus de tout l'écosystème.
Où les décodages se trompent
Voici les erreurs de décodage qui ne cessent de resurgir dans les codebases Go, grosso modo dans l'ordre où elles apparaissent dans les filages de support :
- Choisir
StdEncodingpour des données URL-safe (ou l'inverse). Le symptôme est une erreur au premier-,_,+ou/, et la correction est de regarder la chaîne avant de choisir le décodeur. - Coller une chaîne venue d'un terminal, d'un e-mail ou d'un PDF, qui glisse des espaces, des tabulateurs ou des reliquats de fins de ligne. Go saute les vrais retours à la ligne, mais un espace au milieu de la chaîne est un octet corrompu, et le décalage de l'erreur pointera droit dessus.
- Oublier que le résultat est un
[]byte. L'afficher tel quel vous donne une liste de nombres, et le passer à une fonction qui attend une chaîne exige une conversionstring(...). - Vérifier l'erreur et utiliser quand même les données partielles. Le préfixe à moitié décodé est réel, mais ce n'est pas le payload, et un code qui le traite comme tel échoue en production avec des données exactement de la moitié de la bonne longueur.
- Dimensionner le buffer de
Decodeaveclen(src)au lieu deDecodedLen(len(src)). La première taille est fausse, mais dans l'autre sens que vous n'espériez pas, et la panique qu'elle provoque n'arrive que sur les grosses entrées, ce qui en fait la favorite des environnements de préproduction. - Supposer que les segments de JWT portent un padding. Ce n'est pas le cas, et un décodeur avec padding échoue sur le dernier caractère avec une erreur qui ressemble à un mystère. Utilisez
RawURLEncoding. - Croire que tout caractère blanc est sauté. Ce n'est pas le cas. Seuls les deux caractères de retour à la ligne le sont, et le « caractère blanc » du presse-papiers est une famille bien plus large que ça.
- Décoder deux fois, ou ne pas décoder deux fois, quand la valeur est un base64 de base64 (un fichier joint à un e-mail qui était lui-même joint). Le contrôle tient en un aller-retour : décodez une fois, voyez si le résultat a encore l'air d'être du base64, et seulement alors décodez encore.
L'autre moitié du travail
Voilà toute la moitié décodage de l'histoire : un paquet, quatre décodeurs prêts à l'emploi, un décodeur de flux pour les grosses données, un mode strict pour les protocoles pointilleux, et des messages d'erreur qui vous disent à quel octet les choses ont pris l'eau. Apprenez les règles de tolérance, choisissez votre décodeur en regardant les caractères, bornez vos entrées, et le base64 en Go devient l'utilitaire ennuyeux, prévisible et sans dépendance pour lequel il a été conçu.
Quand le travail s'inverse, et que votre programme Go doit produire des chaînes base64 au lieu de les ouvrir, l'article relié sur l'encodage Base64 en Go couvre cette moitié en détail : l'API à une seule méthode de l'encodeur, l'appel Close qui avale silencieusement vos deux derniers octets, le coupage des lignes pour la MIME, et la façon dont les quatre encodages se répartissent sur les canaux qu'ils empruntent.
Dernière mise à jour : 2026-09-08
Article associé : Encodage Base64 en Go : un guide complet