Vous avez à traiter le format Base64 ? Alors ce site est parfait pour vous ! Utilisez notre outil en ligne super pratique pour encoder ou décoder vos données.

Encodage Base64 en Go : un guide complet

De temps en temps, votre programme Go doit remettre des données binaires à un monde qui n'accepte que du texte : un champ JSON qui doit rester une chaîne, une URL qui doit rester un jeton unique, une pièce jointe d'e-mail qui traverse des serveurs qui se souviennent des jours du 7 bits, une image qui veut vivre dans le HTML pour que la page évite une requête. Le base64 est le coursier pour ce travail-là, précisément, et la page d'accueil de ce site explique déjà le format en profondeur, alors cet article va directement au métier d'empaquetage : produire des chaînes base64 en Go que chaque décodeur de la planète peut ouvrir sans se battre.

La bonne nouvelle pour commencer : l'encodeur est la moitié douce de l'histoire. Une méthode, pas de renvoi d'erreur, pas de mode d'échec, une sortie byte-identique sur chaque version de Go depuis la première stable. Tout le drame vit autour de cette méthode : choisir le bon alphabet pour le canal que la chaîne va emprunter, l'appel Close qui avale silencieusement vos deux derniers octets quand vous l'oubliez, la taxe de taille que le format fait payer, et le fait que Go, comme Python, Java et Node, n'enveloppe jamais sa sortie à 76 caractères. Rencontrez d'abord la fonction, puis rencontrez les pièges.

Empaqueter sans jamais échouer

Neuf fois sur dix, une vie d'encodage en Go tient en une seule méthode du type Encoding, et comme tous les points d'entrée d'encodage du paquet (Encode, AppendEncode), elle n'a pas de renvoi d'erreur :

func (enc *Encoding) EncodeToString(src []byte) string

Donnez-lui des octets, elle vous donne une chaîne, et c'est tout le contrat :

package main

import (
  "encoding/base64"
  "fmt"
)

func main() {
  packed := base64.StdEncoding.EncodeToString([]byte("Man"))
  fmt.Println(packed) // TWFu
}

Il n'y a pas de valeur d'erreur parce qu'il n'y a rien qui puisse mal tourner : n'importe quel octet est une entrée légale, l'alphabet le couvre toujours, et la sortie est toujours du pur ASCII. Trois propriétés méritent d'être mémorisées, parce qu'elles répondent à la moitié de toutes vos futures questions. D'abord, la longueur de sortie est une fonction arithmétique pure de la longueur d'entrée, et le paquet vous tend même la formule comme une méthode : EncodedLen(n) renvoie (n+2)/3*4 pour les encodages avec padding, alors 3 octets en entrée deviennent 4 caractères, 6 en deviennent 8, et ainsi de suite. Ensuite, le format fait payer une taxe de taille : tous les trois octets de données reviennent comme quatre caractères, c'est l'augmentation familière d'environ 33 pour cent qui apparaît sur vos factures de bande passante et vos quotas de stockage. Enfin, la méthode est déterministe : les mêmes octets produisent toujours la même chaîne, sur n'importe quelle machine, dans n'importe quelle version de Go, pour toujours. C'est cette détermination qui fait du base64 un format de sérialisation plutôt qu'un mystère.

Une note spécifique à Go sur le côté entrée : la méthode prend un []byte, pas une string, et la conversion []byte(...) est explicite à chaque site d'appel - Go ne convertit jamais une chaîne en tranche pour vous - et elle produit une copie indépendante des octets de la chaîne. Le compilateur peut éliminer cette copie quand la tranche n'est que lue et ne s'échappe pas, c'est pourquoi le coût est généralement non mesurable ; mais si la tranche est stockée ou renvoyée, le temps d'exécution paie une vraie copie O(n). Le texte dans un programme Go est de l'UTF-8 par convention, alors quand vous encodez une chaîne, vous encodez ses octets UTF-8, et c'est exactement ce que chaque décodeur moderne de l'autre bout attend. On en reparle dans la section Texte, octets et Unicode.

Comment Go le fournit

Comme tout dans cet article, l'encodeur vient du paquet de la bibliothèque standard encoding/base64, qui est fourni depuis la toute première version du langage et dont le fichier source porte encore son en-tête de copyright 2009. Il n'y a pas de module à récupérer, pas de drapeau de fonctionnalité à basculer, et pas de particularité de plateforme : si go version fonctionne, go doc encoding/base64 affiche toute l'API pour vous.

