编码和解码字符串
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(_ 处) |
aGk | illegal base64 data at input byte 0 |
aGk=!! | illegal base64 data at input byte 4 |
aGk== | illegal base64 data at input byte 4 |
aGVs bG8K | illegal 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 换行的输入也能解码。空格和其他字符则会报错。