Go での Base64 デコード:完全ガイド
APIレスポンスの中に、長い文字列がひそんでいます。それは値であることを装っていますが、実際はファイル、トークン、画像、あるいはあなたのシステムより3年古いシステムからのメッセージなのです。ログ行、データベースの行、JSONペイロードのどこかで、base64文字列は絶えず顔を出します。標準の64文字の文字表、ときどきプラスとスラッシュが添い、ときどきダッシュとアンダースコアが添い、そしてたまには署名のように最後尾に止まったイコール記号2つ、というわけです。
このサイトのホームページは、すでにフォーマット自体を説明しています。64個の印刷可能文字がそれぞれ6ビットずつ運び、入力の3バイトに対して文字4つ、そして作業の仕上げにパディング、というものです。そこでこの記事は、面白い決断が潜んでいる仕事の半分へまっすぐ進みます。Goでそれらの文字列を開くことです。朗報は、Goはこれをするには素晴らしい場所だということです。標準ライブラリのパッケージ1つ、依存はゼロ、デフォルトでは厳格だが改行には寛容なデコーダー、そして失敗した正確なバイトを指し示すエラーメッセージがあります。
Goに同梱されているもの
必要なものはすべて、すでに標準ライブラリにあります。パッケージの名は encoding/base64。ソースファイルには、この言語が生まれた年に付けられた2009年の著作権ヘッダーが今なお残っており、有効化するエクステンションも、取得するモジュールも、切り替える設定もありません。go version があなたのマシンで何かを印刷するなら、あなたはすでに道具全体を所有しているのです。
この記事を書いている時点での最新リリースは Go 1.27.1 で、2026年9月1日にリリースされました。サポート対象のもう一方のラインはGo 1.26(現在は1.26.8)です。base64のAPIは両ラインで同一であり、Go 1の互換性保証のおかげで、今日base64をデコードするプログラムは、将来のあらゆるリリースでもまったく同じ動きをしてくれます。Go本体は、go.dev/dl の公式ターボール(go1.27.1.linux-amd64.tar.gz のようなもので、/usr/local に展開します)、ディストリビューションのパッケージマネージャー(Ubuntuベースのシステムでは sudo apt install golang-go)、または複数のGoバージョンを並べて使いたいなら golang.org/dl ラッパーから入手できます。
Goがインストールされれば、go doc encoding/base64 はAPI全体を読みやすいカラムで印刷してくれます。記憶を呼び戻すにはこれが最速です。この記事がどこかで使う唯一の追加物は、レガシー文字セット用の golang.org/x/text で、go get golang.org/x/text でインストールします。それは独自のセクションに1回だけ登場し、それ以外は純粋な標準ライブラリだけです。
初めてのデコード
Goでのデコード生活の9割は、Encoding 型のメソッド1つで済みます:
func (enc *Encoding) DecodeString(s string) ([]byte, error)
base64文字列を渡すと、それが表すバイトを返してくれます。入力がわがままを働かせたら、エラーもつきます:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
decoded, err := base64.StdEncoding.DecodeString("TWFu")
if err != nil {
fmt.Println("decode failed:", err)
return
}
fmt.Println(string(decoded)) // Man
}
そのシグネチャについて、覚える価値があることが2つあります。第一に、結果は文字列ではなく []byte です。展開したバイトが、完全に正当なbase64でありながら、テキストとしては完全に最悪なものである可能性があるためです。PNGヘッダー、圧縮アーカイブ、バイナリプロトコルなどですね。ペイロードがテキストだと分かっているときだけ、string(...) で包んでください。第二に、このメソッドは常に2つの値を返します。nilのエラーは、文字列がクリーンなbase64だったことを意味し、nilでないエラーは入力のどこかが壊れていたことを意味します。その場合、受け取ったバイト列は空ではなく、部分的な結果になっていることもあります。その挙動の両面は、下のエラーのセクションで確認できます。
4つのデコーダー、1つの問い:どの文字表か?
Goには、完成済みの Encoding 値が4つ同梱されています。正しいものを選ぶことは、あらゆるデコードにおける最初の真の決断です。下記のテーブルがその座席表です:
| 変数 | 文字表 | パディング | どこで出会うか |
|---|---|---|---|
StdEncoding |
A-Z a-z 0-9 + / |
= |
MIMEメール、data URL、HTTP Basic認証、PEMファイル、一般的なJSON |
URLEncoding |
A-Z a-z 0-9 - _ |
= |
URLのパスとクエリ、ファイル名 |
RawStdEncoding |
A-Z a-z 0-9 + / |
なし | コンパクトな生成元が作る、パディングなしの標準base64 |
RawURLEncoding |
A-Z a-z 0-9 - _ |
なし | JWTセグメント、コンパクトなAPI識別子 |
選ぶ最速の方法は、データ自体を見ることです。+ または / を含む文字列は、標準文字表の文字列しかありえません。そのため、Std デコーダーのいずれかが必要です。- または _ を含む文字列は、RFC 4648のURL安全な変種なので、URL デコーダーのいずれかが必要です。それから尾部をチェックします。末尾に = があるならパディング付きの変種、なければ Raw の方です。間違った選択がどんな感覚か、次の通りです:
decoded, err := base64.StdEncoding.DecodeString("P29_")
// err: illegal base64 data at input byte 3
// アンダースコアは標準文字表に含まれないため、
// デコーダーは認識しない最後の文字で止まる
decoded, err = base64.URLEncoding.DecodeString("P29_")
// decoded は3バイト 0x3f 0x6f 0x7f で、err は nil
独自の64文字の文字表を定義したプロデューサーからのデータをデコードする場合は、base64.NewEncoding("...64 chars...") がそれ用のデコーダーを構築してくれます。文字表はちょうど64個の一意なバイト値で、改行を含んではなりません - 含めると関数はパニックします - ドキュメントは文字表がパディング文字を除外することを要求していますが、関数はそれを強制していません - '=' を含む文字表でも、パニックせずに受け入れられます。日常の仕事ではめったに必要になりませんが、そこにあるのです。そして、プライベートなスキームをデコードする唯一の方法でもあります。
寛容性の問題:Goはどんな入力を許すのか?
すべてのbase64デコーダーは、1つの居心地の悪い決断をしなければなりません。どんなゴミまで飲み込むか、ということです。Goの答えは、慎重に引かれた一線です。寛容な側では、デコーダーは入力内のどこに現れてもキャリッジリターンとラインフィードをスキップします。そのため、メールクライアントやPEMツールに多くの行へ折り返された文字列も、前処理なしでデコードできます:
decoded, err := base64.StdEncoding.DecodeString("T\nW\nF\r\nu")
// decoded は「Man」、err は nil
// 文字列内の \r と \n はすべて単純に無視された
厳格な側では、それ以外のすべてが禁止事項です。スペース、タブ、PDFからコピーしてきたゼロ幅文字、ヘッダーから紛れ込んだ余計なコロン:デコーダーが文字表にない改行以外の文字に遭遇した瞬間、それは停止してオフセットを報告します。そして、すでにデコード済みのものはそのまま保持します:
decoded, err := base64.StdEncoding.DecodeString("TWFu junk")
// decoded は「Man」(スペースの前部分)、
// err は:illegal base64 data at input byte 4
この組み合わせは人々を驚かせます。失敗したデコードでも、使える半分の結果を手にできることがあるからです。それが機能なのか危険なのかはあなた次第ですが、要点は err == nil が、データが完全である唯一の条件だということです。
パディングには独自のルールがあり、それはパディング付きとrawの変種で異なります。パディング付きのデコーダーはグループで動きます。グループとは、実在する4文字、または2文字の後に == が続くもののどちらかです。1文字だけでは完全にグループは成立しません。そのため "T" は失敗し、"TWF" も失敗します。3文字には1つのパディング記号が必要で、それが欠けているからです。rawのデコーダーはパディングの要件を捨てますが、グループの4文字中3文字が欠ける長さもやはり受け入れられません。そのため "T" はそこでも失敗し、"TW" は1バイトへ気持ちよくデコードされます。
base64.StdEncoding.DecodeString("T") // 入力バイト 0 でエラー
base64.StdEncoding.DecodeString("TWF") // 入力バイト 0 でエラー
base64.RawStdEncoding.DecodeString("TW") // 1バイト、エラーなし
base64.StdEncoding.DecodeString("TWFu====") //「Man」とバイト 4 でエラー
もう1つ、気分を切り替えるものがあります。Go 1.8で追加された Strict() です。厳格モードでは、デコーダーはRFC 4648の3.5節にある正規形を強制します。最終グループの未使用の末尾ビットはゼロでなければならない、というものです。通常モードはそこを気にしません。それらのビットは単に使われないだけなので、"Qm==" はバイト B に文句なしでデコードされます。厳格モードはそれを拒否します:
decoded, err := base64.StdEncoding.DecodeString("Qm==")
// decoded は「B」、err は nil(末尾ビットは破棄された)
decoded, err = base64.StdEncoding.Strict().DecodeString("Qm==")
// err は:illegal base64 data at input byte 2
ドキュメントが指摘するように、厳格モードでも改行はスキップされることに注意してください。正規エンコーディングを気にするプロトコルを扱っているとき、あるいは手ぬるい生成元を静かに受け入れる代わりに拒否したいときに、Strict() を使います。
場所を教えてくれるエラー
このパッケージでのすべての失敗は、具体的で検査できる値として届きます。入力が文字表にないものを含んでいたり、パディングが間違っていたりすると、デコーダーは base64.CorruptInputError を返し、そのメッセージには問題のバイトオフセットが含まれます:
type CorruptInputError int64
func (e CorruptInputError) Error() string {
return "illegal base64 data at input byte " + strconv.FormatInt(int64(e), 10)
}
そのオフセットこそが、「何かが失敗した」と「この900キロバイトの文字列の4,102番目の文字が、クリップボードから漂い込んだタブだ」との違いです。いつものGoの慣用句でキャッチします:
package main
import (
"encoding/base64"
"errors"
"fmt"
)
func main() {
_, err := base64.StdEncoding.DecodeString("TWF$")
var corrupt base64.CorruptInputError
if errors.As(err, &corrupt) {
fmt.Printf("bad byte at offset %d: %v\n", int(corrupt), err)
// bad byte at offset 3: illegal base64 data at input byte 3
return
}
fmt.Println("not a corrupt-input error:", err)
}
人々を最も混乱させる入力たちの症状表がこちらです:
| 入力(StdEncoding) | 結果 | 理由 |
|---|---|---|
TWF$ |
バイト 3 でエラー | $ は文字表に含まれない |
T |
バイト 0 でエラー | 1文字では完全なグループは成立しない |
TWF |
バイト 0 でエラー | 3文字には1つの = が必要で、それが欠けている |
TWFu junk |
Man とバイト 4 でエラー |
スペースは改行ではないため、そこでデコードが止まる |
TWFu\t |
Man とバイト 4 でエラー |
タブはスキップされない。スキップされるのは \r と \n のみ |
T\nW\nF\nu |
Man、エラーなし |
改行はどこにても無視される |
==== |
バイト 0 でエラー | グループの先頭のパディングは不正 |
(empty string) |
空の結果、エラーなし | 0バイトのbase64は0バイトにデコードされる |
実用的なヒントを1つ。本番でデコードが失敗したら、オフセットとその周辺の短いウィンドウをログに記録してください。90%の確率で、“corrupt”なバイトとは、トランスポート、クリップボード、PDFビューアーが文字列に忍び込ませた空白で、修正はトリムやストリップで済み、設計変更ではありません。
ファイルを開く
Base64ファイルは、base64を含むテキストファイルにすぎません。そのため、Goのいつものファイルツールがそのまま適用できます。メモリに余裕を持って収まるファイルなら、全部読んで文字列をデコードします:
package main
import (
"encoding/base64"
"fmt"
"io"
"os"
)
func main() {
f, err := os.Open("payload.b64")
if err != nil {
fmt.Println("open failed:", err)
return
}
defer f.Close()
raw, err := io.ReadAll(f)
if err != nil {
fmt.Println("read failed:", err)
return
}
decoded, err := base64.StdEncoding.DecodeString(string(raw))
if err != nil {
fmt.Println("decode failed:", err)
return
}
fmt.Println("decoded", len(decoded), "bytes")
}
大きなファイルでは、より良いパターンはストリーミングです。ここで活躍するのは、パッケージAPIの残り半分のほうで、NewDecoder は任意の io.Reader をbase64デコードするリーダーで包み込みます。そのため、ペイロード全体をメモリに保持することなく、ファイルからファイルへパイプできます:
in, err := os.Open("payload.b64")
if err != nil {
panic(err)
}
defer in.Close()
dec := base64.NewDecoder(base64.StdEncoding, in)
out, err := os.Create("payload.bin")
if err != nil {
panic(err)
}
defer out.Close()
written, err := io.Copy(out, dec)
if err != nil {
panic(err)
}
fmt.Println("wrote", written, "bytes")
DecodeString が行う追加のアロケーションを避けたい場合は、中間の選択肢があります。Decode は、あなたが制御する宛先バッファに書き込みます。DecodedLen でサイズを計算してください。これは、与えられた入力長に対する出力バイト数の最大値を返します:
raw, err := os.ReadFile("payload.b64")
if err != nil {
panic(err)
}
buf := make([]byte, base64.StdEncoding.DecodedLen(len(raw)))
n, err := base64.StdEncoding.Decode(buf, raw)
if err != nil {
panic(err)
}
data := buf[:n] // 実際のデコード後のサイズ
fmt.Println(len(data), "bytes")
ただし、最後に挙げた方には注意してください。Decode は、バッファのサイズ付けをあなたに信頼して任せます。小さすぎると、メソッドはエラーを返さずに、インデックス範囲外でパニックします。使うべき数値は DecodedLen で、len(raw) ではありません。
GoのためのBase64コマンドライン
Unixシステムはcoreutilsに base64 ユティリティを同梱しますが、Goには同等のバイナリがありません。Goの世界での慣用的な答えは、インストールするパッケージではなく、あなたが所有するプログラムです。encoding/base64、flag パッケージ、標準入力を軸にした小さなコマンドラインツール。パイプで入ってきたものをデコードして、生のバイトを書き出す、完成形がこちらです。約40行:
package main
import (
"encoding/base64"
"flag"
"fmt"
"io"
"os"
)
func main() {
urlSafe := flag.Bool("url", false, "use the URL-safe alphabet")
flag.Parse()
enc := base64.StdEncoding
if *urlSafe {
enc = base64.URLEncoding
}
raw, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintln(os.Stderr, "read failed:", err)
os.Exit(1)
}
decoded, err := enc.DecodeString(string(raw))
if err != nil {
fmt.Fprintln(os.Stderr, "decode failed:", err)
os.Exit(1)
}
os.Stdout.Write(decoded)
}
go build -o b64 . で1回ビルドすれば、それはMakefile、CIパイプライン、シェル関数のどこにでも置けるクロスプラットフォームのデコーダーになります。printf 'TWFu' | ./b64 は Man を印刷し、./b64 -url < token.b64 > token.bin はURL安全なトークンをファイルに展開します。この設計には、注目すべき2つの性質があります。デコード前にstdinをすべて読むため、改行を含む折り返された入力でも、デコーダーの改行寛容さのおかげで問題なくデコードできます。また、悪い入力でステータス1で終了し、クレームをstderrに書き出すため、謝るスクリプトではなく、パイプラインの中のツールのように振る舞います。GoのCLIの腕前の全てがここにあります。パッケージ1つ、フラグ1つ、標準入力、標準出力、終了コード、というわけです。
URL安全なデコード
URL安全な変種が存在するのは、標準文字表がURLの文法と衝突するためです。クエリ文字列では + はしばしばスペースとして読まれ、/ は新しいパスセグメントの始まりになるため、URLに埋め込まれた標準base64文字列は1文字ずつパーセントエスケープする必要があり、それは解析に遅く、見た目が悪いのです。RFC 4648の代替文字表は、+ と / を - と _ に置き換えます。どちらもあるいはURLのパス、クエリ、ファイル名の中でエスケープなしで合法です。
Goでは、切り替えは単に別のデコーダー変数を使うことです。データがURL安全でパディング付きなら URLEncoding、URL安全でパディングなしなら RawURLEncoding を使います。定番のケースは、URLやファイル名の中に住む識別子です:
decoded, err := base64.RawURLEncoding.DecodeString("-w9n")
// decoded は3バイト 0xfb 0x0f 0x67
// ダッシュとアンダースコアはURL安全な文字表の一部なので、
// StdEncoding が失敗するところを RawURLEncoding が扱える
実際のGoコードで出会う場所:JWTセグメント(次節でカバー)、システムが生成してURLに保存する不透明な識別子、Webサーバーやクラウドオブジェクトストレージを壊してはならないファイル名、そしてドキュメントで “base64url” を約束したあらゆるAPI。1つの警告:URL安全は、生成側と消費側の間の契約であり、データのプロパティではありません。文字列に + や / が含まれていれば、それはURL安全ではありません。決定的な話で、URLデコーダーで何度やり直しても助かりません。まず文字を見てから、デコーダーを選びましょう。
JWTの中をのぞき見る
JSON Web Tokenは、ドットで区切られた3つのbase64urlセグメントです。ヘッダー、クレームのペイロード、シグネチャで、どのセグメントにもパディングはありません。それがJWTを、Goでデコードする最も身近なものの1つにしているのです。ヘッダーとペイロードはキーなしで読めるので、デバッグでもセキュリティレビューでも覚えておく価値があります:
package main
import (
"encoding/base64"
"fmt"
"log"
"strings"
)
func main() {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiR28gRGV2ZWxvcGVyIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.NwJQAKfJpMJQuK0gEECtXtO8cIoFnDp0ovXyl7dY1BQ"
parts := strings.Split(token, ".")
if len(parts) != 3 {
log.Fatal("not a JWT: expected three dot-separated parts")
}
for i, name := range []string{"header", "payload"} {
plain, err := base64.RawURLEncoding.DecodeString(parts[i])
if err != nil {
log.Fatalf("bad %s: %v", name, err)
}
fmt.Printf("%s: %s\n", name, plain)
}
// header: {"alg":"HS256","typ":"JWT"}
// payload: {"name":"Go Developer","sub":"1234567890"}
}
デコーダーの選択に注意してください。RawURLEncoding であり、StdEncoding ではありません。JWTセグメントはURL安全な文字表を使い、パディングを持ちません。長さが4の倍数に1つか2つ足りないセグメントは、パディング付きのデコーダーで最後に失敗します。追いかけると混迷するエラーです。シグネチャセグメントはキーなしでは読めませんし、クライアントが最初の2セグメントを偽造するのを止めるものはないので、ペイロードだけを根拠に何かを信頼しようとしてはいけません。検証が必要なら、メンテナンスされたライブラリを使いましょう。事実上の標準は github.com/golang-jwt/jwt/v5 です(go get github.com/golang-jwt/jwt/v5 でインストールします):
package main
import (
"fmt"
"log"
"github.com/golang-jwt/jwt/v5"
)
func main() {
secret := []byte("hmac-secret")
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiR28gRGV2ZWxvcGVyIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.NwJQAKfJpMJQuK0gEECtXtO8cIoFnDp0ovXyl7dY1BQ"
parsed, err := jwt.Parse(token, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return secret, nil
})
if err != nil {
log.Fatal("token rejected:", err)
}
claims, _ := parsed.Claims.(jwt.MapClaims)
fmt.Println("subject:", claims["sub"])
}
このライブラリについて、知っておく価値がある詳細が2つあります。第一に、3セグメントのbase64urlエンコードとデコードは内部で処理されるため、署名や検証の際に encoding/base64 に直接触れることはありません。第二に、v5ライブラリは UnsafeAllowNoneSignatureType 定数を明示的に渡さない限り、alg=none のトークンを拒否します。これで、クラシックな“未署名トークンを受け入れてしまう”ミスから身を守れます。
Data URL
data URLは、ペイロードがデータ自体であるURLです。RFC 2397による構文は data:[mediatype][;base64],data で、任意のメディアタイプ、任意の ;base64 フラグ、カンマ、そして内容、という並びです。;base64 フラグがある場合、内容は標準base64になります。data URLがこの記事と同じセクションを共有する理由がそこにあります。ブラウザはこれを使って画像やフォントをHTMLとCSSに直接埋め込み、ページが1つ少ないリクエストで済むようにします:
<img src="data:image/png;base64,iVBORw0KGgo=" alt="pixel">
Goの標準ライブラリにはdata URLのヘルパーはありませんが、このフォーマットは strings で手動解析するほど単純です。ほとんどのGoプログラムがそうしています:
package main
import (
"encoding/base64"
"fmt"
"strings"
)
func main() {
url := "data:image/png;base64,iVBORw0KGgo="
if !strings.HasPrefix(url, "data:") {
fmt.Println("not a data URL")
return
}
rest := url[len("data:"):]
comma := strings.Index(rest, ",")
if comma == -1 {
fmt.Println("missing comma")
return
}
meta := rest[:comma] // image/png;base64
encoded := rest[comma+1:] // iVBORw0KGgo=
if !strings.HasSuffix(meta, ";base64") {
fmt.Println("this variant is percent-encoded, not base64")
return
}
mediaType := strings.TrimSuffix(meta, ";base64")
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
fmt.Println("decode failed:", err)
return
}
fmt.Println(mediaType, "carries", len(decoded), "bytes")
}
心に留めておくべき落とし穴が3つあります。第一に、;base64 フラグは任意です。これがなければペイロードはbase64ではなくパーセントエンコードされたASCIIなので、デコーダーを呼ぶ前に末尾を確認してください。第二に、メディアタイプが省略された場合は text/plain;charset=US-ASCII がデフォルトになります。画像ではめったに関係ありませんが、他の内容を解析している人々は驚きます。第三に、data URLは小さなペイロード用のトリックです。RFC自体がこのスキームは短い値にしか役に立たないと述べており、base64の33パーセントのサイズ膨張は、500キロバイトのロゴをHTMLに貼り付けられた666キロバイトの文字列に変えてしまいます。キャッシュもできず、共有もできません。アイコンやサムネイルには使いますが、動画には使わないでください。
HTTPとAPIの仕事
GoのWebサービスで最も一般的なデコードは、JSONボディのフィールドです。アップロードフォーム、APIレスポンス、ウェブフックのいずれかが、実際はファイルである文字列をあなたに手渡します。構造体にアンマーシャルして、それからフィールドをデコードします:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
)
type payload struct {
Avatar string `json:"avatar"`
}
func main() {
body := []byte(`{"avatar": "iVBORw0KGgo="}`)
var p payload
if err := json.Unmarshal(body, &p); err != nil {
fmt.Println("bad JSON:", err)
return
}
img, err := base64.StdEncoding.DecodeString(p.Avatar)
if err != nil {
fmt.Println("bad avatar:", err)
return
}
fmt.Println("avatar is", len(img), "bytes")
}
APIが標準とURL安全の両方の文字列を受け入れる場合、実用的なパターンは、1つのデコーダーを試して、末尾付近で CorruptInputError で失敗したら、諦める前にもう片方を試す、ということです。このダンスは1回だけにし、一般的戦略として“イコール記号を剥がして祈る”に逃げてはいけません。
HTTP Basic認証では、何もしなくて済みます。Goがやってくれるからです。Request.BasicAuth はGo 1.4から利用可能で、Authorization ヘッダーを分割してユーザー名とパスワードを返します。RFC 2617が定義する user:pass 対に対して、標準base64デコーダーをすでに実行済みです:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok || user != "alice" || pass != "s3cret" {
w.Header().Set("WWW-Authenticate", `Basic realm="api"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
fmt.Fprintln(w, "hello", user)
})
http.ListenAndServe(":8080", mux)
}
Basic認証は保護ではなく認証だということを覚えておいてください。ヘッダーはbase64であり、暗号化ではないので、HTTPS上でだけ送る必要があります。クライアント側なら、鏡像の呼び出しは req.SetBasicAuth(user, pass) で、標準エンコーダーで同じヘッダーをあなたに構築してくれます。
APIハンドラーのための防御的な習慣を1つ。デコードする前に、http.MaxBytesReader または同等の長さチェックでボディに制限を設けてください。base64文字列は自分自身の長さの約4分の3にデコードされるので、Nバイトのボディ制限はデコード後の結果をNバイト以下に保ち、悪意あるクライアントが何を投稿してもメモリは有界のままです。無制限のボディをデコードすることは、典型的なメモリ枯渇のベクトルです。テキストをバイナリに変えられるメガバイト数を攻撃者がコントロールできるからです。
レガシー文字セット
base64をデコードするとバイトが得られ、モダンなシステムではそれらのバイトはほぼ常にUTF-8です。その場合、string(decoded) が全てのお話です。しかしbase64は古いフォーマットで、その多くはWindows-1252、ISO-8859-1、Shift JIS、あるいはその他の1バイトまたは2バイトのレガシー文字セットを使っていたシステムで作られました。生成側がそうしていたなら、あなたがデコードするバイトは有効なUTF-8ではなく、Goはそれを装いません。シーケンスが壊れている場所にはどこでも置換文字を表示します。
Goの答えは golang.org/x/text モジュールです。これは一般的な文字セットで、レガシーにエンコードされたバイトをUTF-8へ(そして元へ)変換してくれます。変換の場所はデコードの直後で、関数呼び出し1つで済みます:
package main
import (
"encoding/base64"
"fmt"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
func main() {
// レガシーツールがWindows-1252として保存した「Café」を、
// 伝送のためにbase64エンコードしたもの
encoded := "Q2Fm6Q=="
raw, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
fmt.Println("decode failed:", err)
return
}
utf8, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), raw)
if err != nil {
fmt.Println("charset conversion failed:", err)
return
}
fmt.Println(string(utf8)) // Café
}
このモジュールは、文字セットファミリごとに1つのサブパッケージを持っています。WindowsとISOの1バイトテーブルには charmap、Shift JISとEUC-JPには japanese、EUC-KRには korean、GB18030には simplifiedchinese、Big5には traditionalchinese です。経験則としては、生成側の文字セットを本当に知っている場合のみ変換してください。UTF-8のバイトを2回変換しても、大きく失敗するわけではなく、単にテキストが壊れるだけです。迷ったら、ペイロードをバイトとして扱い、下流のコンシューマーに決めさせましょう。
ストリーミングとチャンク分割デコード
ファイルのセクションで NewDecoder に見ましたね。ここは、それが独自のセクションに値する理由です。これは真のストリーミングアダプターです。基になるリーダーから必要な分だけ引き出し、その場でデコードし、ストリームが悪くなった瞬間に CorruptInputError を返します。ストリーム全体はテラバイト規模でも構いません。あなたが保持するメモリは、あなたのバッファとあなたが書く出力だけです。一般的なコンシューマーパターンは2つ。小さいストリームには io.ReadAll、それ以外には io.Copy です:
small, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, r))
// 設定のブロックや小さな添付ファイルならこれで十分
w, err := io.Copy(out, base64.NewDecoder(base64.StdEncoding, r))
// 動画、ターボール、リストアジョブならこれで十分
Go 1.22以降、このパッケージには AppendDecode もあります。呼び出しごとに新しいスライスを割り当てる代わりに、あなたが再利用するバッファへデコードします。これは、行プロセッサやプロトコルデコーダーのように、ループで多くのチャンクをデコードするホットパスのための道具です:
var buf []byte
for _, chunk := range chunks {
buf, err = base64.StdEncoding.AppendDecode(buf, chunk)
if err != nil {
return err
}
process(buf)
}
このメソッドは、デコードしたチャンクを buf がすでに保持しているものに追加して、拡張されたスライスを返します。必要に応じて裏側の配列を成長させます。バッファがすでに正しいサイズまで成長している定常状態では、チャンクごとにゼロアロケーションです。ベンチマークではっきりと現れます。ワークロードが“まれに1回デコードする”なら、DecodeString のほうが簡単な選択です。“密なループで何千回もデコードする”なら、AppendDecode こそが手にすべきものです。
安全に保つ
Goのプログラムが実際にこのパッケージを使う方法に固有のセキュリティ注意点をいくつか挙げます。第一に、base64はエンコードであり、暗号化ではありません。base64文字列は、Webブラウザの開発者ツールを持つ誰でも読めるので、“送る前にパスワードをbase64にする”はセキュリティ対策ではなく、伝送上の便宜です。機密性は文字表ではなく、TLSから来ていなければなりません。
第二に、入力に境界を設けましょう。base64文字列のデコード後のサイズは、その長さの DecodedLen 以下です。そのため、割り当てる前にその数値を制限値と照合し、デコーダーが触れる前に、リクエストボディをサイズ上限で包んでください。両方のチェックはそれぞれ1行で、合わせて無制限のデコードを有界なデコードに変えてくれます。
第三に、手ぬるい入力へのスタンスを決めましょう。通常モードは、最終グループの未使用の末尾ビットを黙って破棄します。つまり、異なる2つの文字列が同じバイトにデコードされ得るということです。多くのデータではそれは問題になりません。プロトコルの一部、署名されたメッセージ、比較または保存される値のようなものには、Strict() が保守的な選択です。正規形を唯一の受け入れ形にするためです。
第四に、デコードされたバイトがどこに行くかに注意してください。デコードした値がファイル名、パス、SQLフラグメント、コマンド引数になった場合、base64レイヤーは何も守ってくれません。そのバイトは、今やあなたのプログラムの信頼できない入力であり、通常の無害化ルールが、他のユーザーデータとまったく同じように適用されるのです。
デコーダーはどれくらい速いか?
Goのbase64は速く、大きなデータでも速さを保ちます。実装はリフレクションがなく、文字ごとにアロケーションもしない、シンプルな表引きループだからです。Go 1.26を動かす最近のデスクトップCPUでは、500バイトの文字列がアロケーション1回で、およそ4分の1マイクロ秒でデコードされます。これはおよそ2ギガバイト毎秒のオーダーです。1メガバイトのbase64は1ミリ秒をはるかに下回る時間でデコードされ、1ギガバイトは1秒をはるかに下回ります。数値はハードウェアとともに動きますが、形状は動きません。base64のデコードがボトルネックになることはほとんどなく、大抵、ボトルネックになるのはその周囲のネットワークやディスクです。
ホットループの中にいるなら、アロケーションプロファイルこそが注目を集めるべきものです。DecodeString は呼び出しごとに結果スライスを割り当てます。事前サイズ付けされた宛先を使う Decode と、再利用バッファを使う AppendDecode は、定常状態でそのアロケーションをまったく避けられます。1リクエストに数回しか起きないデコードなら、これらはどれも問題になりません。1秒に数百万回起きるデコードなら、それはフラットなメモリプロファイルと、忙しく回り続けるガベージコレクタとの違いになります。
パッケージの短い歴史
base64パッケージは、Go標準ライブラリで最も古い部分の一つです。ソースファイルの著作権ヘッダーは2009年と読めます。これはこの言語が作られた年で、このパッケージは2012年3月の最初の安定版リリースであるGo 1.0から標準ライブラリの一部でした。つまり、今日あなたが呼ぶ DecodeString は、10年以上にわたりGoプログラムが呼び続けてきた、同じ挙動の同じAPIなのです。
それ以降の成長は控えめで、実用的でした。2015年8月のGo 1.5は、パディングなしの RawStdEncoding と RawURLEncoding 値を追加し、JWT風のコンパクトな文字列への道を開きました。2017年2月のGo 1.8は Strict() を追加し、プロトコルが正規の入力を要求する手段を与えました。2024年2月のGo 1.22は、baseエンコーディングのファミリー全体に AppendDecode と AppendEncode を追加し、WithPadding を意味のない引数を拒否するように厳しくしました。そして2026年9月現在、Go 1.27.1が最新リリース、Go 1.26がもう一方のサポートラインという状況で、APIはこの記事で説明したとおりです。完成済みのエンコーディング4つ、ストリームデコーダー、厳格モード、そしてパフォーマンスのためのappendファミリー。
より深い事実は、互換性の約束です。Go 1の保証は、このパッケージが永遠に同じ入力を同じように受け入れ、同じように拒否し続けることを意味します。だから、あなたが今年、2015年に作られたデータフォーマットに向けて書くデコーダーは、動き続けてくれるのです。これほど古く、これほど退屈なフォーマットにとって、これ以上のニュースはありません。
驚くこと
Goをしばらくやれば、base64に驚くことはなくなります。でも最初の数回は、これらの事実がいくつかはぐっときます。ですから、ここに出しておきます:
- デコーダーは入力内のどこにでも
\rと\nをスキップしますが、スペース、タブ、ゼロ幅スペースはスキップしません。この寛容さは意図的なもので、MIMEで折り返された入力を動かすために存在します。そして、仕様が止まる場所できっちり止まります。 - 失敗したデコードでも、実在するデータを返すことがあります。部分的な結果は、悪いバイトの前にデコードされたすべてで、エラーはそれを代わるものとしてではなく、それとともに届きます。
CorruptInputErrorは、メソッドが付いたint64にすぎません。“エラー”とはオフセットそのもので、メッセージは必要時に組み立てられます。DecodeとEncodeは、どちらも宛先バッファのサイズ付けをあなたに信頼して任せます。小さすぎるバッファを渡すと、エラーはもらえません。パニックがもらえます。- 1文字だけでは、組み込みの4つのエンコーディングのいずれにも有効な入力になりません。base64の文字1つは6ビットを運び、バイトには8ビット必要なので、1文字に完全なグループは存在しません。パディングがあろうがなかろうがです。
- 2026年8月現在、pkg.go.dev では244,000を超えるパブリックパッケージが
encoding/base64をインポートに挙げています。静かに、エコシステム全体で最も依存されているパッケージの一つです。
デコードが間違える場所
これらは、Goのコードベースで繰り返し登場するデコードの間違いです。サポートスレッドに現れる順番に近い順に並べています:
- URL安全なデータに
StdEncodingを選ぶ(またはその逆)。症状は、最初の-、_、+、/でのエラーで、修正はデコーダーを選ぶ前に文字列を見ることです。 - ターミナル、メール、PDFから文字列を貼り付け、スペース、タブ、行末のゴミが混入する。Goは本物の改行はスキップしますが、文字列の途中のスペースは破損バイトで、エラーのオフセットはまさにそこを指します。
- 結果が
[]byteであることを忘れる。生のまま印刷すると数字のリストが表示され、文字列を期待する関数に渡すにはstring(...)変換が必要です。 - エラーを確認したのに、部分的なデータを使ってしまう。半分デコードされたプレフィックスは実在しますが、それはペイロードではありません。それをペイロードとして扱うコードは、正しい長さのちょうど半分というデータで、本番環境で失敗します。
DecodedLen(len(src))ではなくlen(src)でDecodeのバッファをサイズ指定する。最初のサイズは、あなたが願う逆方向に間違っていて、それが引き起こすパニックは大きな入力でのみ起きます。そのためステージング環境の好むものになっています。- JWTセグメントがパディングを持つと仮定する。持っていません。パディング付きのデコーダーは最後の文字で失敗し、謎のように読めるエラーになります。
RawURLEncodingを使ってください。 - すべての空白がスキップされると信じる。そうではありません。スキップされるのは2つの改行文字だけで、クリップボードの“空白”はそれよりはるかに大きなファミリーです。
- 値がbase64のbase64(添付されたメールにさらに添付されたファイル)なのに、2重にデコードするか、2回デコードし忘れる。チェックは1回のラウンドトリップです。1回デコードして、結果がまだbase64のように見えるか見て、それで初めて2回デコードします。
仕事の残り半分
これがデコード側の物語のすべてです。パッケージ1つ、完成済みのデコーダー4つ、大きなデータ用のストリームデコーダー、うるさいプロトコルのための厳格モード、そして何が壊れたかのバイトを教えてくれるエラーメッセージ。寛容性のルールを学び、文字を見てデコーダーを選び、入力に境界を設ければ、Goのbase64は、設計された通りの退屈で予測可能でゼロ依存のユーティリティになります。
仕事が反転し、Goプログラムがbase64文字列を開く代わりにそれを生成する必要があるとき、関連記事「GoでのBase64エンコード」はその側を詳しく扱います。エンコーダーの1メソッドのAPI、あなたの最後の2バイトを静かに飲み込んでしまう Close の呼び出し、MIMEのための行折り返し、そして4つのエンコーディングが通るチャネルにどう対応するか、というわけです。
最終更新: 2026-10-10
関連記事: Go での Base64 エンコード:完全ガイド