À 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. Installez Go depuis les tarballs officiels sur go.dev/dl, depuis le gestionnaire de paquets de votre distribution (sudo apt install golang-go), ou à travers le wrapper golang.org/dl si vous jonglez avec les versions. L'API base64 est identique sur les deux lignes supportées, et le tableau ci-dessous est toute l'histoire de ce qui a changé, ce qui est une liste courte pour un paquet aussi central :

Version Année Ce qui a changé dans encoding/base64
Go 1.0 2012 Paquet stable dès le premier jour ; copyright du source 2009
Go 1.5 2015 RawStdEncoding et RawURLEncoding ajoutés pour une sortie sans padding
Go 1.8 2017 Strict() ajouté pour le décodage canonique (côté décodeur)
Go 1.22 2024 AppendEncode et AppendDecode ajoutés ; WithPadding rejette désormais les mauvais arguments
Go 1.27.1 2026 Version actuelle ; API inchangée, comportement stable à l'octet grâce à la promesse Go 1

La conséquence pratique de cette histoire : le code écrit contre cette API en 2015 se compile et se comporte identiquement aujourd'hui, et les chaînes que votre programme encode en 2026 se decoderont correctement sur n'importe quelle version de Go, passée ou future. Pour un format de sérialisation, c'est le super-pouvoir discret.

Choisir un alphabet pour la destination

L'encodage a une seule vraie décision, et c'est une question de voyage : où cette chaîne va-t-elle ? Go vous donne quatre encodeurs prêts à l'emploi, et chacun est réglé pour un canal différent :

Encodeur Alphabet Padding Envoyez-le là quand la chaîne voyage à travers
StdEncoding A-Z a-z 0-9 + / = corps JSON, parties MIME d'e-mail, data URLs, auth HTTP Basic, PEM, la plupart des API
URLEncoding A-Z a-z 0-9 - _ = chemins et requêtes d'URL, noms de fichiers, partout où + ou / devrait être échappé
RawStdEncoding A-Z a-z 0-9 + / aucun chaînes à alphabet standard compactes où le padding ne doit pas apparaître
RawURLEncoding A-Z a-z 0-9 - _ aucun segments de JWT, identifiants compacts, jetons embarqués dans des URL

Le raisonnement derrière les variantes est le raisonnement derrière le format lui-même. L'alphabet standard est ce que la MIME et la plupart des API attendent, alors c'est la valeur par défaut et la réponse sûre quand personne ne vous a dit le contraire. L'alphabet URL-safe existe parce que + et / sont des caractères réservés dans les URL : un plus dans une chaîne de requête est souvent lu comme un espace, et un slash commence un nouveau segment de chemin, alors le base64 standard dans une URL casse ou exige un échappement en percent sur les caractères qui portent +, / ou = - quelques pour cent d'un jeton typique. Les échanger contre - et _, légaux sans échappement dans les chemins, les requêtes et les noms de fichiers, c'est le correctif que le RFC 4648 a normalisé. Les variantes Raw jettent les signes d'égalité de fin entièrement, ce qui compte dans les contextes où le padding est soit interdit, soit tout simplement jamais utilisé, comme les segments de JWT. La règle qui vous épargne la plupart du débogage : l'encodeur que vous choisissez et le décodeur que l'autre côté utilise forment un seul contrat, et c'est la destination qui écrit ce contrat, pas vous.

