Base64 decoding in Go comes up constantly β JWT inspection, binary file attachments, API payloads from cloud services. Go's standard encoding/base64package handles all of it, but picking the wrong encoding variant (standard vs URL-safe, padded vs unpadded) is the single most common source of βillegal base64 dataβ errors. This guide covers StdEncoding, URLEncoding, RawURLEncoding, streaming decoding with base64.NewDecoder, JWT payload inspection, and four mistakes that trip up almost everyone the first time. For one-off decoding in the browser, ToolDeck's Base64 decoder gets the job done instantly without writing a line of code.
- βencoding/base64 is part of the Go standard library β no go get required
- βUse RawURLEncoding for JWT tokens and most modern APIs (no padding, URL-safe alphabet)
- βStdEncoding uses + and / with = padding; URLEncoding swaps to - and _ but keeps padding
- βbase64.NewDecoder wraps any io.Reader for streaming decode without loading into memory
- βAlways check the returned error β invalid padding and wrong alphabet produce illegal base64 data
What is Base64 Decoding?
Base64 encoding represents binary data as ASCII text using 64 printable characters (AβZ, aβz, 0β9, plus two extras). Decoding reverses this β it converts that ASCII representation back into the original bytes. Every 4 Base64 characters decode to exactly 3 bytes. The scheme exists because many transport layers (email, HTTP headers, JSON fields) are designed for text, not raw binary. Below is what the round-trip looks like:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// Raw bytes β Base64 encoded β decoded back to raw bytes
original := []byte("service_token:xK9mP2qR")
// Encoded: "c2VydmljZV90b2tlbjp4SzltUDJxUg=="
encoded := base64.StdEncoding.EncodeToString(original)
decoded, _ := base64.StdEncoding.DecodeString(encoded)
fmt.Println(string(decoded) == string(original)) // true
}Decode Base64 in Go with encoding/base64
The encoding/base64 package ships with Go β no external dependencies. It exposes four pre-defined encoding variants as package-level variables. The most commonly used function for string input is DecodeString, which returns a byte slice and an error.
package main
import (
"encoding/base64"
"fmt"
"log"
)
func main() {
// Standard Base64 β the alphabet uses + and / with = padding
encoded := "eyJob3N0IjoiZGItcHJvZC51cy1lYXN0LTEiLCJwb3J0Ijo1NDMyfQ=="
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
log.Fatalf("decode error: %v", err)
}
fmt.Println(string(decoded))
// {"host":"db-prod.us-east-1","port":5432}
}The Decode method works on byte slices rather than strings, and writes the result into a pre-allocated destination buffer. You need to size the buffer correctly β use base64.StdEncoding.DecodedLen(len(src)) to get the maximum size (it may be a few bytes larger than the actual decoded length due to padding).
package main
import (
"encoding/base64"
"fmt"
"log"
)
func main() {
src := []byte("eyJob3N0IjoiZGItcHJvZCIsInBvcnQiOjU0MzJ9")
dst := make([]byte, base64.RawStdEncoding.DecodedLen(len(src)))
n, err := base64.RawStdEncoding.Decode(dst, src)
if err != nil {
log.Fatalf("decode: %v", err)
}
fmt.Println(string(dst[:n]))
// {"host":"db-prod","port":5432}
}DecodedLen returns an upper bound, not the exact length. Always use the n return value from Decode to slice the result correctly: dst[:n].StdEncoding vs URLEncoding β Choosing the Right Variant
This is where most confusion lives. Go's encoding/base64 exposes four encoding objects, and picking the wrong one is guaranteed to give you an error. The difference comes down to two things: the alphabet and padding.
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// JWT header payload β URL-safe, no padding
jwtHeader := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjMtMDkifQ"
// Wrong: StdEncoding fails on URL-safe input without padding
_, err1 := base64.StdEncoding.DecodeString(jwtHeader)
fmt.Println("StdEncoding error:", err1)
// StdEncoding error: illegal base64 data at input byte 43
// Correct: RawURLEncoding β no padding, URL-safe alphabet
decoded, err2 := base64.RawURLEncoding.DecodeString(jwtHeader)
fmt.Println("RawURLEncoding ok:", err2, "β", string(decoded))
// RawURLEncoding ok: <nil> β {"alg":"RS256","kid":"2023-09"}
}The four variants in plain terms:
My rule of thumb: if it came from a JWT, OAuth flow, or a cloud provider SDK, reach for RawURLEncoding first. If it came from email attachments or old-school web forms, try StdEncoding. The error message always tells you the exact byte position where decoding failed.
Decode Base64 from a File and API Response
Reading a Base64-encoded file
Binary files (images, PDFs, certificates) are sometimes stored Base64-encoded on disk. Read the file, strip any trailing whitespace, then decode:
package main
import (
"encoding/base64"
"fmt"
"log"
"os"
"strings"
)
func main() {
raw, err := os.ReadFile("certificate.pem.b64")
if err != nil {
log.Fatalf("read file: %v", err)
}
// Strip newlines β Base64 files often have line breaks every 76 chars
cleaned := strings.ReplaceAll(strings.TrimSpace(string(raw)), "\n", "")
decoded, err := base64.StdEncoding.DecodeString(cleaned)
if err != nil {
log.Fatalf("decode: %v", err)
}
if err := os.WriteFile("certificate.pem", decoded, 0600); err != nil {
log.Fatalf("write: %v", err)
}
fmt.Printf("decoded %d bytes β certificate.pem\n", len(decoded))
}Decoding a Base64 field from an API JSON response
Cloud APIs frequently return binary data (encryption keys, signed blobs, thumbnails) as Base64 strings inside JSON. Unmarshal the JSON first, then decode the target field:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
)
type SecretResponse struct {
Name string `json:"name"`
Payload string `json:"payload"` // Base64-encoded secret value
Version int `json:"version"`
}
func fetchAndDecodeSecret(secretURL string) ([]byte, error) {
resp, err := http.Get(secretURL)
if err != nil {
return nil, fmt.Errorf("http get: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var secret SecretResponse
if err := json.NewDecoder(resp.Body).Decode(&secret); err != nil {
return nil, fmt.Errorf("decode json: %w", err)
}
value, err := base64.StdEncoding.DecodeString(secret.Payload)
if err != nil {
return nil, fmt.Errorf("decode base64: %w", err)
}
return value, nil
}
func main() {
value, err := fetchAndDecodeSecret("https://api.example.com/secrets/db-password")
if err != nil {
log.Fatalf("fetch secret: %v", err)
}
fmt.Printf("secret value: %s\n", value)
}fmt.Errorf("decode base64: %w", err) rather than losing context. The original error message from encoding/base64 includes the byte position of the failure, which is useful during debugging.Streaming Large Base64-encoded Files
Loading a 500 MB Base64-encoded file into memory with os.ReadFile then calling DecodeString uses roughly 750 MB of RAM (the encoded string plus the decoded bytes). base64.NewDecoder wraps any io.Reader and decodes in chunks, keeping memory usage near-constant. Combine it with io.Copy for a clean streaming pipeline:
package main
import (
"encoding/base64"
"fmt"
"io"
"log"
"os"
)
func streamDecodeFile(srcPath, dstPath string) error {
src, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("open source: %w", err)
}
defer src.Close()
dst, err := os.Create(dstPath)
if err != nil {
return fmt.Errorf("create dest: %w", err)
}
defer dst.Close()
decoder := base64.NewDecoder(base64.StdEncoding, src)
written, err := io.Copy(dst, decoder)
if err != nil {
return fmt.Errorf("stream decode: %w", err)
}
fmt.Printf("written %d bytes to %s\n", written, dstPath)
return nil
}
func main() {
if err := streamDecodeFile("backup.tar.b64", "backup.tar"); err != nil {
log.Fatal(err)
}
}base64.NewDecoder expects clean, uninterrupted Base64 data. If the source file has line breaks (common in PEM and MIME-encoded files), wrap the source reader with a line-stripping reader or pre-process the file to remove newlines before streaming.Base64 Decoding from the Command Line
Every macOS and Linux system ships with base64; on Windows, PowerShell has a built-in equivalent. For quick inspection of API payloads, these are faster than writing a Go script.
# Decode a Base64 string (Linux / macOS)
echo "eyJob3N0IjoiZGItcHJvZCIsInBvcnQiOjU0MzJ9" | base64 --decode
# {"host":"db-prod","port":5432}
# Decode and pretty-print with jq (pipe the JSON output)
echo "eyJob3N0IjoiZGItcHJvZCIsInBvcnQiOjU0MzJ9" | base64 --decode | jq .
# {
# "host": "db-prod",
# "port": 5432
# }
# Decode a Base64-encoded file to binary
base64 --decode < encrypted_payload.b64 > encrypted_payload.bin
# macOS uses -D flag instead of --decode
echo "c2VjcmV0LXRva2Vu" | base64 -DFor inspecting JWT tokens without any tools installed, paste the token into ToolDeck's Base64 decoder β split on the dots and decode each part.
High-Performance Alternative: encoding/base64 is Already Fast
Unlike Python, where orjson vs jsonis a meaningful performance conversation, Go's encoding/base64is already assembly-optimized and genuinely fast for most workloads. That said, if you're processing millions of records in a tight loop, filippo.io/base64 provides SIMD-accelerated decoding with a drop-in API.
go get filippo.io/base64
package main
import (
"fmt"
"log"
"filippo.io/base64"
)
func main() {
// Drop-in replacement β same API as encoding/base64
encoded := "eyJob3N0IjoiY2FjaGUtcHJvZCIsInBvcnQiOjYzNzl9"
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
log.Fatalf("decode: %v", err)
}
fmt.Println(string(decoded))
// {"host":"cache-prod","port":6379}
}The performance gain is most visible on amd64 with AVX2 support β benchmarks show 2β4x throughput improvement on large inputs. For everyday API response decoding (a few hundred bytes at a time), stick with the standard library.
Decoding Base64 JWT Payload in Go
JWT inspection comes up in almost every backend service. In my experience, most debugging sessions boil down to βwhat's actually in this token?β β and you can answer that without pulling in a full JWT library. The token has three Base64url-encoded segments separated by dots. The middle segment is the payload you actually care about:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"strings"
)
type JWTPayload struct {
Subject string `json:"sub"`
Issuer string `json:"iss"`
Expiry int64 `json:"exp"`
Roles []string `json:"roles"`
}
func decodeJWTPayload(token string) (*JWTPayload, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid JWT: expected 3 segments, got %d", len(parts))
}
// JWT uses RawURLEncoding β URL-safe alphabet, no = padding
raw, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("decode payload: %w", err)
}
var payload JWTPayload
if err := json.Unmarshal(raw, &payload); err != nil {
return nil, fmt.Errorf("unmarshal payload: %w", err)
}
return &payload, nil
}
func main() {
token := "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c3ItNDQyIiwiaXNzIjoiYXV0aC5leGFtcGxlLmNvbSIsImV4cCI6MTc0MTk1NjgwMCwicm9sZXMiOlsiYWRtaW4iLCJhdWRpdG9yIl19.SIGNATURE"
payload, err := decodeJWTPayload(token)
if err != nil {
log.Fatalf("jwt: %v", err)
}
fmt.Printf("Subject: %s\n", payload.Subject)
fmt.Printf("Issuer: %s\n", payload.Issuer)
fmt.Printf("Roles: %v\n", payload.Roles)
// Subject: usr-442
// Issuer: auth.example.com
// Roles: [admin auditor]
}Common Mistakes
I've encountered all four of these in real code reviews β the first two in particular show up almost every time someone integrates a new auth provider.
Problem: JWT tokens and OAuth tokens use the URL-safe Base64 alphabet (- and _). Passing them to StdEncoding.DecodeString fails with 'illegal base64 data'.
Fix: Check your input source: tokens from auth systems use RawURLEncoding; binary attachments use StdEncoding.
// JWT header β URL-safe, no padding token := "eyJhbGciOiJSUzI1NiJ9" decoded, err := base64.StdEncoding.DecodeString(token) // err: illegal base64 data at input byte 19
// JWT header β correct encoding
token := "eyJhbGciOiJSUzI1NiJ9"
decoded, err := base64.RawURLEncoding.DecodeString(token)
// decoded: {"alg":"RS256"}
// err: nilProblem: Decode writes into a pre-allocated buffer and returns the number of bytes actually written. DecodedLen returns an upper bound, so the tail of the buffer may contain garbage bytes.
Fix: Always slice the result with dst[:n] β not dst[:len(dst)].
dst := make([]byte, base64.StdEncoding.DecodedLen(len(src))) base64.StdEncoding.Decode(dst, src) fmt.Println(string(dst)) // may include trailing zero bytes
dst := make([]byte, base64.StdEncoding.DecodedLen(len(src)))
n, err := base64.StdEncoding.Decode(dst, src)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(dst[:n])) // correct β only the decoded bytesProblem: Base64 strings copied from terminals, emails, or config files often have trailing newlines or spaces. Passing them directly to DecodeString fails at the whitespace character.
Fix: Call strings.TrimSpace (and strings.ReplaceAll for embedded newlines) before decoding.
// Value read from a config file with a trailing newline encoded := "c2VydmljZV9rZXk6eEtNcDI=\n" decoded, err := base64.StdEncoding.DecodeString(encoded) // err: illegal base64 data at input byte 24
encoded := "c2VydmljZV9rZXk6eEtNcDI=\n" cleaned := strings.TrimSpace(encoded) decoded, err := base64.StdEncoding.DecodeString(cleaned) // decoded: "service_key:xKMp2" // err: nil
Problem: Calling string(decoded) on binary data (images, compressed payloads) produces invalid UTF-8 strings. Go strings can hold arbitrary bytes, but some operations will mangle the content.
Fix: Keep binary data as []byte throughout your pipeline. Only call string(decoded) when the decoded content is guaranteed to be text.
decoded, _ := base64.StdEncoding.DecodeString(pngBase64)
// Treating binary PNG as a string loses data
imageStr := string(decoded)
os.WriteFile("image.png", []byte(imageStr), 0644) // may corruptdecoded, err := base64.StdEncoding.DecodeString(pngBase64)
if err != nil {
log.Fatal(err)
}
// Write bytes directly β no string conversion
os.WriteFile("image.png", decoded, 0644)Method Comparison
All variants ship in the standard library β no external dependencies for any of these.
For JWT tokens and OAuth flows: RawURLEncoding. For email attachments and MIME data: StdEncoding. For large binary files from disk or network: wrap a reader in base64.NewDecoder β it keeps memory use flat regardless of file size. Need a custom alphabet? base64.NewEncoding(alphabet) builds a new encoding object for exotic use cases.
For quick one-off checks during development, the online Base64 decoder is faster than spinning up a Go program.
Frequently Asked Questions
How do I decode a Base64 string in Go?
Import encoding/base64 and call base64.StdEncoding.DecodeString(s). It returns ([]byte, error) β always check the error. If the string uses URL-safe characters (- and _ instead of + and /), use base64.URLEncoding.DecodeString instead. For JWT tokens and most modern APIs, RawURLEncoding (no padding) is the right choice.
package main
import (
"encoding/base64"
"fmt"
"log"
)
func main() {
encoded := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
decoded, err := base64.RawURLEncoding.DecodeString(encoded)
if err != nil {
log.Fatalf("decode: %v", err)
}
fmt.Println(string(decoded))
// {"alg":"HS256","typ":"JWT"}
}What is the difference between StdEncoding and URLEncoding in Go?
StdEncoding uses the standard Base64 alphabet with + and / characters and = padding β defined in RFC 4648 Β§4. URLEncoding substitutes + with - and / with _ making the output safe in URLs and HTTP headers without percent-encoding β defined in RFC 4648 Β§5. Use URLEncoding for JWT tokens, OAuth tokens, and any data embedded in query strings.
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// Standard: may contain + / and = characters
std := base64.StdEncoding.EncodeToString([]byte("hello/world"))
fmt.Println(std) // "aGVsbG8vd29ybGQ="
// URL-safe: replaces + with - and / with _
url := base64.URLEncoding.EncodeToString([]byte("hello/world"))
fmt.Println(url) // "aGVsbG8vd29ybGQ=" (same β diff shows with different bytes)
// JWT headers never have padding β use RawURLEncoding
raw := base64.RawURLEncoding.EncodeToString([]byte("hello/world"))
fmt.Println(raw) // "aGVsbG8vd29ybGQ" (no trailing =)
}How do I fix "illegal base64 data" errors in Go?
This error means the input contains characters outside the expected alphabet, or the padding is wrong. Three common causes: using StdEncoding on URL-safe input (swap to URLEncoding), using a padded encoder on unpadded input (swap to RawStdEncoding/RawURLEncoding), or trailing whitespace/newlines. Strip whitespace with strings.TrimSpace before decoding.
package main
import (
"encoding/base64"
"fmt"
"log"
"strings"
)
func main() {
// Input from a webhook payload β has newlines stripped from wire format
raw := " aGVsbG8gd29ybGQ= \n"
cleaned := strings.TrimSpace(raw)
decoded, err := base64.StdEncoding.DecodeString(cleaned)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(decoded)) // hello world
}How do I stream-decode a large Base64-encoded file in Go?
Use base64.NewDecoder(base64.StdEncoding, reader) which wraps any io.Reader and decodes on the fly. Pipe it through io.Copy to write to the destination without buffering the entire file in memory.
package main
import (
"encoding/base64"
"io"
"log"
"os"
)
func main() {
src, err := os.Open("attachment.b64")
if err != nil {
log.Fatal(err)
}
defer src.Close()
dst, err := os.Create("attachment.bin")
if err != nil {
log.Fatal(err)
}
defer dst.Close()
decoder := base64.NewDecoder(base64.StdEncoding, src)
io.Copy(dst, decoder)
}Can I decode a Base64 JWT payload in Go without a JWT library?
Yes. A JWT is three Base64url-encoded segments joined by dots. Split on "." and decode the second segment (index 1) with base64.RawURLEncoding.DecodeString β JWT headers and payloads use the URL-safe alphabet and no padding. The signature segment (index 2) is binary and usually only needed for verification.
package main
import (
"encoding/base64"
"fmt"
"log"
"strings"
)
func main() {
token := "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3ItOTAxIiwicm9sZSI6ImFkbWluIn0.SIG"
parts := strings.Split(token, ".")
if len(parts) < 2 {
log.Fatal("invalid JWT format")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
log.Fatalf("decode payload: %v", err)
}
fmt.Println(string(payload))
// {"sub":"usr-901","role":"admin"}
}What encoding should I use to decode Base64 data from an HTTP API response?
Check the API docs or inspect the encoded string. If it contains + or / characters and ends with =, use StdEncoding. If it uses - and _ characters without =, use RawURLEncoding. When uncertain, try RawURLEncoding first β most modern APIs (OAuth2, JWT, Google Cloud, AWS) use URL-safe Base64 without padding.
package main
import (
"encoding/base64"
"strings"
)
// Detect encoding variant from the encoded string
func decodeAPIPayload(encoded string) ([]byte, error) {
// URL-safe characters without padding β common in modern APIs
if !strings.Contains(encoded, "+") && !strings.Contains(encoded, "/") {
return base64.RawURLEncoding.DecodeString(encoded)
}
// Standard Base64 with padding
return base64.StdEncoding.DecodeString(encoded)
}Related Tools
- Base64 Encoder β encode binary data or text to Base64 in the browser, useful for generating test fixtures to paste into your Go unit tests.
- JWT Decoder β split and decode all three JWT segments at once, with field-by-field payload inspection β no Go code required when you just need to read a token during debugging.
- URL Decoder β percent-decode URL-encoded strings, handy when API responses mix Base64url data with percent-encoded query parameters.
- JSON Formatter β after decoding a Base64 JWT payload or API response, paste the JSON here to pretty-print and validate the structure instantly.