Base64 Encoding in Go: A Complete Guide
Every now and then your Go program has to hand binary data to a world that only accepts text: a JSON field that must stay a string, a URL that must stay a single token, an email attachment crossing servers that remember the 7-bit days, an image that wants to live inside the HTML so the page skips a request. Base64 is the courier for exactly that job, and the home page above already explains the format in depth, so this article goes straight to the packing craft: producing base64 strings in Go that every decoder on the planet can open without a fight.
The good news up front: the encoder is the gentle half of the story. One method, no error return, no failure mode, byte-identical output on every Go release since the first stable one. All the drama lives around that method: picking the right alphabet for the channel the string will travel through, the Close call that quietly swallows your last two bytes when you forget it, the size tax the format carries, and the fact that Go, unlike most of the world, never wraps its output at 76 characters. Meet the function first, then meet the traps.
Packing Without Failing
Ninety percent of encoding life in Go is one method on the Encoding type, and it is the only entry point in the whole package with no error return:
func (enc *Encoding) EncodeToString(src []byte) string
Hand it bytes, it gives you a string, and that is the entire contract:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
packed := base64.StdEncoding.EncodeToString([]byte("Man"))
fmt.Println(packed) // TWFu
}
There is no error value because there is nothing to go wrong: any byte is legal input, the alphabet always covers it, and the output is always pure ASCII. Three properties are worth memorizing, because they answer half of all future questions. First, the output length is a pure arithmetic function of the input length, and the package even hands you the formula as a method: EncodedLen(n) returns (n+2)/3*4 for padded encodings, so 3 input bytes become 4 characters, 6 become 8, and so on. Second, the format carries a size tax: every three bytes of data come back as four characters, which is the familiar expansion of roughly 33 percent that shows up in your bandwidth bills and storage quotas. Third, the method is deterministic: the same bytes always produce the same string, on any machine, in any version of Go, forever. That determinism is what makes base64 a serialization format instead of a mystery.
One Go-specific note on the input side: the method takes []byte, not string, and in Go a string is just a read-only slice of bytes, so the conversion is free and automatic at the boundary. Text in a Go program is UTF-8 by convention, so when you encode a string you are encoding its UTF-8 bytes, and that is exactly what every modern decoder on the other end expects. More on that in the character set section.
How Go Ships It
Like everything in this article, the encoder comes from the standard library package encoding/base64, which has shipped since the first release of the language and whose source file still carries its 2009 copyright header. There is no module to fetch, no feature flag to flip, and no platform quirk: if go version works, go doc encoding/base64 prints the whole API for you.
As of this writing the newest release is Go 1.27.0, out on August 19, 2026, with the Go 1.26 line (currently 1.26.7) as the other supported track. Install Go from the official tarballs on go.dev/dl, from your distribution's package manager (sudo apt install golang-go), or through the golang.org/dl wrapper if you juggle versions. The base64 API is identical on both supported lines, and the table below is the whole history of what ever changed, which is a short list for a package this central:
| Release | Year | What changed in encoding/base64 |
|---|---|---|
| Go 1.0 | 2012 | Package stable from day one; source copyright 2009 |
| Go 1.5 | 2015 | RawStdEncoding and RawURLEncoding added for unpadded output |
| Go 1.8 | 2017 | Strict() added for canonical decoding (decoder side) |
| Go 1.22 | 2024 | AppendEncode and AppendDecode added; WithPadding now rejects bad arguments |
| Go 1.27.0 | 2026 | Current release; API unchanged, behavior byte-stable by the Go 1 promise |
The practical consequence of that history: code written against this API in 2015 compiles and behaves identically today, and the strings your program encodes in 2026 will decode correctly on any Go release, past or future. For a serialization format, that is the quiet superpower.
Picking an Alphabet for the Destination
Encoding has one real decision, and it is a question of travel: where will this string go? Go gives you four ready-made encoders, and each one is tuned to a different channel:
| Encoder | Alphabet | Padding | Send it there when the string travels through |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
JSON bodies, email MIME parts, data URLs, HTTP Basic auth, PEM, most APIs |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
URL paths and queries, file names, anywhere + or / would need escaping |
RawStdEncoding |
A-Z a-z 0-9 + / |
none | Compact standard-alphabet strings where padding must not appear |
RawURLEncoding |
A-Z a-z 0-9 - _ |
none | JWT segments, compact identifiers, tokens embedded in URLs |
The reasoning behind the variants is the reasoning behind the format itself. The standard alphabet is what MIME and most APIs expect, so it is the default and the safe answer when nobody told you otherwise. The URL-safe alphabet exists because + and / are reserved characters in URLs: a plus in a query string is often read as a space, and a slash starts a new path segment, so standard base64 in a URL either breaks or needs percent-escaping on nearly every character. Swapping them for - and _, which are legal unescaped in paths, queries and file names, is the fix RFC 4648 standardized. The Raw variants drop the trailing equals signs entirely, which matters in contexts where padding is either forbidden or simply never used, like JWT segments. The rule that saves you from most of the debugging: the encoder you pick and the decoder the other side uses are one contract, and the contract is written by the destination, not by you.
If a system you are talking to defined a private 64-character alphabet, base64.NewEncoding("...64 chars...") builds you an encoder for it, and WithPadding(rune) lets you swap the padding character or disable it with NoPadding. Both functions panic on invalid arguments (a wrong alphabet length, a duplicate character, a newline in the alphabet, a padding character that clashes with the alphabet), so build your custom encoders once, at startup, never in a hot path.
The Close Trap
Here is the most famous trap in this package, and it only appears when you encode a stream instead of a string. NewEncoder wraps any io.Writer in a base64-encoding writer, and because base64 works in blocks of three input bytes producing four output characters, the encoder has to buffer your last one or two bytes, waiting to see whether more are coming. They only flush when you close it:
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 -- where is the "lo"?
buf.Reset()
enc = base64.NewEncoder(base64.StdEncoding, &buf)
enc.Write([]byte("hello"))
enc.Close()
fmt.Println(buf.String()) // aGVsbG8= -- the full encoding of "hello"
}
The first printout is the whole lesson: without Close, the encoder emitted only the first complete block, three bytes of "hello" becoming "aGVs", and the remaining two bytes simply vanished into the internal buffer. The second printout, after Close, is the correct, complete string. The fix is a habit, not a technique: the moment you create an encoder, create its cleanup too:
enc := base64.NewEncoder(base64.StdEncoding, w)
defer enc.Close() // remember to check the returned error in production code
Two details make this trap sharper than it looks. First, Close does real work: it flushes the pending partial block and it can fail, because it writes to the underlying writer, so the idiomatic version checks its error, especially when the destination is a network or a disk. Second, the documentation says it is an error to call Write after Close, but the runtime does not enforce that sentence. If you write again after closing, the encoder quietly starts a fresh block and appends it, producing a string with padding in the middle of it, which is invalid base64 that most decoders will reject with a confusing offset. The contract is yours to keep.
Line Wrapping, the Go Way
Every other major base64 implementation you have ever used wraps its output: MIME wants lines of at most 76 characters, PEM uses 64, email clients all over the world insert a CRLF every so often. Go's encoder does none of that. It emits one continuous line, no matter how large the payload, and it has done so since the package was born. The output for a megabyte of data is a single megabyte-and-a-third line, start to finish, no breaks.
That is a deliberate choice, not an oversight. The format works identically with or without the line breaks, Go's own decoder skips them anywhere in the input, and an encoder that silently inserts CRLFs into your data would surprise programs that store the string in a database column or compare it for equality. The cost is that you have to wrap yourself when the channel requires it, which is one small helper:
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))
}
A note on the direction of travel: because Go's decoder ignores newlines anywhere, wrapped input decodes perfectly on the Go side of any bridge. The other direction is where care is needed: if you send wrapped output to a consumer that does not expect breaks (a JSON field, a URL, a token), strip them first, because that consumer may treat a newline as a corrupt character. Know which convention your channel lives in, and emit it on purpose.
Packing for Email and MIME
Email is the oldest home of base64. The original SMTP protocol was designed to transport 7-bit ASCII, so attachments were base64-encoded before sending and decoded on arrival, and the MIME standard (RFC 2045) formalized the practice: the header Content-Transfer-Encoding: base64 marks a part, and the body should be broken into lines of at most 76 characters with CRLF between them.
Go's net/smtp package sends the bytes you give it, and it will not build MIME parts for you, so in a program that composes mail the base64 piece looks like this:
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())
}
Three things to notice. The standard encoder is the right one here, because MIME is the original standard-alphabet context. The line breaks are CRLF, not the platform's native newline, because that is what the RFC specifies and what mail parsers expect. And if your program sends real email at volume, a maintained MIME library will build the whole message for you; the point of this example is the base64 half, which is the part that belongs to this package. Get the alphabet and the line convention right, and the rest of MIME is someone else's problem.
Packing Files
For files that fit in memory, the pattern is the same two lines as anywhere else: read, then EncodeToString. For files that do not, streaming keeps your memory flat, and the recipe is a file, an encoder, a copy, and two closes in the right order:
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) // flushes the final partial block
}
if err := out.Close(); err != nil {
panic(err)
}
The order of the closes is the subtle part, and it is the file version of the Close trap: the encoder must be closed before the file, because enc.Close is what writes the final partial block into the file, and closing the file first would leave that block in a buffer that writes into nothing. With defer, remember that deferred calls run in reverse order, so registering out.Close first and enc.Close second (or, as in the example above, closing the encoder explicitly before deferring the file) is what makes the sequence safe.
Keep the size tax in your head when you plan around this pattern: a 10 megabyte photo becomes roughly 13.3 megabytes of text, and a 100 megabyte archive becomes a 133 megabyte string on disk. If the destination has a quota, a limit or a price per byte, the base64 version of your file is what is being counted, not the original.
Packing for the Web: Data URLs
Browsers will happily load an image or a font from a string that lives inside the HTML or CSS itself, and that string is a data URL: the media type, the ;base64 flag, a comma, and the payload, all in one URL. Go has no data URL helper, but building one is string concatenation, because the format is a contract you can see written out:
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...
}
Two rules keep data URLs out of the weeds. Always include the media type: it is optional in the grammar (the default is text/plain;charset=US-ASCII), but a browser guessing the type of your binary payload is not a scenario you want. And treat data URLs as a small-asset trick. The RFC says the scheme is only useful for short values, and the 33 percent expansion is what makes the difference between a 2 kilobyte icon that saves a request and a 5 megabyte photo that bloats every page load, with no cache to share it and no URL to hand to anyone. Icons, favicons, small sprites: yes. Product photography: no.
Packing for HTTP
Three HTTP contexts dominate base64 in Go services, and two of them come with built-in help. The first is the JSON body, the workhorse: you encode a value before marshaling, and the field carries a plain string across the wire:
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="}
}
If a type appears in many places, the clean Go move is to implement MarshalJSON and UnmarshalJSON on it, so the base64 step is invisible to every call site. The second context is HTTP Basic authentication, where the standard library does the whole job: Request.SetBasicAuth(user, pass) builds the Authorization header for you, running the standard encoder over the user:pass pair that RFC 2617 specifies. The one rule there is not to improvise: Basic auth is standard base64 with a Basic prefix, and a URL-safe alphabet or a missing padding sign will turn a working login into a 401 that nobody can explain.
The third context is URLs, where the string is the payload of a path segment or a query parameter. Here the standard alphabet is a poor choice, because +, / and = all collide with URL grammar, and you end up percent-escaping most of the string. Encode with the URL-safe variant instead, and the token survives the URL intact. If the consumer still percent-escapes it, nothing is broken, but if it does not, you have saved yourself a class of 404s.
URL-Safe Output
URL-safe base64 deserves its own section in Go because it is the variant you will reach for more often than the standard one, and because Go makes the switch free. The alternate alphabet from RFC 4648 replaces + with - and / with _, so the output needs no escaping in URL paths, queries or file names, and it reads as a single clean token in a log line. The two ready-made encoders are URLEncoding (padded) and RawURLEncoding (unpadded):
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
That one input, three outputs: the standard version needs a percent-escape for its plus sign, the URL-safe version is one token, and the raw version drops the padding too. The typical Go jobs for each: opaque identifiers that a service generates and then stores in URLs, routes or file names; API tokens that clients paste into query strings; anything that will appear in a log line where a plus or a slash is one character away from being mistaken for syntax.
The discipline that keeps this clean is the same as everywhere in this article: the variant is a contract with the consumer. If the other side expects standard base64 and you send URL-safe, its decoder fails at the first dash, and the error will be a byte offset near the end of a perfectly good string, which is not an obvious thing to debug. When in doubt, ask what the other side expects, read the spec it points to, and pick the encoder from the destination, not from habit.
Packing JWTs
JSON Web Tokens are the most visible consumer of base64 in modern APIs, and they pin down the exact variant: JWS compact serialization, per RFC 7515, is three base64url segments with no padding, joined by dots. Header, payload, signature. That means the encoder of choice for anything you build by hand is 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)
}
Read that example as a lesson in what the format is, not as a recommendation to ship it: it shows exactly where the base64 sits (twice before signing, once after) and why the signature covers the encoded segments, not the raw JSON. In production, sign and verify with a maintained library, because JWT has a long tail of mistakes (clock skew on expiry, algorithm confusion, missing audience checks) that the base64 layer cannot see. The de facto Go library is github.com/golang-jwt/jwt/v5, installed with 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)
}
The library performs the base64url encoding of every segment internally, so you never touch encoding/base64 at all, which is the best outcome: one fewer place for a padding or alphabet mistake to hide. And note the guard it gives you for free: v5 rejects tokens that claim alg=none unless you explicitly opt in with its UnsafeAllowNoneSignatureType constant, which is the protection you want without thinking about it.
Text, Bytes, and Unicode
Go's stance on this question is the shortest of any major language, and it is the reason base64 is so pleasant here: a string in Go is a read-only sequence of bytes, and the text in your program is UTF-8. There is no hidden encoding layer, no "the string is actually UTF-16" surprise, and no charset flag to set. When you write EncodeToString([]byte(myText)), you are encoding the UTF-8 bytes of the text, full stop:
s := "Café ☕"
packed := base64.StdEncoding.EncodeToString([]byte(s))
fmt.Println(packed) // Q2Fmw6kg4piV
That one line is the whole story for modern text, including emoji and CJK: base64 operates on bytes, UTF-8 is just a byte sequence, and every decoder on the other side that follows the same convention will give you the same string back. The []byte(...) conversion is a view, not a copy, so there is no cost to it either.
The one case where the story gets longer is legacy data: bytes that were produced by a Windows-1252, Shift JIS or ISO-8859-1 system and are not valid UTF-8. If you base64-encode those bytes as-is, you have faithfully transported broken text, which is not what anyone wanted. The fix is to normalize before you encode, using golang.org/x/text, so the base64 string carries clean UTF-8 from the moment it leaves your program:
import (
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
legacy := []byte{0x43, 0x61, 0x66, 0xE9} // "Café" in Windows-1252
utf8, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), legacy)
if err != nil {
panic(err)
}
packed := base64.StdEncoding.EncodeToString(utf8)
The same module covers japanese, korean, simplifiedchinese and traditionalchinese in addition to charmap. The practical rule: convert once, at the boundary where legacy bytes enter your program, and from then on everything you encode is UTF-8. Do not convert twice, do not guess, and never let a non-UTF-8 payload sneak into a base64 string that a modern consumer will decode and display.
Measuring the Encoder
The encoder is a table lookup with no branching on the input and no allocation beyond the output string, and it shows in the numbers. On a recent desktop CPU running Go 1.26, encoding 500 bytes takes roughly three tenths of a microsecond with two allocations, which works out to on the order of a gigabyte and a half per second. A megabyte of data encodes in well under a millisecond; the encoder will rarely be anything you can feel.
The one lever worth knowing is the allocation profile in hot loops. EncodeToString allocates the output string on every call, which is the right trade for the 99 percent case. If you are encoding thousands of chunks per second into a growing buffer, AppendEncode, added in Go 1.22, appends the encoded bytes to a slice you reuse and performs no allocation in steady state once the buffer has grown to size:
var out []byte
for _, chunk := range chunks {
out = base64.StdEncoding.AppendEncode(out, chunk)
}
Use EncodeToString for one-offs, AppendEncode for tight loops, and NewEncoder for streams and files. Whichever you pick, remember that the network or the disk around the encoder is almost always the slow part, so profile the whole path before you optimize the alphabet.
Security Considerations
The most important security sentence in this article: base64 is not encryption, and "we base64 it first" is not a security measure. The alphabet makes data text-safe, not secret, and anyone with a browser's developer tools can read your base64 in an instant. Confidentiality comes from TLS and from access control, and base64's job is to get bytes across a text-only channel without corrupting them. Keep those two jobs separate in your design and in your documentation, and you avoid the classic "the password is protected, look, it is base64" review comment.
The second consideration is size. Because the format expands by a third, every limit in your system has a base64 version: an API that accepts 4 megabyte of JSON accepts roughly 3 megabytes of original data when the payload is a base64 field, a URL with a length budget gets shorter in raw bytes when the token is URL-safe and unpadded, and a database column sized for the raw value may be too small for the encoded one. Do the arithmetic with EncodedLen before you store, send or limit, and remember that the expansion is on the input you start with, not the string you end up with.
Third, think about where the encoded string can be observed. Base64 strings are log-friendly and screen-friendly, which is a feature, until a 20 megabyte attachment base64s into 26 megabytes of text that your access log dutifully records on every request. Log the length, the first few dozen characters, and the identifier, not the payload, and you keep your logs readable and your disk alive. Finally, in URLs, prefer the URL-safe variant so that your tokens do not spend half their characters as percent-escapes, which bloats the URL and occasionally trips a gateway or a proxy that has a strict view of what belongs in a query string.
Fun Facts and Go Oddities
A few facts that are specific to this package, for the times you want to be right in a code review:
EncodeToStringis the only entry point in the whole package that has no error return. Encoding cannot fail in Go, which is a rare and quiet kind of freedom: any byte is legal input, and the only way to get a bad string is to pick the wrong alphabet for the channel.EncodedLenis pure arithmetic,(n+2)/3*4for padded encodings, computed with no allocation and no loop. It exists so you can size buffers and quotas without ever encoding a byte.- The internal stream encoder hides a 3-byte input buffer and a 1024-byte output buffer, which is why
NewEncoderwrites in chunks and why the last partial block can only leave throughClose. The buffers are the reason for the trap. - The documentation says it is an error to write after calling
Close, but the runtime does not enforce the sentence. A lateWriteis accepted, appends a fresh block, and produces a string with padding in the middle of it: invalid base64, generated politely, with no error value in sight. - Go's encoder has never wrapped its output at 76 characters, and it is the one mainstream implementation where a megabyte of base64 is one line. Your MIME-wrapping helper is a personal project, which is also a good way to remember that the line breaks in email base64 are a MIME convention, not a base64 requirement.
- As of August 2026, more than 244,000 public packages on pkg.go.dev import
encoding/base64. Whatever your Go program is, it is almost certainly doing base64 somewhere, whether you know it or not. - The Go 1 compatibility promise applies to this package with special force: the output of a program that encoded a string in 2013 is byte-identical on Go 1.27 today. Base64 strings are, in Go, effectively immortal.
The Mistakes That Keep Resurfacing
The encoding mistakes that keep resurfacing in Go codebases, in roughly the order they arrive:
- Forgetting
Closeon the stream encoder, and shipping a string that is missing its last one or two bytes. The bug survives every test that uses input whose length is a multiple of three, which is how it reaches production. - Closing the file before the encoder, so the final partial block flushes into a file handle that has already gone. The output is truncated by exactly the same amount, and the error only shows up on odd-sized inputs.
- Expecting 76-character line breaks in MIME or email output and being confused when Go hands you one long line. The wrapping is a channel convention, and in Go it is your code's job to apply it.
- Using the standard alphabet inside URLs, then spending an afternoon chasing 404s and 400s that are really a percent-encoding problem. If the string will live in a URL, start from
URLEncodingorRawURLEncoding. - Emitting padding where the consumer forbids it: JWT segments, some token formats, a few strict parsers. The raw variants exist for exactly this reason, and the error message from the other side is often a byte offset at the very end of your string.
- Base64-ing a secret and calling it protection. It is not. The header, the token, the "encrypted" field: readable by anyone in half a second. Use TLS, use hashing where a hash is what the protocol wants, and let base64 do its one honest job.
- Forgetting the 33 percent when setting limits: body sizes, column widths, URL budgets, quota checks. The arithmetic is one call to
EncodedLen, and the cost of skipping it is a 413 or a truncated column in production. - Encoding text that is not UTF-8, which faithfully transports the breakage. Normalize legacy charsets with
golang.org/x/textbefore you encode, so the base64 string carries clean bytes. - Writing to the encoder after closing it, out of habit or out of a retry loop. No error is raised, and the output is silently invalid.
- Assuming the decoder on the other end is as forgiving as Go's. Go skips newlines anywhere, but other languages and parsers are stricter about whitespace and about line length, so match the channel's convention instead of the Go runtime's mood.
The Flip Side
That is the encoding side of the story: one method that cannot fail, four encoders matched to the channels their strings will travel, a stream encoder with one mandatory Close, and a format that expands your data by a third and never, ever wraps its lines. Pick the alphabet from the destination, close your encoders, do the size arithmetic up front, and base64 in Go stays the quiet, zero-dependency utility it has been since 2009.
And when the traffic reverses, when your program receives one of these strings and has to open it, the related article on Base64 decoding in Go covers that side in detail: the decoder's tolerance rules, the error offsets that tell you the byte where input goes wrong, strict mode for picky protocols, and the same four encodings from the other direction.
Last updated: 2026-08-29
Related article: Base64 Decoding in Go: A Complete Guide