Si un système avec qui vous parlez a défini un alphabet privé de 64 caractères, base64.NewEncoding("...64 chars...") vous fabrique un encodeur pour lui, et WithPadding(rune) vous laisse changer le caractère de padding ou le désactiver avec NoPadding. Les deux fonctions paniquent sur des arguments invalides (une longueur d'alphabet fausse, un caractère en double, un retour à la ligne dans l'alphabet, un caractère de padding en collision avec l'alphabet), alors construisez vos encodeurs personnalisés une fois, au démarrage, jamais dans un chemin chaud.

Le piège de Close

Voici le piège le plus célèbre de ce paquet, et il n'apparaît que quand vous encodez un flux au lieu d'une chaîne. NewEncoder enrobe n'importe quel io.Writer dans un Writer qui encode le base64, et comme le base64 travaille par blocs de trois octets d'entrée produisant quatre caractères de sortie, l'encodeur doit mettre en buffer vos dernier ou deux octets, en attendant de voir si d'autres arrivent. Ils ne sont vidés que quand vous fermez :

package main

import (
  "bytes"
  "encoding/base64"
  "fmt"
)

func main() {
  var buf bytes.Buffer
  enc := base64.NewEncoder(base64.StdEncoding, &buf)
  enc.Write([]byte("hello"))
  fmt.Println(buf.String()) // aGVs  -- ou est "lo" ?

  buf.Reset()
  enc = base64.NewEncoder(base64.StdEncoding, &buf)
  enc.Write([]byte("hello"))
  enc.Close()
  fmt.Println(buf.String()) // aGVsbG8=  -- l'encodage complet de "hello"
}

La première sortie est toute la leçon : sans Close, l'encodeur n'a émis que le premier bloc complet, trois octets de « hello » devenus « aGVs », et les deux octets restants ont tout simplement disparu dans le buffer interne. La deuxième sortie, après Close, est la chaîne correcte et complète. Le correctif est une habitude, pas une technique : au moment où vous créez un encodeur, créez aussi son nettoyage :

enc := base64.NewEncoder(base64.StdEncoding, w)
defer enc.Close() // pensez à vérifier l'erreur renvoyée dans un code de production

Deux détails rendent ce piège plus tranchant qu'il n'y paraît. D'abord, Close fait du vrai travail : il vide le bloc partiel en attente et il peut échouer, parce qu'il écrit vers le Writer sous-jacent, alors la version idiomatique vérifie son erreur, surtout quand la destination est un réseau ou un disque. Ensuite, la documentation dit que c'est une erreur d'appeler Write après Close, mais le temps d'exécution n'applique pas cette phrase. Si vous écrivez encore après avoir fermé, l'encodeur démarre silencieusement un bloc neuf et l'ajoute, produisant une chaîne avec du padding au milieu, ce qui est du base64 invalide que la plupart des décodeurs rejetteront avec un décalage confus. Le contrat, c'est à vous de le tenir.

L'enveloppage des lignes, façon Go

Toute autre grande implémentation base64 que vous ayez utilisée enveloppe sa sortie : la MIME veut des lignes d'au plus 76 caractères, le PEM en utilise 64, les clients e-mail du monde entier insèrent un CRLF de temps en temps. L'encodeur de Go ne fait rien de tout ça. Il émet une seule ligne continue, quelle que soit la taille du payload, et il le fait depuis la naissance du paquet. La sortie pour un mégaoctet de données est une seule ligne d'un mégaoctet et un tiers, du début à la fin, sans coupure.

C'est un choix délibéré, pas un oubli. Le format fonctionne identiquement avec ou sans les retours à la ligne, le propre décodeur de Go les saute n'importe où dans l'entrée, et un encodeur qui insérerait silencieusement des CRLF dans vos données surprendrait les programmes qui stockent la chaîne dans une colonne de base de données ou la comparent par égalité. Le prix est que vous devez envelopper vous-même quand le canal l'exige, et c'est un petit assistant :

package main

import (
  "bytes"
  "encoding/base64"
  "fmt"
)

func wrapAt(s string, width int) string {
  var out bytes.Buffer
  for i := 0; i < len(s); {
    end := i + width
    if end > len(s) {
      end = len(s)
    }
    out.WriteString(s[i:end])
    out.WriteByte('\n')
    i = end
  }
  return out.String()
}

func main() {
  raw := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x42}, 100))
  fmt.Print(wrapAt(raw, 76))
}

Une note sur le sens du voyage : parce que le décodeur de Go ignore les retours à la ligne n'importe où, une entrée enveloppée se décode parfaitement côté Go de n'importe quel pont. C'est dans l'autre sens qu'il faut faire attention : si vous envoyez une sortie enveloppée à un consommateur qui ne s'attend pas à des coupures (un champ JSON, une URL, un jeton), retirez-les d'abord, parce que ce consommateur peut traiter un retour à la ligne comme un caractère corrompu. Sachez dans quelle convention vit votre canal, et émettez-la exprès.

Empaqueter pour l'e-mail et la MIME

L'e-mail est le plus ancien foyer du base64. Le protocole SMTP original a été conçu pour transporter de l'ASCII 7 bits, alors les pièces jointes étaient encodées en base64 avant l'envoi et décodées à l'arrivée, et la norme MIME (RFC 2045) a formalisé la pratique : l'en-tête Content-Transfer-Encoding: base64 marque une partie, et le corps doit être coupé en lignes d'au plus 76 caractères avec des CRLF entre elles.

Le paquet net/smtp de Go envoie les octets que vous lui donnez, et il ne construira pas les parties MIME pour vous, alors dans un programme qui compose des e-mails, la partie base64 ressemble à ceci :

package main

import (
  "bytes"
  "encoding/base64"
  "fmt"
)

