Decodificação Base64 em Go: um guia completo
Há uma string longa escondida numa resposta de API, fingindo ser um valor quando na verdade é um arquivo, um token, uma imagem ou uma mensagem de um sistema três anos mais velho que o seu. Em algum lugar das suas linhas de log, linhas de banco de dados e payloads JSON, strings base64 aparecem o tempo todo: o alfabeto padrão de 64 letras, às vezes com um mais e uma barra, às vezes com um hífen e um underscore, e de vez em quando dois sinais de igual estacionados no final como uma assinatura.
A página inicial deste site já explica o formato em si: 64 caracteres imprimíveis carregando 6 bits cada, quatro caracteres por três bytes de entrada, e padding para fechar o trabalho. Então este artigo vai direto para a metade do trabalho onde as decisões interessantes moram: abrir essas strings em Go. A boa notícia é que Go é um lugar maravilhoso para fazer isso. Um pacote da biblioteca padrão, zero dependências, um decodificador estrito por padrão mas tolerante com quebras de linha, e mensagens de erro que apontam para o byte exato que deu errado.
O que vem com o Go
Tudo o que você precisa já está na biblioteca padrão. O pacote se chama encoding/base64, o arquivo de origem dele ainda veste um cabeçalho de copyright de 2009, do ano em que a linguagem nasceu, e não há extensão para ativar, módulo para baixar ou configuração para virar. Se go version imprime qualquer coisa na sua máquina, você já é dono da ferramenta inteira.
Até o momento desta escrita, o lançamento mais novo é o Go 1.27.1, lançado em 1 de setembro de 2026, com a linha Go 1.26 (atualmente 1.26.8) como a outra trilha suportada. A API de base64 é idêntica nas duas, e por causa da promessa de compatibilidade do Go 1, um programa que decodifica base64 hoje vai continuar fazendo exatamente a mesma coisa em todo lançamento futuro. Pegue o Go em si nos tarballs oficiais do go.dev/dl (algo como go1.27.1.linux-amd64.tar.gz, extraído para /usr/local), no gerenciador de pacotes da sua distribuição (sudo apt install golang-go em sistemas baseados em Ubuntu), ou via o wrapper golang.org/dl se você gosta de várias versões do Go lado a lado.
Com o Go instalado, go doc encoding/base64 imprime a API inteira numa coluna legível, o que é o jeito mais rápido de refrescar a memória. O único add-on que este artigo usa em algum lugar é o golang.org/x/text para charsets legados, instalado com go get golang.org/x/text. Ele aparece uma vez, na própria seção, e o resto é biblioteca padrão pura.
Sua primeira decodificação
Noventa por cento da vida de decodificação em Go é um método do tipo Encoding:
func (enc *Encoding) DecodeString(s string) ([]byte, error)
Dê a ele uma string base64 e ele devolve os bytes que ela representa, mais um erro quando a entrada se comporta 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
}
Dois detalhes dessa assinatura merecem ser memorizados. Primeiro, o resultado é um []byte, não uma string, porque os bytes que você desembrulha podem ser base64 perfeitamente válido e texto perfeitamente terrível: um cabeçalho PNG, um arquivo comprimido, um protocolo binário. Envolva em string(...) só quando você sabe que o payload é texto. Segundo, o método sempre devolve dois valores. Um erro nil significa que a string era base64 limpo; um erro não-nil significa que a entrada estava quebrada em algum lugar, e o slice de bytes que você recebeu pode ser um resultado parcial em vez de um vazio. Você vai ver os dois lados desse comportamento na seção de erros abaixo.
Quatro decodificadores, uma pergunta: qual alfabeto?
O Go embarca quatro valores Encoding prontos, e escolher o certo é a primeira decisão de verdade em toda decodificação. A tabela abaixo é o mapa de lugares:
| Variável | Alfabeto | Padding | Onde você vai encontrar |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
e-mail MIME, data URLs, HTTP Basic auth, arquivos PEM, JSON em geral |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
caminhos e queries de URL, nomes de arquivo |
RawStdEncoding |
A-Z a-z 0-9 + / |
nenhum | base64 padrão sem padding de produtores compactos |
RawURLEncoding |
A-Z a-z 0-9 - _ |
nenhum | segmentos de JWT, identificadores compactos de API |
O jeito mais rápido de escolher é olhar para os dados em si. Uma string contendo + ou / só pode ser uma string de alfabeto padrão, então precisa de um dos dois decodificadores Std. Uma string contendo - ou _ é a variante URL-safe do RFC 4648, então precisa de um dos dois decodificadores URL. Depois confira o final: caracteres = no fim significam a variante com padding, e a ausência deles significa a variante Raw. Aqui está como é a sensação da escolha errada:
decoded, err := base64.StdEncoding.DecodeString("P29_")
// err: illegal base64 data at input byte 3
// o underscore não está no alfabeto padrão, então o
// decodificador para no último caractere que não reconhece
decoded, err = base64.URLEncoding.DecodeString("P29_")
// decoded são os três bytes 0x3f 0x6f 0x7f, err é nil
Se você está decodificando dados de um produtor que definiu seu próprio alfabeto de 64 caracteres, base64.NewEncoding("...64 chars...") monta um decodificador para ele. O alfabeto deve ter exatamente 64 valores de byte únicos e não pode conter quebra de linha - a função entra em pânico caso contrário - enquanto a documentação exige que o alfabeto exclua o caractere de padding, a função não aplica isso - um alfabeto contendo '=' é aceito sem pânico. No trabalho do dia a dia você raramente vai precisar, mas ele está lá, e é o único jeito de decodificar um esquema privado.
O problema da tolerância: que entrada o Go aceita?
Todo decodificador base64 tem que tomar uma decisão desconfortável: quanto lixo ele está disposto a engolir? A resposta do Go é uma linha cuidadosamente traçada. No lado tolerante, o decodificador pula carriage returns e line feeds em qualquer lugar da entrada, então uma string que foi quebrada em várias linhas por um cliente de e-mail ou uma ferramenta PEM decodifica sem nenhum pré-processamento:
decoded, err := base64.StdEncoding.DecodeString("T\nW\nF\r\nu")
// decoded é "Man", err é nil
// todo \r e \n na string foi simplesmente ignorado
No lado estrito, tudo o mais é proibido. Um espaço, um tab, um caractere de largura zero copiado de um PDF, dois-pontos perdidos de um cabeçalho: no momento em que o decodificador encontra um caractere que não está no alfabeto e não é quebra de linha, ele para e reporta o offset. E ele guarda tudo o que já decodificou:
decoded, err := base64.StdEncoding.DecodeString("TWFu junk")
// decoded é "Man" (a parte antes do espaço),
// err é: illegal base64 data at input byte 4
Essa combinação surpreende as pessoas: uma decodificação falha ainda pode te entregar um meio-resultado utilizável. Se isso é um recurso ou um perigo depende de você; o ponto é que err == nil é a única condição em que os dados estão completos.
Padding tem as próprias regras, e elas diferem entre as variantes com padding e as raw. Os decodificadores com padding trabalham em grupos: um grupo ou tem quatro caracteres de verdade, ou tem dois caracteres de verdade seguidos por ==. Um caractere sozinho nunca é um grupo completo, então "T" falha, e "TWF" também falha, porque três caracteres precisam de um sinal de padding que está faltando. Os decodificadores raw dispensam o requisito de padding, mas ainda não aceitam um comprimento em que um grupo ficaria faltando três dos seus quatro caracteres, então "T" também falha ali, enquanto "TW" decodifica feliz para um único byte.
base64.StdEncoding.DecodeString("T") // erro no byte 0 da entrada
base64.StdEncoding.DecodeString("TWF") // erro no byte 0 da entrada
base64.RawStdEncoding.DecodeString("TW") // 1 byte, sem erro
base64.StdEncoding.DecodeString("TWFu====") // "Man" mais erro no byte 4
Existe mais um interruptor de humor: Strict(), adicionado no Go 1.8. No modo estrito, o decodificador aplica a forma canônica da seção 3.5 do RFC 4648: os bits de fim não usados do grupo final devem ser zero. O modo normal não se importa, porque esses bits simplesmente nunca são usados, então "Qm==" decodifica para o byte B sem reclamar. O modo estrito recusa em vez disso:
decoded, err := base64.StdEncoding.DecodeString("Qm==")
// decoded é "B", err é nil (os bits de fim foram descartados)
decoded, err = base64.StdEncoding.Strict().DecodeString("Qm==")
// err é: illegal base64 data at input byte 2
Note que mesmo no modo estrito as quebras de linha continuam sendo puladas, como a documentação aponta. Use Strict() quando você estiver falando um protocolo que se importa com codificação canônica, ou quando quiser rejeitar produtores descuidados em vez de absorver os bits deles em silêncio.
Erros que dizem onde
Toda falha neste pacote chega como um valor concreto e inspecionável. Quando a entrada contém algo que o alfabeto não conhece, ou o padding está errado, o decodificador devolve um base64.CorruptInputError, e a mensagem dele inclui o offset do byte do problema:
type CorruptInputError int64
func (e CorruptInputError) Error() string {
return "illegal base64 data at input byte " + strconv.FormatInt(int64(e), 10)
}
Esse offset é a diferença entre "algo falhou" e "o caractere 4.102 desta string de 900 quilobytes é um tab que vagou para cá de uma área de transferência". Pegue com o idioma usual do Go:
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)
}
Aqui está o quadro de sintomas das entradas que mais confundem as pessoas:
| Entrada (StdEncoding) | Resultado | Por quê |
|---|---|---|
TWF$ |
erro no byte 3 | $ não está no alfabeto |
T |
erro no byte 0 | um caractere nunca é um grupo completo |
TWF |
erro no byte 0 | três caracteres precisam de um = que está faltando |
TWFu junk |
Man mais erro no byte 4 |
espaço não é quebra de linha, então a decodificação para ali |
TWFu\t |
Man mais erro no byte 4 |
tabs não são pulados, apenas \r e \n |
T\nW\nF\nu |
Man, sem erro |
quebras de linha são ignoradas em qualquer lugar |
==== |
erro no byte 0 | padding no começo de um grupo não é válido |
(string vazia) |
resultado vazio, sem erro | zero bytes de base64 decodificam para zero bytes |
Uma dica prática: quando uma decodificação falha em produção, registre o offset e uma janela curta ao redor dele. Noventa por cento das vezes o byte "corrompido" é espaço em branco que o transporte, a área de transferência ou um leitor de PDF enfiou na string, e a correção é um trim ou um strip, não um redesign.
Abrindo arquivos
Arquivos base64 são só arquivos de texto que contêm base64, então as ferramentas de arquivo usuais do Go se aplicam. Para um arquivo que cabe confortavelmente na memória, leia tudo e decodifique a string:
package main
import (
"encoding/base64"
"fmt"
"io"
"os"
)
func main() {
f, err := os.Open("payload.b64")
if err != nil {
fmt.Println("open failed:", err)
return
}
defer f.Close()
raw, err := io.ReadAll(f)
if err != nil {
fmt.Println("read failed:", err)
return
}
decoded, err := base64.StdEncoding.DecodeString(string(raw))
if err != nil {
fmt.Println("decode failed:", err)
return
}
fmt.Println("decoded", len(decoded), "bytes")
}
Para arquivos grandes, o padrão melhor é streaming, e ele usa a outra metade da API do pacote: NewDecoder envolve qualquer io.Reader num leitor que decodifica base64, então você pode passar arquivo para arquivo sem nunca segurar o payload inteiro na memória:
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")
Existe uma opção intermediária quando você quer evitar a alocação extra que o DecodeString faz: o Decode escreve num buffer de destino que você controla. Dimensione com DecodedLen, que devolve o número máximo de bytes de saída para um comprimento de entrada dado:
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] // o tamanho decodificado real
fmt.Println(len(data), "bytes")
Mas tenha cuidado com essa última: o Decode confia em você para dimensionar o buffer. Se for pequeno demais, o método não devolve erro; entra em pânico com índice fora do alcance. DecodedLen é o número a usar, não len(raw).
Uma linha de comando base64 para o Go
Sistemas Unix embarcam um utilitário base64 com o coreutils, e o Go não embarca binário equivalente. A resposta idiomática no mundo Go não é um pacote que você instala, mas um programa que você possui: uma pequena ferramenta de linha de comando construída em volta de encoding/base64, do pacote flag e da entrada padrão. Aqui está uma completa, uns quarenta linhas, que decodifica o que for passado por pipe e escreve os bytes crus por fora:
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)
}
Compile uma vez com go build -o b64 . e ele vira um decodificador multiplataforma que você pode soltar num Makefile, num pipeline de CI ou numa função de shell: printf 'TWFu' | ./b64 imprime Man, e ./b64 -url < token.b64 > token.bin desembrulha um token URL-safe para um arquivo. Duas propriedades do design merecem nota. Como ele lê o stdin inteiro antes de decodificar, entrada quebrada com quebras de linha decodifica sem problema, graças à tolerância do decodificador a quebras de linha. E como ele sai com status 1 em entrada inválida e escreve a reclamação no stderr, ele se comporta como uma ferramenta num pipeline em vez de um script que se desculpa. Essa é a arte inteira de um CLI em Go: um pacote, um flag, entrada padrão, saída padrão e um código de saída.
Decodificação URL-safe
A variante URL-safe existe porque o alfabeto padrão colide com a gramática de URLs: + é frequentemente lido como espaço em query strings, e / começa um novo segmento de caminho, então uma string base64 padrão embutida numa URL precisa ser percent-escaped caractere por caractere, o que é lento de parsear e feio de ler. O alfabeto alternativo do RFC 4648 troca + e / por - e _, ambos legais sem escape em caminhos de URL, queries e nomes de arquivo.
Em Go, a troca é só uma variável de decodificador diferente. Se seus dados são URL-safe com padding, use URLEncoding; se são URL-safe sem padding, use RawURLEncoding. O caso clássico é um identificador que mora numa URL ou num nome de arquivo:
decoded, err := base64.RawURLEncoding.DecodeString("-w9n")
// decoded são os três bytes 0xfb 0x0f 0x67
// o hífen e o underscore fazem parte do alfabeto URL-safe,
// então o RawURLEncoding lida com eles onde o StdEncoding falharia
Onde você vai encontrar no código Go de verdade: segmentos de JWT (coberto a seguir), identificadores opacos que sistemas geram e guardam em URLs, nomes de arquivo que não podem quebrar um servidor web ou um object store em nuvem, e qualquer API que prometeu "base64url" na documentação. Um aviso: URL-safe é um contrato entre produtor e consumidor, não uma propriedade dos dados. Se a string contém um + ou um /, ela não é URL-safe, ponto final, e nenhuma quantidade de tentativas com o decodificador URL vai ajudar. Olhe para os caracteres primeiro, depois escolha o decodificador.
Espiando dentro de JWTs
Um JSON Web Token é três segmentos base64url separados por pontos: um header, um payload de claims e uma assinatura, sem padding em nenhum dos segmentos. Isso faz do JWT uma das coisas mais comuns que você vai decodificar em Go, e o header e o payload são legíveis sem nenhuma chave, o que vale a pena lembrar tanto para debugging quanto para revisões de segurança:
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"}
}
Repare na escolha do decodificador: RawURLEncoding, não StdEncoding. Segmentos de JWT usam o alfabeto URL-safe e não trazem padding, e um segmento cujo comprimento fica um ou dois a menos que um múltiplo de quatro vai falhar num decodificador com padding bem no final, o que é um erro confuso de rastrear. O segmento de assinatura você não consegue ler sem a chave, e não deve tentar confiar em nada com base só no payload, porque nada impede um cliente de forjar os dois primeiros segmentos. Quando precisar de verificação, use uma biblioteca mantida. A de fato é github.com/golang-jwt/jwt/v5 (instale com 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"])
}
Dois detalhes da biblioteca valem a pena conhecer. Primeiro, a codificação e a decodificação base64url dos três segmentos são tratadas internamente, então você nunca toca encoding/base64 diretamente quando assina ou verifica. Segundo, a biblioteca v5 recusa tokens com alg=none a menos que você passe explicitamente a constante UnsafeAllowNoneSignatureType dela, o que te protege do erro clássico de "token sem assinatura aceito".
Data URLs
Uma data URL é uma URL cujo payload é o próprio dado. A sintaxe, do RFC 2397, é data:[mediatype][;base64],data: um media type opcional, uma flag ;base64 opcional, uma vírgula e então o conteúdo. Quando a flag ;base64 está presente, o conteúdo é base64 padrão, é por isso que data URLs e este artigo compartilham uma seção. Navegadores usam para embutir imagens e fontes diretamente no HTML e no CSS, para a página precisar de uma requisição a menos:
<img src="data:image/png;base64,iVBORw0KGgo=" alt="pixel">
A biblioteca padrão do Go não tem helper de data URL, mas o formato é simples o bastante para parsear à mão com strings, o que a maioria dos programas Go faz:
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")
}
Três armadilhas para ter em mente. Primeiro, a flag ;base64 é opcional, e sem ele o payload é ASCII percent-encoded em vez de base64, então confira o sufixo antes de chamar um decodificador. Segundo, quando o media type é omitido, o padrão é text/plain;charset=US-ASCII, o que raramente importa para imagens, mas surpreende quem parseia outros conteúdos. Terceiro, data URLs são um truque de payload pequeno: o próprio RFC diz que o scheme só é útil para valores curtos, e a expansão de tamanho de 33 por cento do base64 transforma um logo de 500 quilobytes numa string de 666 quilobytes colada no seu HTML, sem cache e sem como compartilhar. Use para ícones e thumbnails, não para vídeos.
Trabalho com HTTP e APIs
A decodificação mais comum em serviços web Go é o campo do corpo JSON: um formulário de upload, uma resposta de API ou um webhook te entrega uma string que na verdade é um arquivo. Faça unmarshal num struct e depois decodifique o campo:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
)
type payload struct {
Avatar string `json:"avatar"`
}
func main() {
body := []byte(`{"avatar": "iVBORw0KGgo="}`)
var p payload
if err := json.Unmarshal(body, &p); err != nil {
fmt.Println("bad JSON:", err)
return
}
img, err := base64.StdEncoding.DecodeString(p.Avatar)
if err != nil {
fmt.Println("bad avatar:", err)
return
}
fmt.Println("avatar is", len(img), "bytes")
}
Se sua API aceita strings tanto padrão quanto URL-safe, o padrão pragmático é tentar um decodificador, e se ele falhar com um CorruptInputError perto do final, tentar o outro antes de desistir. Não faça essa dança mais de uma vez, e nunca recorra a "tirar os sinais de igual e torcer" como estratégia geral.
Para autenticação HTTP Basic, você não decodifica nada, porque o Go faz por você. Request.BasicAuth, disponível desde o Go 1.4, divide o cabeçalho Authorization por você e devolve o nome de usuário e a senha, depois de já ter rodado o decodificador base64 padrão no par user:pass definido pelo 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)
}
Lembre que Basic auth é autenticação, não proteção: o cabeçalho é base64, não criptografado, então só deve viajar sobre HTTPS. Se você é o cliente, a chamada espelho é req.SetBasicAuth(user, pass), que monta o mesmo cabeçalho para você com o codificador padrão.
Um hábito defensivo para handlers de API: limite o corpo antes de decodificá-lo, com http.MaxBytesReader ou uma verificação de comprimento equivalente. Uma string base64 decodifica para mais ou menos três quartos do próprio comprimento, então um limite de corpo de N bytes mantém o resultado decodificado abaixo de N bytes, e a memória fica limitada não importa o que um cliente malicioso poste. Decodificar um corpo sem limite é um vetor clássico de esgotamento de memória, porque o atacante controla quantos megabytes de texto pode transformar em binário.
Charsets legados
Decodificar base64 te dá bytes, e em sistemas modernos esses bytes quase sempre são UTF-8, caso em que string(decoded) é a história inteira. Mas base64 é um formato antigo e muita coisa foi produzida por sistemas que usavam Windows-1252, ISO-8859-1, Shift JIS ou outro charset legado de byte único ou duplo. Se o produtor fez isso, os bytes que você decodifica não são UTF-8 válido, e o Go não vai fingir que são: ele vai te mostrar caracteres de substituição onde a sequência estiver quebrada.
A resposta do Go é o módulo golang.org/x/text, que transforma bytes codificados em legacy para UTF-8 (e de volta) para os charsets comuns. O lugar da conversão é logo depois da decodificação, e custa uma chamada de função:
package main
import (
"encoding/base64"
"fmt"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
func main() {
// "Café" armazenado como Windows-1252 por uma ferramenta legada,
// depois codificado em base64 para transporte
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é
}
O módulo tem um subpacote por família de charset: charmap para as tabelas de byte único do Windows e ISO, japanese para Shift JIS e EUC-JP, korean para EUC-KR, simplifiedchinese para GB18030 e traditionalchinese para Big5. A regra prática é converter só quando você de fato conhece o charset do produtor, porque converter bytes UTF-8 uma segunda vez não falha com estrondo; só estraga o texto. Na dúvida, trate o payload como bytes e deixe o consumidor de baixo decidir.
Streaming e decodificações em chunks
Você viu NewDecoder na seção de arquivos; aqui está o que torna ele digno de uma seção própria. Ele é um adaptador de streaming de verdade: puxa do leitor subjacente só o que precisa, decodifica no lugar e devolve um CorruptInputError no momento em que o stream estraga. O stream inteiro pode ser terabytes; a memória que você segura é o seu buffer e a saída que você escreve. Os dois padrões de consumo comuns são io.ReadAll para streams pequenos e io.Copy para o resto:
small, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, r))
// ótimo para um blob de config ou um anexo pequeno
w, err := io.Copy(out, base64.NewDecoder(base64.StdEncoding, r))
// ótimo para um vídeo, um tarball ou um job de restore
Desde o Go 1.22 o pacote também tem AppendDecode, que decodifica num buffer que você reutiliza em vez de alocar um slice novo a cada chamada. É a ferramenta para caminhos quentes que decodificam muitos chunks num loop, como um processador de linhas ou um decodificador de protocolo:
var buf []byte
for _, chunk := range chunks {
buf, err = base64.StdEncoding.AppendDecode(buf, chunk)
if err != nil {
return err
}
process(buf)
}
O método anexa o chunk decodificado ao que buf já guarda e devolve o slice estendido, crescendo o array de trás conforme precisa. Em estado estacionário, onde o buffer já cresceu para o tamanho certo, ele faz zero alocações por chunk, o que aparece claramente num benchmark. Se sua carga é "decodificar uma vez, raramente", DecodeString é a escolha mais simples; se é "decodificar milhares de vezes num loop apertado", AppendDecode é o que pegar.
Mantendo seguro
Alguns avisos de segurança específicos de como programas Go realmente usam este pacote. Primeiro, base64 é codificação, não criptografia. Uma string base64 é legível por qualquer um com as ferramentas de desenvolvedor de um navegador web, então "a gente dá base64 na senha antes de enviar" não é uma medida de segurança; é uma conveniência de transporte. A confidencialidade tem que vir do TLS, não do alfabeto.
Segundo, limite suas entradas. O tamanho decodificado de uma string base64 é no máximo DecodedLen do seu comprimento, então confira esse número contra um limite antes de alocar, e envolva corpos de requisição com um teto de tamanho antes que qualquer coisa toque num decodificador. As duas verificações custam uma linha cada, e juntas transformam uma decodificação sem limite numa decodificação limitada.
Terceiro, decida sua posição em relação a entrada descuidada. O modo normal derruba em silêncio os bits de fim não usados do grupo final, o que significa que duas strings diferentes podem decodificar para os mesmos bytes. Para a maioria dos dados, isso não importa. Para qualquer coisa que seja parte de um protocolo, uma mensagem assinada ou um valor que vai ser comparado ou armazenado, Strict() é a escolha conservadora, porque torna a forma canônica a única forma aceita.
Quarto, tenha cuidado para onde os bytes decodificados vão. Se um valor decodificado vira nome de arquivo, caminho, fragmento SQL ou argumento de comando, a camada base64 não te protegeu de nada: os bytes agora são a entrada não confiável do seu programa, e as regras usuais de sanitização se aplicam exatamente como para qualquer outro dado do usuário.
Quanto é rápido o decodificador?
Base64 em Go é rápido, e continua rápido em dados grandes porque a implementação é um loop simples de lookup em tabela, sem reflexão e sem alocação por caractere. Numa CPU desktop recente rodando Go 1.26, uma string de 500 bytes decodifica em mais ou menos um quarto de microsegundo com uma alocação, o que dá na ordem de dois gigabytes por segundo. Um megabyte de base64 decodifica em muito menos de um milissegundo; um gigabyte em muito menos de um segundo. Os números mudam com o hardware, mas a forma não: decodificar base64 quase nunca é o gargalo; a rede ou o disco em volta geralmente são.
Se você está num loop quente, o perfil de alocação é o que observar. DecodeString aloca o slice de resultado em toda chamada. Decode com destino pré-dimensionado e AppendDecode com buffer reutilizado evitam essa alocação por completo em estado estacionário. Para uma decodificação que acontece algumas vezes por requisição, nada disso importa; para uma decodificação que acontece alguns milhões de vezes por segundo, é a diferença entre um perfil de memória plano e um garbage collector girando sem parar.
Uma breve história do pacote
O pacote base64 é uma das partes mais antigas da biblioteca padrão do Go. O cabeçalho de copyright do arquivo de origem diz 2009, o ano em que a linguagem foi criada, e o pacote faz parte da biblioteca padrão desde o primeiro lançamento estável, o Go 1.0, em março de 2012. Isso significa que o DecodeString que você chama hoje é a mesma API, com o mesmo comportamento, que programas Go chamam há mais de uma década.
O crescimento desde então foi modesto e útil. O Go 1.5 em agosto de 2015 adicionou os valores sem padding RawStdEncoding e RawURLEncoding, que abriram a porta para strings compactas estilo JWT. O Go 1.8 em fevereiro de 2017 adicionou Strict(), dando aos protocolos um jeito de exigir entrada canônica. O Go 1.22 em fevereiro de 2024 adicionou AppendDecode e AppendEncode para a família inteira de codificações base, e apertou WithPadding para rejeitar argumentos sem sentido. E até setembro de 2026, com Go 1.27.1 como o lançamento mais novo e Go 1.26 como a outra linha suportada, a API é exatamente a descrita neste artigo: quatro codificações prontas, um decodificador de stream, um modo estrito e uma família append para desempenho.
O fato mais profundo é a promessa de compatibilidade. A garantia do Go 1 significa que o pacote vai continuar aceitando e rejeitando as mesmas entradas para sempre, então um decodificador que você escreve este ano para um formato de dados produzido em 2015 vai continuar funcionando. Para um formato tão antigo e tão sem graça, essa é a melhor notícia que existe.
Coisas que vão te surpreender
Depois de um tempo em Go você para de se surpreender com base64, mas nas primeiras vezes, alguns desses fatos batem forte, então aqui vão eles:
- O decodificador pula
\re\nem qualquer lugar da entrada, mas não um espaço, não um tab, não um espaço de largura zero. A tolerância é intencional; ela existe para fazer entrada quebrada no estilo MIME funcionar, e para exatamente onde a spec para. - Uma decodificação falha ainda pode devolver dados de verdade. O resultado parcial é tudo que foi decodificado antes do byte ruim, e o erro chega junto, e não no lugar dele.
CorruptInputErroré literalmente umint64com um método anexo. O "erro" é o offset, e a mensagem é montada sob demanda.DecodeeEncodeconfiam em você para dimensionar os buffers de destino. Dê a eles um buffer pequeno demais e você não recebe erro; recebe um panic.- Um caractere sozinho não é entrada válida para nenhuma das quatro codificações embutidas. Um caractere base64 carrega seis bits, e um byte precisa de oito, então não existe grupo completo em um caractere, com padding ou sem.
- Até agosto de 2026, mais de 244.000 pacotes públicos no pkg.go.dev listam
encoding/base64entre os imports. Ele é, em silêncio, um dos pacotes mais dependidos de todo o ecossistema.
Onde as decodificações dão errado
Estes são os erros de decodificação que não param de aparecer em codebases Go, mais ou menos na ordem em que aparecem em threads de suporte:
- Escolher
StdEncodingpara dados URL-safe (ou o contrário). O sintoma é um erro no primeiro-,_,+ou/, e a correção é olhar a string antes de escolher o decodificador. - Colar uma string de um terminal, um e-mail ou um PDF, o que enfia espaços, tabs ou artefatos de fim de linha. O Go pula quebras de linha de verdade, mas um espaço no meio da string é um byte corrompido, e o offset no erro vai apontar certinho para ele.
- Esquecer que o resultado é um
[]byte. Imprimi-lo cru te dá uma lista de números, e passá-lo para uma função que espera uma string precisa de conversãostring(...). - Conferir o erro e mesmo assim usar os dados parciais. O prefixo meio-decodificado é real, mas não é o payload, e código que trata como tal falha em produção com dados que têm exatamente metade do comprimento certo.
- Dimensionar o buffer do
Decodecomlen(src)em vez deDecodedLen(len(src)). O primeiro tamanho está errado na outra direção que você esperava, e o panic que ele dispara só acontece em entradas grandes, o que o torna um favorito dos ambientes de staging. - Assumir que segmentos de JWT trazem padding. Não trazem, e um decodificador com padding falha no último caractere com um erro que parece um mistério. Use
RawURLEncoding. - Acreditar que todo espaço em branco é pulado. Não é. Só os dois caracteres de quebra de linha, e "espaço em branco" na área de transferência é uma família muito maior que isso.
- Decodificar duas vezes, ou esquecer de decodificar duas vezes, quando o valor é base64 de base64 (um arquivo que foi anexado a um e-mail que por sua vez foi anexado). A verificação é uma ida e volta: decodifique uma vez, veja se o resultado ainda parece base64 e só então decodifique de novo.
A outra metade do trabalho
Essa é a história inteira do lado da decodificação: um pacote, quatro decodificadores prontos, um decodificador de stream para dados grandes, um modo estrito para protocolos exigentes e mensagens de erro que dizem o byte onde as coisas deram errado. Aprenda as regras de tolerância, escolha o decodificador olhando para os caracteres, limite suas entradas, e o base64 em Go vira o utilitário sem graça, previsível e sem dependências para o qual foi desenhado.
Quando o trabalho inverte, e seu programa Go precisa produzir strings base64 em vez de abri-las, o artigo relacionado sobre codificação Base64 em Go cobre esse lado em detalhe: a API de um método do codificador, a chamada Close que engole em silêncio seus últimos dois bytes, a quebra de linha para MIME e como as quatro codificações se encaixam nos canais por onde viajam.
Última atualização: 2026-09-08
Artigo relacionado: Codificação Base64 em Go: um guia completo