Codificación Base64 en Go: una guía completa
De vez en cuando tu programa Go tiene que entregar datos binarios a un mundo que solo acepta texto: un campo JSON que debe seguir siendo un string, una URL que debe seguir siendo un solo token, un adjunto de correo que cruza servidores que recuerdan los días de 7 bits, una imagen que quiere vivir dentro del HTML para que la página se salte una petición. Base64 es el mensajero perfecto para ese trabajo, y la página principal de este sitio ya explica el formato a fondo, así que este artículo va directo al oficio del empaquetado: producir cadenas base64 en Go que cualquier decodificador del planeta pueda abrir sin peleas.
La buena noticia primero: el codificador es la mitad amable de la historia. Un método, sin retorno de error, sin modo de fallo, salida idéntica byte a byte en cada versión de Go desde la primera estable. Todo el drama vive alrededor de ese método: elegir el alfabeto correcto para el canal por el que viajará la cadena, la llamada a Close que se traga en silencio tus dos últimos bytes cuando se te olvida, el impuesto de tamaño que lleva el formato, y el hecho de que Go, como Python, Java y Node, nunca dobla su salida en 76 caracteres. Primero conoce la función, y después conoce las trampas.
Empaquetar sin fallar
Noventa por ciento de la vida codificando en Go es un solo método del tipo Encoding, y como todos los puntos de entrada de codificación del paquete (Encode, AppendEncode), no tiene retorno de error:
func (enc *Encoding) EncodeToString(src []byte) string
Le pasas bytes, te da un string, y ese es todo el contrato:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
packed := base64.StdEncoding.EncodeToString([]byte("Man"))
fmt.Println(packed) // TWFu
}
No hay valor de error porque no hay nada que pueda fallar: cualquier byte es entrada legal, el alfabeto siempre lo cubre, y la salida siempre es ASCII puro. Tres propiedades merecen la pena memorizarlas, porque responden a la mitad de todas las preguntas futuras. Primera, la longitud de salida es una función aritmética pura de la longitud de entrada, y el paquete incluso te da la fórmula como método: EncodedLen(n) devuelve (n+2)/3*4 para codificaciones con padding, así que 3 bytes de entrada se convierten en 4 caracteres, 6 en 8, y así sucesivamente. Segunda, el formato lleva un impuesto de tamaño: cada tres bytes de datos vuelven como cuatro caracteres, que es la expansión familiar de aproximadamente 33 por ciento que aparece en tus facturas de ancho de banda y en tus cuotas de almacenamiento. Tercera, el método es determinista: los mismos bytes producen siempre la misma cadena, en cualquier máquina, en cualquier versión de Go, para siempre. Ese determinismo es lo que convierte a base64 en un formato de serialización en lugar de un misterio.
Una nota específica de Go en el lado de la entrada: el método recibe []byte, no string, y la conversión []byte(...) es explícita en cada punto de llamada - Go nunca convierte un string a slice por ti - y produce una copia independiente de los bytes del string. El compilador puede eliminar esa copia cuando el slice solo se lee y no escapa, que es por qué el costo suele ser inapreciable; pero si el slice se guarda o se devuelve, el runtime paga una copia real de O(n). El texto en un programa Go es UTF-8 por convención, así que cuando codificas un string estás codificando sus bytes UTF-8, y eso es exactamente lo que cada decodificador moderno del otro lado espera. Más sobre eso en la sección Texto, bytes y Unicode.
Cómo lo distribuye Go
Como todo en este artículo, el codificador viene del paquete de la biblioteca estándar encoding/base64, que se distribuye desde el primer lanzamiento del lenguaje y cuyo archivo de fuente todavía lleva su encabezado de copyright de 2009. No hay ningún módulo que descargar, ningún flag de característica que cambiar, y ninguna rareza de plataforma: si go version funciona, go doc encoding/base64 imprime la API entera por ti.
A fecha de escribir esto, la versión más nueva es Go 1.27.1, publicada el 1 de septiembre de 2026, con la línea Go 1.26 (actualmente 1.26.8) como la otra rama soportada. Instala Go desde los tarballs oficiales en go.dev/dl, desde el gestor de paquetes de tu distribución (sudo apt install golang-go), o a través del envoltorio golang.org/dl si equilibras versiones. La API de base64 es idéntica en las dos líneas soportadas, y la tabla de abajo es toda la historia de lo que alguna vez cambió, que es una lista corta para un paquete tan central:
| Versión | Año | Qué cambió en encoding/base64 |
|---|---|---|
| Go 1.0 | 2012 | Paquete estable desde el primer día; copyright del código fuente de 2009 |
| Go 1.5 | 2015 | Añadidos RawStdEncoding y RawURLEncoding para salida sin padding |
| Go 1.8 | 2017 | Añadido Strict() para decodificación canónica (lado decodificador) |
| Go 1.22 | 2024 | Añadidos AppendEncode y AppendDecode; WithPadding ahora rechaza argumentos malos |
| Go 1.27.1 | 2026 | Versión actual; API sin cambios, comportamiento estable a byte por la promesa de Go 1 |
La consecuencia práctica de esa historia: el código escrito contra esta API en 2015 compila y se comporta de forma idéntica hoy, y las cadenas que tu programa codifique en 2026 se decodificarán correctamente en cualquier versión de Go, pasada o futura. Para un formato de serialización, ese es el superpoder silencioso.
Elegir un alfabeto para el destino
Codificar tiene una decisión real, y es una cuestión de viaje: ¿a dónde irá esta cadena? Go te da cuatro codificadores ya preparados, y cada uno está afinado para un canal distinto:
| Codificador | Alfabeto | Padding | Envíalo ahí cuando la cadena viaje a través de |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
Cuerpos JSON, partes MIME de correo, data URLs, autenticación HTTP Basic, PEM, la mayoría de las APIs |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
Rutas y consultas de URL, nombres de archivo, cualquier lugar donde + o / necesitarían escape |
RawStdEncoding |
A-Z a-z 0-9 + / |
ninguno | Cadenas compactas del alfabeto estándar donde el padding no debe aparecer |
RawURLEncoding |
A-Z a-z 0-9 - _ |
ninguno | Segmentos de JWT, identificadores compactos, tokens incrustados en URLs |
El razonamiento detrás de las variantes es el razonamiento detrás del formato en sí. El alfabeto estándar es lo que esperan MIME y la mayoría de las APIs, así que es el valor por defecto y la respuesta segura cuando nadie te dijo lo contrario. El alfabeto URL-safe existe porque + y / son caracteres reservados en las URLs: un más en una cadena de consulta se lee a menudo como un espacio, y una barra empieza un nuevo segmento de ruta, así que el base64 estándar en una URL o se rompe o necesita escape de porcentaje en los caracteres que llevan +, / o = - un porcentaje o dos de un token típico. Cambiarlos por - y _, que son legales sin escape en rutas, consultas y nombres de archivo, es la solución que estandarizó RFC 4648. Las variantes Raw quitan los signos de igualdad finales por completo, lo cual importa en contextos donde el padding está prohibido o simplemente nunca se usa, como los segmentos de JWT. La regla que te salva de la mayoría del depurado: el codificador que eliges y el decodificador que usa el otro lado son un solo contrato, y el contrato lo escribe el destino, no tú.
Si un sistema con el que hablas definió un alfabeto privado de 64 caracteres, base64.NewEncoding("...64 chars...") te construye un codificador para él, y WithPadding(rune) te deja cambiar el carácter de padding o desactivarlo con NoPadding. Ambas funciones lanzan un pánico con argumentos inválidos (un alfabeto de longitud incorrecta, un carácter duplicado, un salto de línea en el alfabeto, un carácter de padding que colisiona con el alfabeto), así que construye tus codificadores personalizados una vez, al arrancar, nunca en una ruta caliente.
La trampa de Close
Aquí está la trampa más famosa de este paquete, y solo aparece cuando codificas un stream en lugar de un string. NewEncoder envuelve cualquier io.Writer en un escritor que codifica base64, y como base64 trabaja en bloques de tres bytes de entrada que producen cuatro caracteres de salida, el codificador tiene que poner en buffer tus últimos uno o dos bytes, esperando a ver si viene más. Solo salen cuando lo cierras:
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 -- ¿dónde está el "lo"?
buf.Reset()
enc = base64.NewEncoder(base64.StdEncoding, &buf)
enc.Write([]byte("hello"))
enc.Close()
fmt.Println(buf.String()) // aGVsbG8= -- la codificación completa de "hello"
}
La primera salida es toda la lección: sin Close, el codificador emitió solo el primer bloque completo, tres bytes de "hello" convirtiéndose en "aGVs", y los dos bytes restantes simplemente desaparecieron en el buffer interno. La segunda salida, después de Close, es la cadena correcta y completa. La solución es un hábito, no una técnica: en el momento en que creas un codificador, crea también su limpieza:
enc := base64.NewEncoder(base64.StdEncoding, w)
defer enc.Close() // recuerda comprobar el error devuelto en código de producción
Dos detalles hacen esta trampa más afilada de lo que parece. Primero, Close hace trabajo real: vuelca el bloque parcial pendiente y puede fallar, porque escribe en el escritor subyacente, así que la versión idiomática comprueba su error, especialmente cuando el destino es una red o un disco. Segundo, la documentación dice que es un error llamar a Write después de Close, pero el runtime no impone esa frase. Si escribes de nuevo después de cerrar, el codificador arranca en silencio un bloque fresco y lo añade, produciendo una cadena con padding en medio de ella, que es base64 inválido que la mayoría de los decodificadores rechazarán con un offset confuso. El contrato es tuyo de guardar.
La envoltura de líneas, a lo Go
Toda otra implementación importante de base64 que hayas usado dobla su salida: MIME quiere líneas de a lo sumo 76 caracteres, PEM usa 64, y los clientes de correo de todo el mundo insertan un CRLF de vez en cuando. El codificador de Go no hace nada de eso. Emite una sola línea continua, por grande que sea el payload, y lo ha hecho desde que el paquete nació. La salida para un megabyte de datos es una única línea de un megabyte y un tercio, de principio a fin, sin cortes.
Es una elección deliberada, no un descuido. El formato funciona idénticamente con o sin los saltos de línea, el propio decodificador de Go los salta en cualquier parte de la entrada, y un codificador que insertara CRLFs en silencio en tus datos sorprendería a los programas que guardan la cadena en una columna de base de datos o la comparan por igualdad. El costo es que tienes que doblar la línea tú cuando el canal lo exige, y eso es un pequeño auxiliar:
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))
}
Una nota sobre la dirección del viaje: como el decodificador de Go ignora los saltos de línea en cualquier parte, una entrada con saltos se decodifica perfectamente en el lado Go de cualquier puente. La otra dirección es donde hay que tener cuidado: si envías salida con saltos a un consumidor que no espera cortes (un campo JSON, una URL, un token), quítalos primero, porque ese consumidor puede tratar un salto de línea como un carácter corrupto. Sabe en qué convención vive tu canal, y emítela a propósito.
Empaquetar para correo y MIME
El correo es el hogar más antiguo de base64. El protocolo SMTP original fue diseñado para transportar ASCII de 7 bits, así que los adjuntos se codificaban en base64 antes de enviar y se decodificaban al llegar, y el estándar MIME (RFC 2045) formalizó la práctica: el encabezado Content-Transfer-Encoding: base64 marca una parte, y el cuerpo debe romperse en líneas de a lo sumo 76 caracteres con CRLF entre ellas.
El paquete net/smtp de Go envía los bytes que le das, y no va a construirte partes MIME, así que en un programa que compone correo la pieza base64 se ve así:
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())
}
Tres cosas a las que prestar atención. El codificador estándar es el correcto aquí, porque MIME es el contexto original del alfabeto estándar. Los saltos de línea son CRLF, no el salto de línea nativo de la plataforma, porque eso es lo que especifica el RFC y lo que esperan los analizadores de correo. Y si tu programa envía correo real a volumen, una biblioteca MIME mantenida te construirá el mensaje entero; el punto de este ejemplo es la mitad base64, que es la parte que le toca a este paquete. Acierta el alfabeto y la convención de línea, y el resto de MIME es problema de otro.
Empaquetar archivos
Para archivos que caben en memoria, el patrón son las mismas dos líneas de siempre: lee, y luego EncodeToString. Para archivos que no, el streaming mantiene tu memoria plana, y la receta es un archivo, un codificador, una copia, y dos cierres en el orden correcto:
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) // vuelca el bloque parcial final
}
if err := out.Close(); err != nil {
panic(err)
}
El orden de los cierres es la parte sutil, y es la versión de archivos de la trampa de Close: el codificador debe cerrarse antes que el archivo, porque enc.Close es lo que escribe el bloque parcial final en el archivo, y cerrar el archivo primero dejaría ese bloque en un buffer que escribe en la nada. Con defer, recuerda que las llamadas diferidas se ejecutan en orden inverso, así que registrar out.Close primero y enc.Close segundo (o, como en el ejemplo de arriba, cerrar el codificador explícitamente antes de diferir el archivo) es lo que hace la secuencia segura.
Mantén el impuesto de tamaño en la cabeza cuando planes alrededor de este patrón: una foto de 10 megabytes se convierte en aproximadamente 13,3 megabytes de texto, y un archivo de 100 megabytes se convierte en una cadena de 133 megabytes en disco. Si el destino tiene una cuota, un límite o un precio por byte, lo que se cuenta es la versión base64 de tu archivo, no el original.
Empaquetar para la web: data URLs
Los navegadores cargan gustosamente una imagen o una fuente desde una cadena que vive dentro del propio HTML o CSS, y esa cadena es una data URL: el tipo de medio, el flag ;base64, una coma, y el payload, todo en una URL. Go no tiene ningún auxiliar de data URLs, pero construir uno es concatenación de strings, porque el formato es un contrato que puedes ver escrito:
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...
}
Dos reglas mantienen las data URLs fuera de los líos. Incluye siempre el tipo de medio: es opcional en la gramática (el valor por defecto es text/plain;charset=US-ASCII), pero un navegador adivinando el tipo de tu payload binario no es un escenario que quieras. Y trata las data URLs como un truco de activo pequeño. El RFC dice que el esquema solo es útil para valores cortos, y la expansión del 33 por ciento es lo que marca la diferencia entre un icono de 2 kilobytes que ahorra una petición y una foto de 5 megabytes que hincha cada carga de página, sin ninguna caché que lo comparta y ninguna URL que darle a nadie. Iconos, favicons, sprites pequeños: sí. Fotografía de producto: no.
Empaquetar para HTTP
Tres contextos HTTP dominan base64 en los servicios Go, y dos de ellos traen ayuda integrada. El primero es el cuerpo JSON, el caballo de batalla: codificas un valor antes de marshalar, y el campo lleva un string plano por el cable:
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 tipo aparece en muchos sitios, la jugada limpia en Go es implementar MarshalJSON y UnmarshalJSON en él, para que el paso base64 sea invisible para cada punto de llamada. El segundo contexto es la autenticación HTTP Basic, donde la biblioteca estándar hace el trabajo entero: Request.SetBasicAuth(user, pass) construye el encabezado Authorization por ti, pasando el codificador estándar sobre el par user:pass que especifica RFC 2617. La única regla ahí es no improvisar: la autenticación Basic es base64 estándar con un prefijo Basic , y un alfabeto URL-safe o un signo de padding ausente convertirán un login funcional en un 401 que nadie puede explicar.
El tercer contexto son las URLs, donde la cadena es el payload de un segmento de ruta o un parámetro de consulta. Aquí el alfabeto estándar es una mala elección, porque +, / y = colisionan todos con la gramática de las URLs, y cada aparición de ellos necesita un escape de porcentaje. Codifica en su lugar con la variante URL-safe, y el token sobrevive la URL intacto. Si el consumidor aun así lo escapa con porcentaje, nada se rompe, pero si no lo hace, te has ahorrado una clase de 404.
Salida URL-safe
El base64 URL-safe merece su propia sección en Go porque es la variante a la que recurrirás con más frecuencia que a la estándar, y porque Go hace el cambio gratis. El alfabeto alternativo de RFC 4648 reemplaza + por - y / por _, así que la salida no necesita escape en rutas de URL, consultas o nombres de archivo, y se lee como un solo token limpio en una línea de log. Los dos codificadores ya preparados son URLEncoding (con padding) y RawURLEncoding (sin 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
Una entrada, tres salidas: la versión estándar necesita un escape de porcentaje para su signo más, la versión URL-safe es un solo token, y la versión raw tampoco lleva padding. Los trabajos típicos Go de cada una: identificadores opacos que un servicio genera y luego guarda en URLs, rutas o nombres de archivo; tokens de API que los clientes pegan en cadenas de consulta; cualquier cosa que aparezca en una línea de log donde un más o una barra están a un carácter de ser confundidos con sintaxis.
La disciplina que mantiene esto limpio es la misma que en todas partes en este artículo: la variante es un contrato con el consumidor. Si el otro lado espera base64 estándar y tú envías URL-safe, su decodificador falla en el primer guion, y el error será un offset de byte cerca del final de una cadena perfectamente buena, que no es algo evidente de depurar. Ante la duda, pregunta qué espera el otro lado, lee la especificación a la que apunta, y elige el codificador desde el destino, no desde el hábito.
Empaquetar JWT
Los JSON Web Tokens son el consumidor más visible de base64 en las APIs modernas, y fijan la variante exacta: la serialización compacta JWS, según RFC 7515, es tres segmentos base64url sin padding, unidos por puntos. Encabezado, payload, firma. Eso significa que el codificador de elección para lo que construyas a mano es 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)
}
Lee ese ejemplo como una lección de lo que es el formato, no como una recomendación de desplegarlo: muestra exactamente dónde se sienta el base64 (dos veces antes de firmar, una después) y por qué la firma cubre los segmentos codificados, no el JSON crudo. En producción, firma y verifica con una biblioteca mantenida, porque JWT tiene una larga cola de errores (desfase de reloj en la expiración, confusión de algoritmo, comprobaciones de audiencia ausentes) que la capa base64 no puede ver. La biblioteca Go de facto es github.com/golang-jwt/jwt/v5, instalada con 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 biblioteca realiza la codificación base64url de cada segmento internamente, así que nunca tocas encoding/base64 en absoluto, que es el mejor resultado: un sitio menos donde puede esconderse un error de padding o de alfabeto. Y fíjate en la guardia que te da de gratis: v5 rechaza tokens que afirman alg=none a menos que optes explícitamente con su constante UnsafeAllowNoneSignatureType, que es la protección que quieres sin tener que pensar en ella.
Texto, bytes y Unicode
La postura de Go sobre esta cuestión es la más corta de cualquier lenguaje importante, y es la razón de que base64 sea tan agradable aquí: un string en Go es una secuencia de bytes de solo lectura, y el texto en tu programa es UTF-8. No hay ninguna capa de codificación oculta, ninguna sorpresa de "el string en realidad es UTF-16", y ningún flag de charset que configurar. Cuando escribes EncodeToString([]byte(myText)), estás codificando los bytes UTF-8 del texto, punto:
s := "Café ☕"
packed := base64.StdEncoding.EncodeToString([]byte(s))
fmt.Println(packed) // Q2Fmw6kg4piV
Esa sola línea es la historia entera para el texto moderno, incluyendo emoji y CJK: base64 opera sobre bytes, UTF-8 es solo una secuencia de bytes, y cada decodificador del otro lado que siga la misma convención te dará la misma cadena de vuelta. La conversión []byte(...) es una copia independiente, que el compilador elimina cuando el slice solo se lee y no escapa - así que en la práctica no cuesta nada que puedas medir.
El único caso donde la historia se alarga son los datos legacy: bytes producidos por un sistema Windows-1252, Shift JIS o ISO-8859-1 que no son UTF-8 válido. Si codificas en base64 esos bytes tal cual, has transportado fielmente texto roto, que no es lo que nadie quería. La solución es normalizar antes de codificar, usando golang.org/x/text, para que la cadena base64 lleve UTF-8 limpio desde el momento en que sale de tu programa:
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= -- el mismo "Café", ahora bytes UTF-8 limpios listos para viajar
El mismo módulo cubre japanese, korean, simplifiedchinese y traditionalchinese además de charmap. La regla práctica: convierte una vez, en el límite donde los bytes legacy entran en tu programa, y a partir de entonces todo lo que codifiques es UTF-8. No conviertas dos veces, no adivines, y nunca dejes que un payload no UTF-8 se meta en una cadena base64 que un consumidor moderno decodificará y mostrará.
Midiendo el codificador
El codificador es una búsqueda en tabla sin ramas sobre la entrada y sin asignación más allá del string de salida, y se nota en los números. En una CPU de escritorio reciente corriendo Go 1.26, codificar 500 bytes toma aproximadamente tres décimas de un microsegundo con dos asignaciones, lo que se traduce en un gigabyte y medio por segundo, más o menos. Un megabyte de datos se codifica en mucho menos de un milisegundo; el codificador rara vez será algo que puedas notar.
La única palanca que merece la pena conocer es el perfil de asignaciones en bucles calientes. EncodeToString asigna el string de salida en cada llamada, que es el buen cambio para el caso del 99 por ciento. Si estás codificando miles de trozos por segundo en un buffer que crece, AppendEncode, añadido en Go 1.22, añade los bytes codificados a un slice que reutilizas y no realiza ninguna asignación en estado estable una vez que el buffer ha crecido al tamaño justo:
var out []byte
for _, chunk := range chunks {
out = base64.StdEncoding.AppendEncode(out, chunk)
}
Usa EncodeToString para trabajos sueltos, AppendEncode para bucles apretados, y NewEncoder para streams y archivos. Sea el que elijas, recuerda que la red o el disco que rodean al codificador son casi siempre la parte lenta, así que haz un perfil del camino entero antes de optimizar el alfabeto.
Consideraciones de seguridad
La frase de seguridad más importante de este artículo: base64 no es cifrado, y "base64 primero" no es una medida de seguridad. El alfabeto hace los datos seguros para texto, no secretos, y cualquiera con las herramientas de desarrollo de un navegador puede leer tu base64 en un instante. La confidencialidad viene de TLS y del control de acceso, y el trabajo de base64 es llevar bytes a través de un canal solo de texto sin corruptarlos. Mantén esos dos trabajos separados en tu diseño y en tu documentación, y evitas el clásico comentario de revisión de "la contraseña está protegida, mira, es base64".
La segunda consideración es el tamaño. Como el formato se expande en un tercio, cada límite en tu sistema tiene una versión base64: una API que acepta 4 megabytes de JSON acepta aproximadamente 3 megabytes de datos originales cuando el payload es un campo base64, una URL con presupuesto de longitud queda más corta en bytes crudos cuando el token es URL-safe y sin padding, y una columna de base de datos dimensionada para el valor crudo puede ser demasiado pequeña para el codificado. Haz la aritmética con EncodedLen antes de guardar, enviar o limitar, y recuerda que la expansión es sobre la entrada con la que empiezas, no sobre la cadena con la que terminas.
Tercera, piensa en dónde la cadena codificada puede ser observada. Las cadenas base64 son amigables con los logs y con la pantalla, que es una ventaja, hasta que un adjunto de 20 megabytes se base64ifica a 26 megabytes de texto que tu log de acceso registra cumplidamente en cada petición. Registra la longitud, los primeros cuarenta o cincuenta caracteres, y el identificador, no el payload, y mantienes tus logs legibles y tu disco vivo. Por fin, en las URLs, prefiere la variante URL-safe para que tus tokens no gasten ninguno de sus caracteres en escapes de porcentaje, que hinchan la URL y de vez en cuando enganchan a un gateway o un proxy con una visión estricta de lo que pertenece en una cadena de consulta.
Curiosidades y rarezas de Go
Unos hechos específicos de este paquete, para las veces que quieres tener la razón en una revisión de código:
EncodeToStringes el caballo de batalla, y como todos los puntos de entrada de codificación del paquete (Encode,AppendEncode), no tiene retorno de error - codificar no puede fallar en Go, que es una clase rara y silenciosa de libertad: cualquier byte es entrada legal, y la única forma de obtener una cadena mala es elegir el alfabeto equivocado para el canal.EncodedLenes aritmética pura,(n+2)/3*4para codificaciones con padding, calculada sin asignación y sin bucle. Existe para que puedas dimensionar buffers y cuotas sin codificar nunca un byte.- El codificador de stream interno esconde un buffer de entrada de 3 bytes y un buffer de salida de 1024 bytes, que es por qué
NewEncoderescribe por trozos y por qué el último bloque parcial solo puede salir porClose. Los buffers son la razón de la trampa. - La documentación dice que es un error escribir después de llamar a
Close, pero el runtime no impone la frase. UnWritetardío se acepta, añade un bloque fresco, y produce una cadena con padding en medio de ella: base64 inválido, generado sin protestar, sin ningún valor de error a la vista. - El codificador de Go nunca ha doblado la línea en su salida en 76 caracteres - como los codificadores de Python, Java y Node, el codificador de Go produce una línea para un megabyte de datos. Tu auxiliar de envoltura MIME es un proyecto personal, que también es una buena forma de recordar que los saltos de línea en el base64 de correo son una convención MIME, no un requisito de base64.
- A fecha de agosto de 2026, más de 244.000 paquetes públicos en pkg.go.dev importan
encoding/base64. Sea lo que sea tu programa Go, casi con seguridad está haciendo base64 en algún sitio, lo sepas o no. - La promesa de compatibilidad de Go 1 se aplica a este paquete con fuerza especial: la salida de un programa que codificó un string en 2013 es idéntica a byte en Go 1.27 hoy. Las cadenas base64 son, en Go, efectivamente inmortales.
Los errores que no paran de reaparecer
Los errores de codificación que no paran de reaparecer en codebases Go, más o menos en el orden en que llegan:
- Olvidar
Closeen el codificador de stream, y desplegar una cadena a la que le faltan sus últimos uno o dos bytes. El bug sobrevive a cada prueba que usa entrada cuya longitud es múltiplo de tres, y así es como llega a producción. - Cerrar el archivo antes que el codificador, para que el bloque parcial final se vuelque en un manejador de archivo que ya se fue. La salida queda truncada exactamente la misma cantidad, y el error solo aparece con entradas de tamaño raro.
- Esperar saltos de línea de 76 caracteres en la salida MIME o de correo y quedarse confuso cuando Go te entrega una línea larga. El salto de línea es una convención del canal, y en Go es trabajo de tu código aplicarla.
- Usar el alfabeto estándar dentro de URLs, y luego pasar una tarde persiguiendo 404 y 400 que en realidad son un problema de codificación por porcentaje. Si la cadena vivirá en una URL, parte de
URLEncodingoRawURLEncoding. - Emitir padding donde el consumidor lo prohíbe: segmentos de JWT, algunos formatos de token, algunos analizadores estrictos. Las variantes raw existen exactamente por esa razón, y el mensaje de error del otro lado a menudo es un offset de byte justo al final de tu cadena.
- Codificar en base64 un secreto y llamarlo protección. No lo es. El encabezado, el token, el campo "cifrado": legible por cualquiera en medio segundo. Usa TLS, usa hashing donde un hash es lo que el protocolo quiere, y deja que base64 haga su único trabajo honesto.
- Olvidar el 33 por ciento al poner límites: tamaños de cuerpo, anchos de columna, presupuestos de URL, comprobaciones de cuota. La aritmética es una llamada a
EncodedLen, y el costo de saltársela es un 413 o una columna truncada en producción. - Codificar texto que no es UTF-8, que transporta fielmente el desastre. Normaliza charsets legacy con
golang.org/x/textantes de codificar, para que la cadena base64 lleve bytes limpios. - Escribir en el codificador después de cerrarlo, por hábito o desde un bucle de reintentos. No se lanza ningún error, y la salida es inválida en silencio.
- Dar por sentado que el decodificador del otro lado es tan indulgente como el de Go. Go salta los saltos de línea en cualquier parte, pero otros lenguajes y analizadores son más estrictos con el espacio en blanco y con la longitud de línea, así que ajusta la convención del canal en lugar del humor del runtime Go.
El otro lado
Esa es la parte de codificación de la historia: un método que no puede fallar, cuatro codificadores acoplados a los canales por los que viajarán sus cadenas, un codificador de stream con un Close obligatorio, y un formato que expande tus datos en un tercio y nunca, nunca dobla la línea. Elige el alfabeto desde el destino, cierra tus codificadores, haz la aritmética de tamaño por adelantado, y base64 en Go se mantiene la utilidad silenciosa y de cero dependencias que ha sido desde 2009.
Y cuando el tráfico se invierte, cuando tu programa recibe una de estas cadenas y tiene que abrirla, el artículo relacionado sobre decodificación Base64 en Go cubre ese lado en detalle: las reglas de tolerancia del decodificador, los offsets de error que te dicen el byte donde la entrada se tuerce, el modo estricto para protocolos caprichosos, y las mismas cuatro codificaciones desde la otra dirección.
Última actualización: 2026-09-08
Artículo relacionado: Decodificación Base64 en Go: una guía completa