func main() {
  body := []byte("hi from Go")
  var part bytes.Buffer
  part.WriteString("Content-Transfer-Encoding: base64\r\n")
  part.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n")

  encoded := base64.StdEncoding.EncodeToString(body)
  for i := 0; i < len(encoded); i += 76 {
    end := i + 76
    if end > len(encoded) {
      end = len(encoded)
    }
    part.WriteString(encoded[i:end] + "\r\n")
  }
  fmt.Print(part.String())
}

Trois choses à remarquer. L'encodeur standard est le bon ici, parce que la MIME est le contexte d'alphabet standard original. Les retours à la ligne sont des CRLF, pas le retour à la ligne natif de la plateforme, parce que c'est ce que le RFC spécifie et ce que les analyseurs de messagerie attendent. Et si votre programme envoie de vrais e-mails en volume, une bibliothèque MIME maintenue construira le message entier pour vous ; le point de cet exemple est la moitié base64, c'est-à-dire la partie qui appartient à ce paquet. Ayez le bon alphabet et la bonne convention de lignes, et le reste de la MIME est le problème de quelqu'un d'autre.

Empaqueter des fichiers

Pour les fichiers qui rentrent en mémoire, le motif, c'est les mêmes deux lignes que partout ailleurs : lire, puis EncodeToString. Pour les fichiers qui n'y rentrent pas, le streaming garde votre mémoire plate, et la recette tient en un fichier, un encodeur, une copie, et deux fermetures dans le bon ordre :

in, err := os.Open("photo.jpg")
if err != nil {
  panic(err)
}
defer in.Close()

out, err := os.Create("photo.b64")
if err != nil {
  panic(err)
}
enc := base64.NewEncoder(base64.StdEncoding, out)
if _, err := io.Copy(enc, in); err != nil {
  panic(err)
}
if err := enc.Close(); err != nil {
  panic(err) // vide le dernier bloc partiel
}
if err := out.Close(); err != nil {
  panic(err)
}

L'ordre des fermetures est la partie subtile, et c'est la version fichier du piège de Close : l'encodeur doit être fermé avant le fichier, parce que c'est enc.Close qui écrit le dernier bloc partiel dans le fichier, et fermer le fichier d'abord laisserait ce bloc dans un buffer qui écrit dans le vide. Avec defer, rappelez-vous que les appels différés s'exécutent dans l'ordre inverse, alors enregistrer out.Close en premier et enc.Close en second (ou, comme dans l'exemple ci-dessus, fermer l'encodeur explicitement avant de différer le fichier), c'est ce qui rend la séquence sûre.

Gardez la taxe de taille dans un coin de la tête quand vous planifiez autour de ce motif : une photo de 10 méga-octets devient environ 13,3 méga-octets de texte, et une archive de 100 méga-octets devient une chaîne de 133 méga-octets sur disque. Si la destination a un quota, une limite ou un prix par octet, c'est la version base64 de votre fichier qui est comptée, pas l'original.

Empaqueter pour le web : data URLs

Les navigateurs chargent gaiement une image ou une police depuis une chaîne qui vit dans le HTML ou le CSS lui-même, et cette chaîne est une data URL : le type média, le drapeau ;base64, une virgule, et le payload, le tout dans une seule URL. Go n'a pas d'assistant pour les data URLs, mais en construire un tient en de la simple concaténation de chaînes, parce que le format est un contrat que vous pouvez voir écrit noir sur blanc :

package main

import (
  "fmt"
  "os"
  "encoding/base64"
)

func main() {
  img, err := os.ReadFile("logo.png")
  if err != nil {
    panic(err)
  }
  url := "data:image/png;base64," + base64.StdEncoding.EncodeToString(img)
  fmt.Println(url)
  // data:image/png;base64,iVBORw0KGgo...
}

Deux règles gardent les data URLs loin des ennuis. Incluez toujours le type média : il est optionnel dans la grammaire (la valeur par défaut est text/plain;charset=US-ASCII), mais un navigateur qui devine le type de votre payload binaire n'est pas un scénario que vous voulez. Et traitez les data URLs comme un tour pour petits actifs. Le RFC dit que le schéma n'est utile que pour de courtes valeurs, et c'est l'augmentation de 33 pour cent qui fait la différence entre une icône de 2 kilo-octets qui économise une requête et une photo de 5 méga-octets qui gonfle chaque chargement de page, sans cache pour la partager et sans URL à donner à quelqu'un. Icônes, favicons, petits sprites : oui. Photographies de produits : non.

Empaqueter pour HTTP

Trois contextes HTTP dominent le base64 dans les services Go, et deux d'entre eux viennent avec de l'aide intégrée. Le premier est le corps JSON, le cheval de trait : vous encodez une valeur avant la sérialisation, et le champ transporte une chaîne banale sur le fil :

package main

