Base64 in Go

Go offers four encodings rather than one, and its decoder is the strictest of any language we have measured — it rejects padding mismatches that JavaScript accepts without comment.

Every snippet and output below was executed on Go 1.22 (darwin/arm64).

The four encodings

b := []byte("Hello, 안녕 🎉")

base64.StdEncoding.EncodeToString(b)     // + /  with padding
base64.URLEncoding.EncodeToString(b)     // - _  with padding
base64.RawStdEncoding.EncodeToString(b)  // + /  no padding
base64.RawURLEncoding.EncodeToString(b)  // - _  no padding

// all four: "SGVsbG8sIOyViOuFlSDwn46J"

Two independent axes — alphabet and padding — give four combinations. And note the trap in that output: for this input all four are identical, because the encoded form happens to contain no + or / and its length is already a multiple of four. A mismatched encoding passes every test until a particular payload produces one of those characters.

Choose by destination. RawURLEncoding for URLs, filenames and JWT segments; StdEncoding for binary in JSON or a database column.

No conversion step

text := "Hello, 안녕 🎉"
b := []byte(text)     // already UTF-8, no error case

len(text)          // 18   ← bytes, not characters
len([]rune(text))  // 11   ← characters

A Go string is UTF-8 by definition, so []byte(s) is a reinterpretation rather than a conversion. This is the cleanest of the three languages: JavaScript needs TextEncoder because btoa throws on non-Latin-1, and Python raises TypeError if handed a str. Go just works.

The residual trap is len(), which counts bytes. Use []rune or utf8.RuneCountInString when you mean characters.

Go is the strict one

We ran the same decode across all three languages. The results differ more than most people expect:

InputJavaScript atobPython b64decodeGo StdEncoding
"YQ=="
"YQ" — no padding✅ returns "a"❌ Incorrect padding❌ illegal base64 data
"w7vDv8O-" — URL alphabet❌ (with validate)❌ at byte 7

Go is strict in the other direction too: RawStdEncoding.DecodeString("YQ==") fails at byte 2, because the padding should not be present for that encoding. The variant you pick has to match the data exactly.

That is a feature. A mismatch fails at the decode with a byte offset rather than producing plausible-looking wrong bytes that surface three systems later.

It is worth seeing the three languages together, because each made a different call on the same question. JavaScript is permissive and will decode an unpadded string; Python enforces padding but tolerates stray characters unless you ask it not to; Go enforces both the alphabet and the padding for whichever encoding you named. None is wrong, but code moving values between them cannot assume the value that worked on one side will be accepted on the other.

Decoding a JWT segment

// ✗ fails twice: wrong alphabet AND missing padding
base64.StdEncoding.DecodeString(segment)

// ✓ JWT segments are base64url without padding
base64.RawURLEncoding.DecodeString(segment)

This is the case where the strictness pays. Both failure modes are reported with the offending byte offset instead of silently yielding something almost right. A JWT library handles this internally — you only need it when inspecting a token by hand.

Streaming, and the Close that matters

enc := base64.NewEncoder(base64.StdEncoding, out)
io.Copy(enc, in)
if err := enc.Close(); err != nil { ... }   // ← required

The encoder buffers a partial group of up to two bytes and only flushes it, with its padding, on Close. Skip that call and the output is silently short by one to three characters — valid-looking Base64 that fails to decode. Defer it, and check the error rather than discarding it, since it is the only place that final write can fail.

Frequently asked questions

Which Base64 encoding should I use in Go?

There are four and the choice is mechanical once you know the two axes. StdEncoding uses + and / with = padding; URLEncoding swaps those for - and _; the Raw variants of each drop the padding. Use RawURLEncoding for anything going into a URL, a filename or a JWT segment, since that combination matches what base64url means everywhere else. Use StdEncoding for ordinary binary in JSON or a database column. We confirmed all four produce identical output for input containing no + or / characters, which is exactly why choosing the wrong one can pass every test you write and then fail on one particular payload in production, long after the code shipped.

Do I need to convert a Go string before encoding?

No. A Go string is already UTF-8 bytes, so []byte(s) is a direct reinterpretation with no conversion step and no error case. This is the cleanest of the three languages we have measured: JavaScript needs TextEncoder because btoa refuses anything above Latin-1, and Python raises TypeError if you hand str to b64encode. Go simply works, because the language guarantees source files are UTF-8 and string literals carry that through unchanged. The one thing to watch is that len() on a string returns bytes rather than characters — our test string measured 18 bytes but 11 runes — so do not use it to reason about text length; use utf8.RuneCountInString or convert to []rune when you mean characters rather than storage.

Why does Go reject Base64 that other languages accept?

Because Go's decoders enforce padding in both directions and most others do not. We measured the same input across three languages: StdEncoding.DecodeString("YQ") fails with "illegal base64 data at input byte 0" because the padding is missing, while JavaScript's atob("YQ") returns "a" quite happily. Go is also strict the other way — RawStdEncoding rejects "YQ==" at byte 2, because for that encoding the padding should not be present at all. The upside is that a mismatched variant fails immediately rather than producing plausible wrong bytes further downstream, which is the failure mode you actually want. A loud error at the boundary costs minutes; wrong bytes three systems downstream cost days and a corrupted record.

How do I decode a JWT segment in Go?

Use RawURLEncoding, which is the exact combination JWT segments are written in — URL-safe alphabet, no padding. Reaching for StdEncoding instead fails twice over: it rejects the hyphen and underscore characters, and it rejects the missing padding. We confirmed both errors. This is the one case where Go's strictness saves you time, because the error names the byte offset of the offending character rather than silently decoding to something that looks almost right. Note that jwt libraries handle this internally; you only need it when inspecting a token by hand, which is exactly when a clear error saves the most time, since you are already debugging.

How do I stream large data through Base64 in Go?

Use base64.NewEncoder and base64.NewDecoder, which wrap an io.Writer and io.Reader respectively, so a large file never has to sit in memory as a single []byte. The one thing people forget is that the encoder must be closed: it buffers a partial group of up to two bytes, and only Close flushes that final group with its padding. Skipping the Close silently truncates the output by one to three characters, which then fails to decode at the far end with an error that points at the data rather than at the missing Close. Defer it as you would any writer, and check the error Close returns rather than discarding it, since that final write is the one place the flush can fail silently otherwise.

References

Check a value against the Base64 encoder and decoder, or compare with Python and JavaScript. More tools at withuse.io/tools.