Закодировать и декодировать строку
Строки в 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: он дописывает последние один-два байта и дополнение. Забытый Close() – классическая ошибка с Base64 в Go: вывод выглядит нормально, но в нём не хватает до четырёх символов. Декодирование устроено симметрично:
r := base64.NewDecoder(base64.StdEncoding, f)
io.Copy(dst, r)
На тестовом файле в 300 КБ оба направления совпали с функциями, работающими в памяти. Декодер пропускает \r и \n, поэтому сразу читает данные с переносами строк в стиле MIME. Чтобы заранее вычислить размер буфера, есть EncodedLen: 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 объяснена каждая из них.
Подводные камни
- Декодирование частей JWT через
StdEncoding. Токены – это 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, или вставляйте\r\nчерез каждые 76 символов сами.
Вопросы и ответы
Чем URLEncoding отличается от RawURLEncoding?
Обе используют URL-safe алфавит с - и _. URLEncoding пишет дополнение = и требует его при декодировании; RawURLEncoding дополнение не пишет и отвергает. JWT используют RawURLEncoding.
Что значит «illegal base64 data at input byte 0»?
Это base64.CorruptInputError, а число – смещение, на котором декодирование сломалось. Байт 0 в короткой строке обычно означает неверную длину, чаще всего пропущенное дополнение; для строк без дополнения берите кодировку Raw.
Игнорирует ли декодер Base64 в Go переводы строк?
Да. \r и \n пропускают все четыре кодировки, поэтому строки, перенесённые в стиле MIME, декодируются. Пробелы и другие символы – ошибка.