import (
  "encoding/base64"
  "encoding/json"
  "fmt"
)

type avatar struct {
  Data string `json:"data"`
}

func main() {
  png := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
  a := avatar{Data: base64.StdEncoding.EncodeToString(png)}
  body, err := json.Marshal(a)
  if err != nil {
    panic(err)
  }
  fmt.Println(string(body))
  // {"data":"iVBORw0KGgo="}
}

Si un type apparaît dans beaucoup d'endroits, le coup propre en Go est d'implémenter MarshalJSON et UnmarshalJSON dessus, pour que l'étape base64 soit invisible pour chaque site d'appel. Le deuxième contexte est l'authentification HTTP Basic, où la bibliothèque standard fait tout le travail : Request.SetBasicAuth(user, pass) construit l'en-tête Authorization pour vous, en passant l'encodeur standard sur la paire user:pass que le RFC 2617 spécifie. La seule règle là-bas est de ne pas improviser : l'auth Basic est du base64 standard avec un préfixe Basic , et un alphabet URL-safe ou un signe de padding manquant transformeront une connexion qui marche en un 401 que personne ne peut expliquer.

Le troisième contexte, ce sont les URL, où la chaîne est le payload d'un segment de chemin ou d'un paramètre de requête. Ici l'alphabet standard est un mauvais choix, parce que +, / et = entrent tous en collision avec la grammaire des URL, et que chaque occurrence doit être échappée en percent. Encodez avec la variante URL-safe à la place, et le jeton survit à l'URL intact. Si le consommateur l'échappe quand même en percent, rien n'est cassé, mais s'il ne le fait pas, vous vous êtes épargné une famille de 404.

Sortie URL-safe

Le base64 URL-safe mérite sa propre section en Go, parce que c'est la variante que vous utiliserez plus souvent que la standard, et parce que Go rend le changement gratuit. L'alphabet alternatif du RFC 4648 remplace + par - et / par _, alors la sortie n'a besoin d'aucun échappement dans les chemins, les requêtes ou les noms de fichiers d'URL, et elle se lit comme un jeton propre et unique dans une ligne de log. Les deux encodeurs prêts à l'emploi sont URLEncoding (avec padding) et RawURLEncoding (sans padding) :

raw := []byte{0xfb, 0x0f, 0x67, 0x01}
fmt.Println(base64.StdEncoding.EncodeToString(raw))     // +w9nAQ==
fmt.Println(base64.URLEncoding.EncodeToString(raw))     // -w9nAQ==
fmt.Println(base64.RawURLEncoding.EncodeToString(raw))  // -w9nAQ

Cette unique entrée, trois sorties : la version standard a besoin d'un échappement en percent pour son signe plus, la version URL-safe est un jeton, et la version brute jette même le padding. Les missions Go typiques de chacune : identifiants opaques qu'un service génère puis stocke dans des URL, des routes ou des noms de fichiers ; jetons d'API que les clients collent dans des chaînes de requête ; tout ce qui apparaîtra dans une ligne de log où un plus ou un slash est à un caractère d'être pris pour de la syntaxe.

La discipline qui garde ça propre est la même que partout dans cet article : la variante est un contrat avec le consommateur. Si l'autre côté attend du base64 standard et que vous envoyez du URL-safe, son décodeur échoue au premier tiret, et l'erreur sera un décalage d'octet près de la fin d'une chaîne parfaitement correcte, ce qui n'est pas une chose évidente à déboguer. En cas de doute, demandez ce que l'autre côté attend, lisez la spécification qu'elle pointe, et choisissez l'encodeur depuis la destination, pas depuis l'habitude.

Empaqueter des JWT

Les JSON Web Tokens sont le consommateur le plus visible du base64 dans les API modernes, et ils fixent la variante exacte : la sérialisation compacte JWS, d'après le RFC 7515, c'est trois segments base64url sans padding, joints par des points. En-tête, payload, signature. Cela veut dire que l'encodeur de choix pour tout ce que vous construisez à la main est RawURLEncoding :

package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/base64"
  "encoding/json"
  "fmt"
)

func main() {
  secret := []byte("hmac-secret")
  header, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
  payload, _ := json.Marshal(map[string]any{"sub": "1234567890"})

  signingInput := base64.RawURLEncoding.EncodeToString(header) + "." +
    base64.RawURLEncoding.EncodeToString(payload)

  mac := hmac.New(sha256.New, secret)
  mac.Write([]byte(signingInput))
  signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
  fmt.Println(signingInput + "." + signature)
}

