上传文件
  1. base64hub
  2. 在代码中使用 Base64
  3. Go (Golang)
代码

Go 中的 Base64

Go 的 encoding/base64 包提供了四种现成的编码和流式包装器。从四种里选对一种,基本就完成了大半工作。下面的输出均来自 Go 1.25。

编码和解码字符串

Go 字符串本身就是 UTF-8,所以只需转换为 []byte:

package main

import (
	"encoding/base64"
	"fmt"
)

func main() {
	enc := base64.StdEncoding.EncodeToString([]byte("héllo wörld"))
	fmt.Println(enc) // aMOpbGxvIHfDtnJsZA==

	dec, err := base64.StdEncoding.DecodeString(enc)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(dec)) // héllo wörld
}

一定要检查 err。失败时,DecodeString 可能会把已经解码的字节和错误一起返回,不检查就使用结果,会悄无声息地拿到被截断的数据。

四种编码:Std、URL、Raw

变量字母表填充<<???>> 编码结果
StdEncoding+ /有PDw/Pz8+Pg==
URLEncoding- _有PDw_Pz8-Pg==
RawStdEncoding+ /无PDw/Pz8+Pg
RawURLEncoding- _无PDw_Pz8-Pg
d := []byte("<<???>>")
base64.URLEncoding.EncodeToString(d)    // PDw_Pz8-Pg==
base64.RawURLEncoding.EncodeToString(d) // PDw_Pz8-Pg

b, err := base64.RawURLEncoding.DecodeString("PDw_Pz8-Pg") // "<<???>>", nil

解码时对填充的要求在两个方向上都很严格。带填充的编码拒绝不带填充的输入,Raw 编码则拒绝 =:

base64.StdEncoding.DecodeString("aGk")
// illegal base64 data at input byte 0
base64.RawURLEncoding.DecodeString("PDw_Pz8-Pg==")
// illegal base64 data at input byte 10

如果你要接受来自多个来源的输入,就先用 strings.TrimRight(s, "=") 去掉末尾的 =,再用 Raw 编码解码。你也可以用 base64.NewEncoding(alphabet) 和 .WithPadding(base64.NoPadding) 构造自定义编码。各种变体分别出现在哪里,见 Base64 与 Base64URL 的区别。

编码和解码文件

data, err := os.ReadFile("hello.txt")
if err != nil {
	log.Fatal(err)
}
fmt.Println(base64.StdEncoding.EncodeToString(data)) // aGVsbG8K

raw, err := base64.StdEncoding.DecodeString("aGVsbG8K")
if err != nil {
	log.Fatal(err)
}
os.WriteFile("copy.txt", raw, 0o644)

用 NewEncoder 和 NewDecoder 流式处理

处理大文件时,包装一个 io.Writer 或 io.Reader 即可,内存占用保持不变:

in, _ := os.Open("big.bin")
defer in.Close()
out, _ := os.Create("big.b64")
defer out.Close()

w := base64.NewEncoder(base64.StdEncoding, out)
if _, err := io.Copy(w, in); err != nil {
	log.Fatal(err)
}
if err := w.Close(); err != nil { // flushes the last partial block
	log.Fatal(err)
}

w.Close() 不会关闭 out,它负责写出最后一两个字节和填充。漏掉它是 Go 中最经典的 Base64 bug:输出看起来没问题,但最多会少四个字符。解码是对称的:

r := base64.NewDecoder(base64.StdEncoding, f)
io.Copy(dst, r)

对一个 300 KB 的测试文件,流式处理和内存中处理的结果完全相同。解码器会跳过 \r 和 \n,所以能直接读取按 MIME 换行的输入。要预先计算缓冲区大小,EncodedLen(5) 返回 8,RawStdEncoding.EncodedLen(5) 返回 7;背后的公式见 体积开销指南。

Go 1.22 新增了 AppendEncode 和 AppendDecode,它们把结果追加到已有的切片上,构造请求头时很方便:

dst := base64.StdEncoding.AppendEncode([]byte("Basic "), []byte("user:pass"))
fmt.Println(string(dst)) // Basic dXNlcjpwYXNz

Basic Auth 请求头生成器 可以在浏览器中生成同样的值。

校验与 CorruptInputError

每个解码错误都是一个 base64.CorruptInputError,即第一个错误字节的 int64 偏移量:

_, err := base64.StdEncoding.DecodeString("PDw_Pz8-Pg==")
var ce base64.CorruptInputError
if errors.As(err, &ce) {
	fmt.Println(err) // illegal base64 data at input byte 3
}

我们用 StdEncoding 得到的结果:

输入错误
PDw_Pz8-Pg==illegal base64 data at input byte 3(_ 处)
aGkillegal base64 data at input byte 0
aGk=!!illegal base64 data at input byte 4
aGk==illegal base64 data at input byte 4
aGVs bG8Killegal base64 data at input byte 4(空格)
aGl=无错误,返回 hi
"aGVs\nbG8K"无错误,跳过换行符

aGl= 的填充位不为零。要拒绝这类不规范的输入,请使用 .Strict():

_, err = base64.StdEncoding.Strict().DecodeString("aGl=")
// illegal base64 data at input byte 3

Base64 校验工具 会高亮同样的位置;常见原因见 Base64 常见错误。

常见陷阱

  • 用 StdEncoding 解码 JWT 的各部分:令牌是不带填充的 Base64URL。请用 RawURLEncoding,或者先用 JWT 解码器 查看令牌。
  • 忽视部分结果:"aGk=!!" 会同时返回 "hi" 和一个错误。当 err != nil 时不要使用这些字节。
  • 忘记对编码器调用 Close():最后一个块会丢失。
  • 复制粘贴带来的空格:解码器只跳过 \r 和 \n。请先用 strings.Join(strings.Fields(s), "") 去掉其他空白。

版本说明

  • RawStdEncoding 和 RawURLEncoding:Go 1.5。
  • Strict():Go 1.8。
  • AppendEncode、AppendDecode:Go 1.22。
  • 这个包没有按 MIME 换行的编码器;电子邮件可以交给 mime 和 net/mail 包处理,或者自己每 76 个字符插入一个 \r\n。

常见问题

URLEncoding 和 RawURLEncoding 有什么区别?

两者都使用带 - 和 _ 的 URL 安全字母表。URLEncoding 会写出 = 填充,解码时也要求有填充;RawURLEncoding 省略填充,并拒绝带填充的输入。JWT 使用的是 RawURLEncoding。

illegal base64 data at input byte 0 是什么意思?

这是一个 base64.CorruptInputError,数字是解码失败的偏移位置。如果是较短的字符串在第 0 个字节处出错,通常意味着长度不对,多半是缺了填充;对不带填充的输入,请用 Raw 编码。

Go 的 Base64 解码器会忽略换行符吗?

会。四种编码都会跳过 \r 和 \n,所以按 MIME 换行的输入也能解码。空格和其他字符则会报错。

继续阅读