Binary data needs a text passport
You have a binary image file, a cryptographic key, or a serialized protobuf message. You need to send it through a system that only understands text. Maybe you are embedding an image in a JSON payload. Maybe you are passing a token in an HTTP header. Binary data contains bytes that look like control characters to a text parser. If you send raw bytes through a text protocol, the message gets corrupted, truncated, or rejected. Base64 solves this by translating arbitrary binary data into a safe subset of ASCII characters. It gives your binary data a passport that text-only systems will accept.
The math behind the alphabet
Base64 works by grouping your input into chunks of three bytes. Three bytes give you 24 bits of data. The encoder splits those 24 bits into four groups of six bits. Each six-bit group maps to one character from a 64-character alphabet. The standard alphabet uses A-Z, a-z, 0-9, plus + and /. If your input isn't a perfect multiple of three bytes, the encoder adds padding characters (=) to keep the output aligned. The result is a string that contains only printable ASCII characters.
The trade-off is size. Base64 increases the data size by roughly 33 percent. Four characters represent three bytes. This overhead is the cost of safety. Base64 is encoding, not compression. It makes data safe to transmit, but it makes the payload larger.
Minimal example
Here's the simplest way to encode and decode data. The encoding/base64 package provides pre-configured encoders. StdEncoding is the default. It follows the standard RFC 4648 specification.
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// Start with raw binary data. In real code this might come from a file or crypto operation.
data := []byte("Hello, World!")
// StdEncoding uses the standard RFC 4648 alphabet and adds padding.
// EncodeToString allocates a new string for the result.
encoded := base64.StdEncoding.EncodeToString(data)
fmt.Println(encoded) // SGVsbG8sIFdvcmxkIQ==
// DecodeString returns a byte slice. Always check the error.
// The decoder validates the input characters and padding.
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
panic(err)
}
fmt.Println(string(decoded)) // Hello, World!
}
How allocation works
The EncodeToString function takes a byte slice and returns a string. Go strings are immutable. The function allocates a new string buffer, fills it with the encoded characters, and returns it. If you are encoding a small value like a JWT token, this allocation is negligible. If you are encoding a large file in a tight loop, the allocation triggers garbage collection.
The package also provides Encode which writes to a []byte destination. This lets you reuse a buffer. You pass a pre-allocated slice, and the encoder writes into it. This reduces memory pressure in high-throughput services. The convention in Go is to prefer EncodeToString for simplicity unless profiling shows allocation is a bottleneck.
URL safety and streaming
Standard Base64 uses + and /. These characters have special meaning in URLs. A plus sign often represents a space. A slash separates path segments. If you put a standard Base64 string into a URL query parameter, the browser or server might misinterpret the characters. You need URL-safe Base64. The URLEncoding variant replaces + with - and / with _. These characters are safe in URLs and filenames.
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// Data containing bytes that will map to + and / in standard encoding.
// 0xFB maps to + and 0xFF maps to / in the standard alphabet.
data := []byte{0xFB, 0xFF, 0x00}
// URLEncoding replaces + with - and / with _.
// This is safe for use in URLs, cookies, and filenames.
urlSafe := base64.URLEncoding.EncodeToString(data)
fmt.Println(urlSafe) // _-8A
// Decode using the same encoder variant.
// Mixing encoders causes runtime errors.
decoded, err := base64.URLEncoding.DecodeString(urlSafe)
if err != nil {
panic(err)
}
fmt.Printf("% x\n", decoded) // fb ff 00
}
For large data, loading the entire payload into memory is wasteful. The package provides NewEncoder and NewDecoder which return io.Writer and io.Reader interfaces. You can pipe data through the encoder without buffering the whole result. This is essential for processing large files or streaming data over a network.
package main
import (
"encoding/base64"
"os"
)
func main() {
// Open a file for writing.
f, err := os.Create("output.b64")
if err != nil {
panic(err)
}
defer f.Close()
// NewEncoder returns an io.Writer.
// Writes to this writer are encoded and piped directly to the file.
enc := base64.NewEncoder(base64.StdEncoding, f)
defer enc.Close()
// Write raw data. The encoder handles chunking and padding automatically.
// This avoids allocating a massive string in memory.
_, err = enc.Write([]byte("Large binary payload here"))
if err != nil {
panic(err)
}
}
Pitfalls and padding wars
The most common mistake is mixing encoders. If you encode with URLEncoding and try to decode with StdEncoding, the decoder fails. The compiler won't catch this. You get a runtime error like illegal base64 data at input byte 2. The error message points to the first invalid character. Always use the same encoder variant for encoding and decoding.
Padding is another trap. The standard encoders require padding characters (=) to align the output. Some systems strip padding to save space. If you receive unpadded data, the standard decoder rejects it. Use RawStdEncoding or RawURLEncoding to handle missing padding. These variants accept strings with or without trailing = signs. They also omit padding when encoding. Don't manually strip padding unless you have to. It adds complexity and bugs. Use the Raw variants instead.
Base64 is not encryption. It is encoding. Anyone can decode a Base64 string. Do not use Base64 to hide secrets. Use it to transport data. If you need security, encrypt the data before encoding it.
Decision matrix
Use base64.StdEncoding when you are working with standard protocols like PEM files, email attachments, or general data interchange where padding is expected.
Use base64.URLEncoding when the Base64 string will appear in a URL, filename, or cookie. The alphabet avoids characters that need percent-encoding.
Use base64.RawStdEncoding when you are consuming data from a system that strips padding characters. This encoder accepts strings with or without trailing = signs.
Use base64.RawURLEncoding when you need URL safety and the source might omit padding.
Use base64.NewEncoder and base64.NewDecoder when you are streaming large amounts of data. These return io.Writer and io.Reader interfaces so you can pipe data without loading the entire payload into memory.
Base64 is a translator, not compression. Padding is part of the contract. Respect it.