Lisez cet exemple comme une leçon sur ce qu'est le format, pas comme une recommandation pour le livrer tel quel : il montre exactement où se trouve le base64 (deux fois avant la signature, une fois après) et pourquoi la signature couvre les segments encodés, pas le JSON brut. En production, signez et vérifiez avec une bibliothèque maintenue, parce que le JWT a une longue traîne d'erreurs (dérive d'horloge sur l'expiration, confusion d'algorithme, vérification d'audience manquante) que la couche base64 ne peut pas voir. La bibliothèque Go de facto est github.com/golang-jwt/jwt/v5, installée avec go get github.com/golang-jwt/jwt/v5 :

package main

import (
  "fmt"
  "log"
  "time"

  "github.com/golang-jwt/jwt/v5"
)

func main() {
  secret := []byte("hmac-secret")
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "sub": "1234567890",
    "exp": time.Now().Add(time.Hour).Unix(),
  })
  signed, err := token.SignedString(secret)
  if err != nil {
    log.Fatal("signing failed:", err)
  }
  fmt.Println(signed)
}

La bibliothèque effectue l'encodage base64url de chaque segment en interne, alors vous ne touchez jamais encoding/base64 du tout, et c'est le meilleur résultat possible : un endroit de moins où une erreur de padding ou d'alphabet peut se cacher. Et notez la garde qu'elle vous offre gratuitement : la v5 refuse les jetons qui prétendent alg=none à moins que vous n'optiez explicitement avec sa constante UnsafeAllowNoneSignatureType, c'est la protection que vous voulez sans y penser.

Texte, octets et Unicode

La position de Go sur cette question est la plus courte de tous les grands langages, et c'est la raison pour laquelle le base64 est si agréable ici : une string en Go est une séquence d'octets en lecture seule, et le texte de votre programme est de l'UTF-8. Il n'y a pas de couche d'encodage cachée, pas de surprise « la chaîne est en réalité de l'UTF-16 », et pas de drapeau de jeu de caractères à régler. Quand vous écrivez EncodeToString([]byte(myText)), vous encodez les octets UTF-8 du texte, point final :

s := "Café ☕"
packed := base64.StdEncoding.EncodeToString([]byte(s))
fmt.Println(packed) // Q2Fmw6kg4piV

Cette seule ligne est toute l'histoire pour le texte moderne, emojis et CJK compris : le base64 opère sur des octets, l'UTF-8 n'est qu'une séquence d'octets, et chaque décodeur de l'autre bout qui suit la même convention vous rendra la même chaîne. La conversion []byte(...) est une copie indépendante, que le compilateur élimine quand la tranche n'est que lue et ne s'échappe pas - alors en pratique elle ne coûte rien de mesurable.

Le seul cas où l'histoire s'allonge, c'est les données héritées : des octets produits par un système Windows-1252, Shift JIS ou ISO-8859-1 et qui ne sont pas de l'UTF-8 valide. Si vous base64-encodez ces octets tels quels, vous avez fidèlement transporté un texte cassé, ce qui n'est pas ce que quelqu'un voulait. Le correctif est de normaliser avant d'encoder, en utilisant golang.org/x/text, pour que la chaîne base64 porte de l'UTF-8 propre dès le moment où elle quitte votre programme :

import (
  "golang.org/x/text/encoding/charmap"
  "golang.org/x/text/transform"
)

legacy := []byte{0x43, 0x61, 0x66, 0xE9} // "Café" en Windows-1252
utf8, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), legacy)
if err != nil {
  panic(err)
}
packed := base64.StdEncoding.EncodeToString(utf8)
// Q2Fmw6k=  -- le même "Café", maintenant des octets UTF-8 propres prêts à voyager

Le même module couvre japanese, korean, simplifiedchinese et traditionalchinese en plus de charmap. La règle pratique : convertissez une fois, à la frontière où les octets hérités entrent dans votre programme, et de là tout ce que vous encodez est de l'UTF-8. Ne convertissez pas deux fois, ne devinez pas, et ne laissez jamais un payload non UTF-8 se glisser dans une chaîne base64 qu'un consommateur moderne va décoder et afficher.

Mesurer l'encodeur

L'encodeur est une consultation de table sans branchement sur l'entrée et sans allocation au-delà de la chaîne de sortie, et ça se voit dans les chiffres. Sur un CPU de bureau récent tournant Go 1.26, encoder 500 octets prend environ trois dixièmes de microseconde avec deux allocations, ce qui donne de l'ordre d'un gigaoctet et demi par seconde. Un mégaoctet de données s'encode en moins d'une milliseconde ; l'encodeur sera rarement quelque chose que vous puissiez sentir.

Le seul levier qui vaille le coup d'être connu est le profil d'allocation dans les boucles chaudes. EncodeToString alloue la chaîne de sortie à chaque appel, ce qui est le bon échange pour le cas des 99 pour cent. Si vous encodez des milliers de morceaux par seconde dans un buffer qui grandit, AppendEncode, ajouté dans Go 1.22, ajoute les octets encodés à une tranche que vous réutilisez et n'effectue aucune allocation en régime établi une fois que le buffer a grandi à sa taille :

var out []byte
for _, chunk := range chunks {
  out = base64.StdEncoding.AppendEncode(out, chunk)
}

Utilisez EncodeToString pour les coups isolés, AppendEncode pour les boucles serrées, et NewEncoder pour les flux et les fichiers. Celui que vous choisissez, rappelez-vous que le réseau ou le disque autour de l'encodeur est presque toujours la partie lente, alors profilez tout le chemin avant d'optimiser l'alphabet.

Considérations de sécurité

La phrase de sécurité la plus importante de cet article : le base64 n'est pas un chiffrement, et « on le base64 d'abord » n'est pas une mesure de sécurité. L'alphabet rend les données sûres pour le texte, pas secrètes, et n'importe qui muni des outils de développement d'un navigateur peut lire votre base64 en un instant. La confidentialité vient du TLS et du contrôle d'accès, et le travail du base64 est de faire passer des octets sur un canal texte seul sans les corrompre. Gardez ces deux métiers séparés dans votre design et dans votre documentation, et vous évitez le commentaire de revue classique « le mot de passe est protégé, regarde, c'est du base64 ».

La deuxième considération, c'est la taille. Parce que le format grossit d'un tiers, chaque limite de votre système a une version base64 : une API qui accepte 4 méga-octets de JSON accepte environ 3 méga-octets de données d'origine quand le payload est un champ base64, une URL avec un budget de longueur se raccourcit en octets bruts quand le jeton est URL-safe et sans padding, et une colonne de base de données dimensionnée pour la valeur brute peut être trop petite pour l'encodée. Faites l'arithmétique avec EncodedLen avant de stocker, d'envoyer ou de limiter, et rappelez-vous que l'augmentation porte sur l'entrée avec laquelle vous partez, pas sur la chaîne dont vous arrivez.

Troisièmement, réfléchissez à l'endroit où la chaîne encodée peut être observée. Les chaînes base64 sont amies des logs et amies des écrans, ce qui est une fonctionnalité, jusqu'à ce qu'une pièce jointe de 20 méga-octets devienne 26 méga-octets de texte que votre log d'accès enregistre consciencieusement à chaque requête. Journalisez la longueur, les premières dizaines de caractères et l'identifiant, pas le payload, et vos logs restent lisibles et votre disque reste en vie. Enfin, dans les URL, préférez la variante URL-safe pour que vos jetons ne gaspillent aucun de leurs caractères en échappements en percent, ce qui gonfle l'URL et dérange parfois une passerelle ou un proxy qui a une vue stricte de ce qui a sa place dans une chaîne de requête.

Faits amusants et particularités Go

Quelques faits spécifiques à ce paquet, pour les moments où vous voulez avoir raison dans une revue de code :

  • EncodeToString est le cheval de trait, et comme tous les points d'entrée d'encodage du paquet (Encode, AppendEncode), il n'a pas de renvoi d'erreur - encoder ne peut pas échouer en Go, ce qui est une liberté rare et discrète : n'importe quel octet est une entrée légale, et le seul moyen d'obtenir une mauvaise chaîne est de choisir le mauvais alphabet pour le canal.
  • EncodedLen est de la pure arithmétique, (n+2)/3*4 pour les encodages avec padding, calculée sans allocation ni boucle. Elle existe pour que vous puissiez dimensionner buffers et quotas sans jamais encoder un octet.
  • L'encodeur de flux interne cache un buffer d'entrée de 3 octets et un buffer de sortie de 1024 octets, c'est pourquoi NewEncoder écrit par morceaux et pourquoi le dernier bloc partiel ne peut sortir que par Close. Les buffers sont la raison du piège.
  • La documentation dit que c'est une erreur d'écrire après avoir appelé Close, mais le temps d'exécution n'applique pas la phrase. Un Write en retard est accepté, ajoute un bloc neuf, et produit une chaîne avec du padding au milieu : du base64 invalide, généré poliment, sans aucune valeur d'erreur en vue.
  • L'encodeur de Go n'a jamais enveloppé sa sortie à 76 caractères - comme les encodeurs de Python, de Java et de Node, l'encodeur de Go produit une seule ligne pour un mégaoctet de données. Votre assistant d'enveloppage MIME est un projet personnel, ce qui est aussi un bon moyen de se rappeler que les retours à la ligne du base64 dans l'e-mail sont une convention MIME, pas une exigence base64.
  • En août 2026, plus de 244 000 paquets publics sur pkg.go.dev importent encoding/base64. Quel que soit votre programme Go, il fait presque certainement du base64 quelque part, que vous le sachiez ou non.
  • La promesse de compatibilité Go 1 s'applique à ce paquet avec une force toute particulière : la sortie d'un programme qui a encodé une chaîne en 2013 est byte-identique sur Go 1.27 aujourd'hui. Les chaînes base64 sont, en Go, pratiquement immortelles.

Les erreurs qui ne cessent de resurgir

Les erreurs d'encodage qui ne cessent de resurgir dans les codebases Go, grosso modo dans l'ordre où elles arrivent :

  • Oublier Close sur l'encodeur de flux, et livrer une chaîne qui manque son dernier ou ses deux derniers octets. Le bug survit à tous les tests qui utilisent une entrée dont la longueur est un multiple de trois, c'est comme ça qu'il arrive en production.
  • Fermer le fichier avant l'encodeur, alors le dernier bloc partiel se vide dans un descripteur de fichier qui n'est plus là. La sortie est tronquée exactement du même montant, et l'erreur n'apparaît que sur les entrées de taille impaire.
  • S'attendre à des retours à la ligne de 76 caractères dans la sortie MIME ou e-mail et être perdu quand Go vous tend une seule longue ligne. L'enveloppage est une convention de canal, et en Go c'est à votre code de l'appliquer.
  • Utiliser l'alphabet standard à l'intérieur des URL, puis passer une après-midi à traquer des 404 et des 400 qui sont en réalité un problème d'encodage en percent. Si la chaîne va vivre dans une URL, partez de URLEncoding ou RawURLEncoding.
  • Émettre du padding là où le consommateur l'interdit : segments de JWT, certains formats de jetons, quelques analyseurs stricts. Les variantes brutes existent exactement pour ça, et le message d'erreur de l'autre côté est souvent un décalage d'octet à la toute fin de votre chaîne.
  • Encoder un secret en base64 et l'appeler protection. Ce n'est pas ça. L'en-tête, le jeton, le champ « chiffré » : lisible par n'importe qui en une demi-seconde. Utilisez le TLS, utilisez l'empreinte quand c'est une empreinte que le protocole veut, et laissez le base64 faire son unique travail honnête.
  • Oublier les 33 pour cent en réglant des limites : tailles de corps, largeurs de colonnes, budgets d'URL, vérifications de quota. L'arithmétique tient en un appel à EncodedLen, et le coût de la sauter est un 413 ou une colonne tronquée en production.
  • Encoder du texte qui n'est pas de l'UTF-8, ce qui transporte fidèlement la casse. Normalisez les jeux de caractères hérités avec golang.org/x/text avant d'encoder, pour que la chaîne base64 porte des octets propres.
  • Écrire vers l'encodeur après l'avoir fermé, par habitude ou depuis une boucle de nouvelle tentative. Aucune erreur n'est levée, et la sortie est silencieusement invalide.
  • Supposer que le décodeur de l'autre bout est aussi conciliant que celui de Go. Go saute les retours à la ligne n'importe où, mais d'autres langages et analyseurs sont plus stricts sur les caractères blancs et sur la longueur des lignes, alors alignez-vous sur la convention du canal, pas sur l'humeur du temps d'exécution de Go.

L'envers de la médaille

Voilà la moitié encodage de l'histoire : une méthode qui ne peut pas échouer, quatre encodeurs assortis aux canaux que leurs chaînes vont emprunter, un encodeur de flux avec un Close obligatoire, et un format qui grossit vos données d'un tiers et n'enveloppe jamais, jamais ses lignes. Choisissez l'alphabet depuis la destination, fermez vos encodeurs, faites l'arithmétique de taille d'avance, et le base64 en Go reste l'utilitaire discret et sans dépendance qu'il a été depuis 2009.

Et quand le trafic s'inverse, quand votre programme reçoit l'une de ces chaînes et doit l'ouvrir, l'article relié sur le décodage Base64 en Go couvre cette moitié en détail : les règles de tolérance du décodeur, les décalages d'erreur qui vous disent l'octet où l'entrée se gâte, le mode strict pour les protocoles pointilleux, et les mêmes quatre encodages vus dans l'autre direction.

Dernière mise à jour : 2026-09-08

Article associé : Décodage Base64 en Go : un guide complet