Base64 形式を扱う必要がありますか?それならこのサイトが最適です!データをエンコードまたはデコードするために便利なオンラインツールをご利用ください。

Go での Base64 エンコード:完全ガイド

ときおり、あなたのGoプログラムは、テキストだけを許す世界にバイナリデータを渡さなければなりません。文字列でいなければならないJSONフィールド、単一のトークンでいなければならないURL、7ビットの日々を覚えているサーバーを渡るメール添付ファイル、ページがリクエストを1つ飛ばすためにHTMLのなかに住みたい画像。Base64はまさにその仕事のための配達役です。このサイトのホームページはすでにフォーマットを詳しく説明しているので、この記事はパッキングの技術へまっすぐ進みます。Goでbase64文字列を生成し、地球上のどのデコーダーも戦うことなく開けるようにすることです。

先に朗報をどうぞ。エンコーダーは、この物語の温和な半分です。メソッドは1つ、エラー返り値なし、失敗モードなし、最初の安定版リリース以降のすべてのGoリリースでバイト単位で同一の出力。すべてのドラマは、そのメソッドの周りにあります。文字列が旅するチャネルに合った正しい文字表を選ぶこと、忘れてしまうと最後の2バイトを静かに飲み込んでしまう Close の呼び出し、フォーマットがかかせるサイズ課税、そしてGoがPython、Java、Nodeと同様に、出力を76文字で折り返さないという事実。まずは関数に会い、それから罠に会いましょう。

失敗しないパッキング

Goでのエンコード生活の9割は、Encoding 型のメソッド1つです。パッケージ内のすべてのエンコード入口(Encode、AppendEncode)と同様に、エラー返り値はありません:

func (enc *Encoding) EncodeToString(src []byte) string

バイトを渡せば、文字列を返してくれます。それが契約のすべてです:

package main

import (
  "encoding/base64"
  "fmt"
)

func main() {
  packed := base64.StdEncoding.EncodeToString([]byte("Man"))
  fmt.Println(packed) // TWFu
}

エラー値がないのは、何が悪くなることもないからです。どんなバイトも合法な入力であり、文字表は常にそれをカバーし、出力は常に純粋なASCIIです。覚える価値がある性質が3つあります。将来の質問の半分を答えられるからです。第一に、出力長は入力長の純粋な算術関数で、パッケージはなんとメソッドとしてその式を手渡してくれます。EncodedLen(n) はパディング付きのエンコーディングで (n+2)/3*4 を返します。つまり、入力3バイトが文字4つに、6バイトが8つに、という具合です。第二に、このフォーマットにはサイズ課税があります。データの3バイトごとに文字4つが返ってくる、あの馴染み深い約33パーセントの膨張です。帯域幅の請求書やストレージクォータに現れるものですね。第三に、このメソッドは決定的です。同じバイトは、どんなマシンでも、どんなバージョンのGoでも、永遠に同じ文字列を生成します。その決定的さが、base64を謎ではなくシリアライゼーションフォーマットにしているのです。

入力側について、Go固有の注意を1つ。このメソッドは string ではなく []byte を受け取り、[]byte(...) 変換は各呼び出し場所で明示的です - Goは文字列をスライスに変換してくれることは決してありません - そして、それは文字列のバイトの独立したコピーを生成します。スライスが読み取り専用で外部にエスケープしない場合、コンパイラーはそのコピーを省略できます。これがコストが通常は測定できない理由です。ただし、スライスが保存されたり返されたりすると、ランタイムは本物のO(n)コピーの代金を払います。Goプログラム中のテキストは慣習としてUTF-8なので、文字列をエンコードするとき、あなたはそれのUTF-8バイトをエンコードしています。そして、それはまさに相手の側のすべてのモダンなデコーダーが期待するものです。その件は、Text, Bytes, and Unicode のセクションで詳しく扱います。

Goが提供する形

この記事のすべてと同様に、エンコーダーは標準ライブラリのパッケージ encoding/base64 から来ます。この言語の最初のリリースから同梱されており、ソースファイルには今なお2009年の著作権ヘッダーが残っています。取得するモジュールも、切り替える機能フラグも、プラットフォームの癖もありません。go version が動けば、go doc encoding/base64 がAPI全体を印刷してくれます。

この記事を書いている時点での最新リリースは Go 1.27.1 で、2026年9月1日にリリースされました。サポート対象のもう一方のラインはGo 1.26(現在は1.26.8)です。Goのインストールは、go.dev/dl の公式ターボール、ディストリビューションのパッケージマネージャー(sudo apt install golang-go)、あるいは複数のバージョンを乗り回しているなら golang.org/dl ラッパーを通じて行います。base64のAPIは両方のサポートラインで同一で、下表はこれまでに変更があったことの全履歴です。これほど中心的存在のパッケージとしては、短いリストですね:

リリース 年 encoding/base64で何が変化したか
Go 1.0 2012 パッケージは最初の日から安定;ソースの著作権は2009年
Go 1.5 2015 パディングなしの出力のために RawStdEncoding と RawURLEncoding が追加
Go 1.8 2017 正規なデコードのために Strict() が追加(デコーダー側)
Go 1.22 2024 AppendEncode と AppendDecode が追加;WithPadding は不正な引数を拒否するように
Go 1.27.1 2026 現在のリリース;APIは変更なし、Go 1の約束により挙動はバイト安定

その履歴の実用的な帰結:2015年にこのAPIに向けて書かれたコードは、今日でもコンパイルでき、まったく同じ挙動をします。そして、あなたのプログラムが2026年にエンコードする文字列は、過去のものであれ未来のものであれ、どんなGoリリースでも正しくデコードされます。シリアライゼーションフォーマットにとって、それが静かな超能力です。

目的地に合わせた文字表の選び方

エンコーディングには1つの真の決断があります。それは旅路の質問です。この文字列はどこに行くのか?Goは完成済みのエンコーダーを4つ与えてくれます。それぞれが異なるチャネルに調整されています:

エンコーダー 文字表 パディング 文字列がここを通るときには送る先
StdEncoding A-Z a-z 0-9 + / = JSONボディ、メールのMIMEパート、data URL、HTTP Basic認証、PEM、ほとんどのAPI
URLEncoding A-Z a-z 0-9 - _ = URLのパスとクエリ、ファイル名、+ や / がエスケープを必要とするあらゆる場所
RawStdEncoding A-Z a-z 0-9 + / なし パディングが表示されてはならない、コンパクトな標準文字表の文字列
RawURLEncoding A-Z a-z 0-9 - _ なし JWTセグメント、コンパクトな識別子、URLに埋め込まれたトークン

これらのバリアントにある理由は、フォーマット自体にある理由なのです。標準文字表は、MIMEとほとんどのAPIが期待するものです。そのため、誰も別のことを教えてくれない限り、それがデフォルトであり、安全な答えです。URL安全な文字表が存在するのは、+ と / がURLにおける予約文字だからです。クエリ文字列のプラスはしばしばスペースとして読まれ、スラッシュは新しいパスセグメントの始まりになるため、URLの中の標準base64は壊れるか、+、/、= を持つ文字にパーセントエスケープが必要になります - 典型的なトークンの数パーセントです。パス、クエリ、ファイル名の中でエスケープなしで合法な - と _ に置き換えることが、RFC 4648が標準化した修正です。Raw のバリアントは末尾のイコール記号をすべて落とします。これは、パディングが禁止されているか、そもそも使われないコンテキスト、JWTセグメントのような場所で重要です。ほとんどのデバッグからあなたを救うルール:あなたが選ぶエンコーダーと、相手の側が使うデコーダーは1つの契約であり、その契約はあなたではなく、目的地が書いたものです。

あなたが話しているシステムが独自の64文字の文字表を定義していた場合、base64.NewEncoding("...64 chars...") がそれ用のエンコーダーを構築し、WithPadding(rune) でパディング文字を交換するか、NoPadding で無効化できます。両方の関数は不正な引数でパニックします(文字表の長さが違う、文字が重複している、文字表に改行がある、文字表と衝突するパディング文字)。そのため、カスタムエンコーダーは起動時に1回だけ構築し、ホットパスでは決して構築しないでください。

Closeの罠

ここがこのパッケージで最も有名な罠です。文字列ではなくストリームをエンコードするときにだけ現れます。NewEncoder は任意の io.Writer をbase64エンコーディングするライターで包みます。base64は入力3バイトで出力4文字のブロックで働くため、エンコーダーはあなたの最後の1バイトか2バイトをバッファに溜め、もっと来るかどうかを待ちます。フッシュされるのは、あなたが閉じたときだけです:

package main

import (
  "bytes"
  "encoding/base64"
  "fmt"
)

func main() {
  var buf bytes.Buffer
  enc := base64.NewEncoder(base64.StdEncoding, &buf)
  enc.Write([]byte("hello"))
  fmt.Println(buf.String()) // aGVs  --「lo」はどこへ?

  buf.Reset()
  enc = base64.NewEncoder(base64.StdEncoding, &buf)
  enc.Write([]byte("hello"))
  enc.Close()
  fmt.Println(buf.String()) // aGVsbG8=  --「hello」の完全なエンコーディング
}

最初の印刷が、その教訓そのものです。Close がないと、エンコーダーは最初の完全なブロックだけを出しました。“hello”の3バイトが“aGVs”になり、残りの2バイトは内部バッファにただ消えたのです。2番目の印刷、Close の後が、正しい完全な文字列です。修正は技術ではなく習慣です。エンコーダーを作る瞬間に、そのクリーンアップも作ってください:

enc := base64.NewEncoder(base64.StdEncoding, w)
defer enc.Close() // 本番コードでは、返されたエラーを確認するのを忘れないで

2つの詳細が、この罠を見た目以上に鋭くしています。第一に、Close は実作業をします。保留中の部分ブロックをフッシュし、失敗することもあります。基になるライターに書き込むからです。そのため慣用的なバージョンは、そのエラーを確認します。特に宛先がネットワークやディスクの場合です。第二に、ドキュメントは Close の後に Write を呼ぶのはエラーだと述べていますが、ランタイムはその文章を強制しません。閉じた後にまた書き込むと、エンコーダーは静かに新しいブロックを始めて追加し、真ん中にパディングがある文字列を生成します。これは不正なbase64で、ほとんどのデコーダーはそれを拒否し、オフセットは混乱を招くものです。契約を守ることはあなたの仕事です。

行折り返し、Go流

あなたがこれまで使ってきた他のすべての主要なbase64実装は、出力を折り返します。MIMEは最大76文字の行を求め、PEMは64を使い、世界中のメールクライアントはときどきCRLFを挿入します。Goのエンコーダーはそれらを一切しません。ペイロードがどれほど大きくても、1つの連続した行を出します。パッケージが生まれた日からずっとそうです。1メガバイトのデータの出力は、1メガバイトと3分の1の単一の行で、最初から最後まで、折り返しなしです。

それは意図的な選択であり、見落としではありません。このフォーマットは、改行があろうがなかろうが同じように動作します。Go自身のデコーダーは入力内のどこでもそれらをスキップしますし、あなたのデータにCRLFを黙って挿入するエンコーダーは、文字列をデータベースのカラムに保存したり等価性を比較したりするプログラムを驚かせるでしょう。代価は、チャネルが要求するときに自分で折り返さなければならないことです。小さなヘルパー1つで済みます:

package main

import (
  "bytes"
  "encoding/base64"
  "fmt"
)

func wrapAt(s string, width int) string {
  var out bytes.Buffer
  for i := 0; i < len(s); {
    end := i + width
    if end > len(s) {
      end = len(s)
    }
    out.WriteString(s[i:end])
    out.WriteByte('\n')
    i = end
  }
  return out.String()
}

func main() {
  raw := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x42}, 100))
  fmt.Print(wrapAt(raw, 76))
}

旅の方向性について、1つの注意。Goのデコーダーはどこにでも改行を無視するので、折り返された入力は、どんなブリッジのGo側でも完璧にデコードされます。反対の方向には注意が必要です。改行を期待しないコンシューマー(JSONフィールド、URL、トークン)に折り返された出力を送るときは、先にそれらを除去してください。そのコンシューマーは、改行を破損文字として扱うかもしれないからです。あなたのチャネルがどの慣行の中で生きているのかを知り、それを意図的に出力してください。

メールとMIME向けのパッキング

メールはbase64で最も古い住処です。元のSMTPプロトコルは7ビットASCIIを運ぶように設計されていたため、添付ファイルは送る前にbase64エンコードされ、着くとデコードされました。そしてMIME標準(RFC 2045)がこの慣行を正式化しました。ヘッダー Content-Transfer-Encoding: base64 がパートをマークし、ボディは最大76文字の行に分割し、行間にCRLFを入れるのが望ましい、とされています。

Goの net/smtp パッケージはあなたが渡したバイトを送るだけで、MIMEパートを構築してくれません。そのため、メールを組み立てるプログラムでは、base64の部分はこうなります:

package main

import (
  "bytes"
  "encoding/base64"
  "fmt"
)

func main() {
  body := []byte("hi from Go")
  var part bytes.Buffer
  part.WriteString("Content-Transfer-Encoding: base64\r\n")
  part.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n")

  encoded := base64.StdEncoding.EncodeToString(body)
  for i := 0; i < len(encoded); i += 76 {
    end := i + 76
    if end > len(encoded) {
      end = len(encoded)
    }
    part.WriteString(encoded[i:end] + "\r\n")
  }
  fmt.Print(part.String())
}

注目すべきことが3つあります。ここでの正しいエンコーダーは標準のもので、MIMEは元々の標準文字表のコンテキストだからです。改行はCRLFで、プラットフォームのネイティブな改行ではありません。RFCがそう規定し、メールパーサーがそれを期待するからです。そして、あなたのプログラムが本物のメールを大量に送るなら、メンテナンスされたMIMEライブラリがメッセージ全体を構築してくれます。この例のポイントはbase64の半分で、それはこのパッケージに属する部分です。文字表と行の慣行を正しくすれば、MIMEの残りは誰かの問題です。

ファイルのパッキング

メモリに収まるファイルでは、パターンはどこも同じ2行です。読んで、それから EncodeToString。収まらないファイルでは、ストリーミングがメモリをフラットに保ちます。レシピは、ファイル1つ、エンコーダー1つ、コピー1つ、そして正しい順序の2つのクローズです:

in, err := os.Open("photo.jpg")
if err != nil {
  panic(err)
}
defer in.Close()

out, err := os.Create("photo.b64")
if err != nil {
  panic(err)
}
enc := base64.NewEncoder(base64.StdEncoding, out)
if _, err := io.Copy(enc, in); err != nil {
  panic(err)
}
if err := enc.Close(); err != nil {
  panic(err) // 最後の部分ブロックをフッシュする
}
if err := out.Close(); err != nil {
  panic(err)
}

クローズの順序が微妙なところで、これはCloseの罠のファイル版です。エンコーダーはファイルよりも先に閉じなければなりません。enc.Close が最後の部分ブロックをファイルに書き込むものだから、ファイルを先に閉じると、そのブロックは何にも書かないバッファに残ってしまいます。defer では、遅延呼び出しは逆順に実行されることを覚えておいてください。だから out.Close を先に登録し enc.Close を2番目に登録する(あるいは、上記の例のように、ファイルを defer する前にエンコーダーを明示的に閉じる)ことで、この順序が安全になるのです。

このパターンを軸に計画を立てるときは、サイズ課税を頭に置いておいてください。10メガバイトの写真はおよそ13.3メガバイトのテキストになり、100メガバイトのアーカイブはディスク上で133メガバイトの文字列になります。目的地にクォータ、制限、バイト単位の価格があるなら、カウントされるのは元のファイルではなく、あなたのファイルのbase64版です。

Web向けのパッキング:Data URL

ブラウザは、HTMLやCSSそのものの中に住む文字列から、喜んで画像やフォントを読み込みます。その文字列がdata URLです。メディアタイプ、;base64 フラグ、カンマ、ペイロード、すべてが1つのURLに詰まっています。Goにはdata URLのヘルパーはありませんが、構築は文字列連結で済みます。フォーマットは、書き出された姿で見える契約だからです:

package main

import (
  "fmt"
  "os"
  "encoding/base64"
)

func main() {
  img, err := os.ReadFile("logo.png")
  if err != nil {
    panic(err)
  }
  url := "data:image/png;base64," + base64.StdEncoding.EncodeToString(img)
  fmt.Println(url)
  // data:image/png;base64,iVBORw0KGgo...
}

data URLで深みにハマるのを避けてくれるルールが2つあります。常にメディアタイプを含めてください。文法では任意ですが(デフォルトは text/plain;charset=US-ASCII)、あなたのバイナリペイロードのタイプを推測するブラウザは、あなたが望むシナリオではありません。そして、data URLは小さなアセットのためのトリックとして扱いましょう。RFCはこのスキームが短い値にしか役に立たないと述べており、33パーセントの膨張が、リクエストを1つ節約する2キロバイトのアイコンと、すべてのページ読み込みを肥大させる5メガバイトの写真との違いを作ります。共有するキャッシュもなく、誰かに渡すURLもないのです。アイコン、ファビコン、小さなスプライト:はい。プロダクト写真:いいえ。

HTTP向けのパッキング

Goのサービスにおけるbase64を支配するのは3つのHTTPコンテキストで、その2つには組み込みの助けがついています。第一はJSONボディ、主力です。マーシャルする前に値をエンコードし、フィールドは素の文字列をワイヤーの上で運ぶのです:

package main

import (
  "encoding/base64"
  "encoding/json"
  "fmt"
)

type avatar struct {
  Data string `json:"data"`
}

func main() {
  png := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
  a := avatar{Data: base64.StdEncoding.EncodeToString(png)}
  body, err := json.Marshal(a)
  if err != nil {
    panic(err)
  }
  fmt.Println(string(body))
  // {"data":"iVBORw0KGgo="}
}

ある型が多くの場所に登場するなら、クリーンなGoの手は、その型に MarshalJSON と UnmarshalJSON を実装して、base64のステップをすべての呼び出し箇所から隠すことです。第二のコンテキストはHTTP Basic認証で、ここは標準ライブラリが全部やってくれます。Request.SetBasicAuth(user, pass) が Authorization ヘッダーをあなたのために構築し、RFC 2617が規定する user:pass 対に対して標準エンコーダーを動かします。そこでの1つのルールは、即興をしないことです。Basic認証は Basic プレフィックス付きの標準base64で、URL安全な文字表やパディング記号の欠落は、動くログインを誰も説明できない401に変えてしまいます。

第三のコンテキストはURLで、文字列はパスセグメントやクエリパラメータのペイロードです。ここでは標準文字表は間違った選択です。+、/、= がすべてURLの文法と衝突し、そのすべてにパーセントエスケープが必要だからです。代わりにURL安全な変種でエンコードすれば、トークンはURLを無傷で生き延びます。コンシューマーがそれでもパーセントエスケープするなら、何も壊れません。もししないなら、あなたは404の1クラスを節約したことになります。

URL安全な出力

Goでは、URL安全なbase64は独自のセクションに値します。標準のものより、あなたが手に取る機会の多い変種だからで、Goはその切り替えを無料で提供してくれるからです。RFC 4648の代替文字表は、+ を - に、/ を _ に置き換えます。そのため出力はURLのパス、クエリ、ファイル名でエスケープを必要としません。ログ行では、1つのクリーンなトークンとして読めます。完成済みの2つのエンコーダーは、URLEncoding(パディング付き)と RawURLEncoding(パディングなし)です:

raw := []byte{0xfb, 0x0f, 0x67, 0x01}
fmt.Println(base64.StdEncoding.EncodeToString(raw))     // +w9nAQ==
fmt.Println(base64.URLEncoding.EncodeToString(raw))     // -w9nAQ==
fmt.Println(base64.RawURLEncoding.EncodeToString(raw))  // -w9nAQ

入力1つ、出力3つです。標準版はプラス記号にパーセントエスケープを必要とし、URL安全版は1つのトークン、raw版はパディングも落とします。それぞれの典型的なGoの仕事:サービスが生成してURL、ルート、ファイル名に保存する不透明な識別子、クライアントがクエリ文字列に貼り付けるAPIトークン、そしてログ行で、プラスやスラッシュが1文字違いで構文と誤認されてしまうかもしれないあらゆるもの。

これをクリーンに保つ規律は、この記事のどこもかしこもと同じです。変種はコンシューマーとの契約であるということです。相手の側が標準base64を期待していてあなたがURL安全を送れば、そのデコーダーは最初のダッシュで失敗し、エラーは完璧な文字列の末尾に近いバイトオフセットになります。これは デバッグ しやすいものではありません。迷ったら、相手の側が何を期待するか聞き、それが指し示す仕様に目を通し、習慣ではなく目的地からエンコーダーを選びましょう。

JWTのパッキング

JSON Web Tokenは、モダンなAPIにおけるbase64の最も目立つ消費者です。そして、正確な変種を固定しています。RFC 7515によるJWSコンパクトシリアライゼーションは、パディングなしのbase64urlセグメント3つをドットで結合したものです。ヘッダー、ペイロード、シグネチャ。つまり、あなたが手で作るもののためのエンコーダーの選択は RawURLEncoding です:

package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/base64"
  "encoding/json"
  "fmt"
)

func main() {
  secret := []byte("hmac-secret")
  header, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
  payload, _ := json.Marshal(map[string]any{"sub": "1234567890"})

  signingInput := base64.RawURLEncoding.EncodeToString(header) + "." +
    base64.RawURLEncoding.EncodeToString(payload)

  mac := hmac.New(sha256.New, secret)
  mac.Write([]byte(signingInput))
  signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
  fmt.Println(signingInput + "." + signature)
}

その例は、出荷するよう推奨するものとしてではなく、フォーマットが何かについての教訓として読んでください。base64がどこに座っているか(署名の前に2回、その後に1回)と、なぜシグネチャが生のJSONではなくエンコードされたセグメントをカバーするのかを示しています。本番では、メンテナンスされたライブラリで署名と検証をしてください。JWTには、base64レイヤーが見ることができない間違いの長い尾(期限切れのクロックスケュー、アルゴリズム混同、オーディエンスチェックの欠如)があるからです。事実上のGoライブラリは github.com/golang-jwt/jwt/v5 で、go get github.com/golang-jwt/jwt/v5 でインストールします:

package main

import (
  "fmt"
  "log"
  "time"

  "github.com/golang-jwt/jwt/v5"
)

func main() {
  secret := []byte("hmac-secret")
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "sub": "1234567890",
    "exp": time.Now().Add(time.Hour).Unix(),
  })
  signed, err := token.SignedString(secret)
  if err != nil {
    log.Fatal("signing failed:", err)
  }
  fmt.Println(signed)
}

このライブラリはすべてのセグメントのbase64urlエンコーディングを内部で行うので、あなたは encoding/base64 に一切触れません。それが最良の結果です。パディングや文字表の間違いが隠れられる場所が1つ減るのです。そして、無料で得られるガードに注目してください。v5は、UnsafeAllowNoneSignatureType 定数で明示的にオプトインしない限り、alg=none を主張するトークンを拒否します。考えずに欲しい保護がこれです。

テキスト、バイト、Unicode

Goはこの問いに対するスタンスが、主要な言語の中で最も短い。そして、ここでbase64がとても気持ち良い理由でもあります。Goの string は読み取り専用のバイト列で、あなたのプログラム中のテキストはUTF-8です。隠れたエンコーディング層もなく、“文字列は実はUTF-16だ”というサプライズもなく、設定するcharsetフラグもありません。EncodeToString([]byte(myText)) と書いたとき、あなたはテキストのUTF-8バイトをエンコードしています。それ以上でもそれ以下でもありません:

s := "Café ☕"
packed := base64.StdEncoding.EncodeToString([]byte(s))
fmt.Println(packed) // Q2Fmw6kg4piV

その1行が、絵文字やCJKを含むモダンなテキストの全てのお話です。base64はバイトで働き、UTF-8はただのバイト列で、同じ慣行に従う相手の側のすべてのデコーダーが同じ文字列を返してくれます。[]byte(...) 変換は独立したコピーで、スライスが読み取り専用で外部にエスケープしない場合、コンパイラーはそれを省略します - つまり実際には、測定できるコストは何もないのです。

お話が長くなる唯一のケースはレガシーデータです。Windows-1252、Shift JIS、ISO-8859-1のシステムで作られ、有効なUTF-8ではないバイト。それらのバイトをそのままbase64エンコードすると、壊れたテキストを忠実に運び出したことになります。それは誰も望んでいません。修正は、エンコードする前に正規化することです。golang.org/x/text を使って、base64文字列があなたのプログラムを離れる瞬間からクリーンなUTF-8を乗せるのです:

import (
  "golang.org/x/text/encoding/charmap"
  "golang.org/x/text/transform"
)

legacy := []byte{0x43, 0x61, 0x66, 0xE9} // Windows-1252の「Café」
utf8, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), legacy)
if err != nil {
  panic(err)
}
packed := base64.StdEncoding.EncodeToString(utf8)
// Q2Fmw6k=  -- 同じ「Café」、今度は旅に出せるクリーンなUTF-8バイト

同じモジュールは、charmap の他に japanese、korean、simplifiedchinese、traditionalchinese もカバーします。実用的なルール:レガシーバイトがあなたのプログラムに入る境界で1回だけ変換し、それ以降あなたがエンコードするものはすべてUTF-8です。2回変換しない、推測しない、そしてモダンなコンシューマーがデコードして表示するbase64文字列に、非UTF-8のペイロードが忍び込むことを決して許さないでください。

エンコーダーを測る

エンコーダーは、入力による分岐がなく、出力文字列を超えるアロケーションもない表引きです。数値にそれが表れています。Go 1.26を動かす最近のデスクトップCPUでは、500バイトのエンコードはアロケーション2回でおよそ10分の3マイクロ秒です。これはおよそ1.5ギガバイト毎秒のオーダーに相当します。1メガバイトのデータは1ミリ秒をはるかに下回る時間でエンコードされ、エンコーダーはあなたが感じることの少ない存在です。

知る価値があるレバーは1つ。ホットループのアロケーションプロファイルです。EncodeToString は呼び出しごとに出力文字列を割り当てますが、99パーセントのケースでは正しいトレードオフです。1秒に数千のチャンクを成長するバッファへエンコードしているなら、Go 1.22で追加された AppendEncode は、エンコードしたバイトをあなたが再利用するスライスに追加し、バッファがサイズに成長した後は定常状態でアロケーションを行いません:

var out []byte
for _, chunk := range chunks {
  out = base64.StdEncoding.AppendEncode(out, chunk)
}

使い捨てには EncodeToString、密なループには AppendEncode、ストリームやファイルには NewEncoder を使ってください。どれを選ぶにしても、エンコーダーの周囲のネットワークやディスクがほぼ常に遅い部分であることを覚えておいてください。文字表を最適化する前に、パス全体をプロファイルしましょう。

セキュリティ上の注意点

この記事で最も重要なセキュリティの一文:base64は暗号化ではなく、“先にbase64にする”はセキュリティ対策ではありません。文字表はデータをテキスト安全にし、秘密にしないのです。ブラウザの開発者ツールを持つ誰でも、あなたのbase64を一瞬で読めます。機密性はTLSとアクセス制御から来て、base64の仕事は、テキスト専用のチャネルを、バイトを壊さずに渡すことです。デザインでもドキュメントでも、その2つの仕事を分けておけば、クラシックなレビューコメント、“パスワードは保護されている、見て、base64なんだ”を避けられます。

第二の注意点はサイズです。このフォーマットは3分の1膨張するため、システム中のあらゆる制限にbase64版があります。4メガバイトのJSONを受け入れるAPIは、ペイロードがbase64フィールドの場合、元のデータはおよそ3メガバイトまでです。長さの予算があるURLは、トークンがURL安全でパディングなしなら、生のバイト数で短くなります。生の値のサイズに合わせたデータベースのカラムは、エンコードされた値には小さすぎるかもしれません。保存、送信、制限の前に EncodedLen で計算をして、膨張は、あなたが終わる文字列に対してではなく、あなたが始める入力に対して起きるのだと覚えておいてください。

第三に、エンコードされた文字列がどこで観測され得るか考えましょう。base64文字列はログと画面の両方に親しみやすい。それは機能です。ただし、20メガバイトの添付ファイルが26メガバイトのテキストにbase64化して、アクセスログが毎回それを律儀に記録し始めるまでは。長さ、最初の数十文字、識別子をログに記録し、ペイロードは記録しないでください。そうすればログは読みやすく、ディスクは生き残ります。最後に、URLではURL安全な変種を優先してください。そうすれば、トークンの文字がパーセントエスケープに使われることがなくなります。パーセントエスケープはURLを肥大させ、クエリ文字列に何が属するかを厳格に考えるゲートウェイやプロキシにときどき引っかかるのです。

おもしろい事実とGoの癖

このパッケージに固有の事実をいくつか。コードレビューで正しくいたいときに:

  • EncodeToString は主力で、パッケージ内のすべてのエンコード入口(Encode、AppendEncode)と同様に、エラー返り値はありません - Goではエンコーディングは失敗しない。これは希少で静かな種類の自由です。どんなバイトも合法な入力であり、悪い文字列を得る唯一の方法は、チャネルに合わない文字表を選ぶことです。
  • EncodedLen は純粋な算術で、パディング付きのエンコーディングでは (n+2)/3*4、アロケーションもループもなしに計算されます。1バイトもエンコードせずにバッファとクォータをサイズ指定できるために、これが存在します。
  • 内部のストリームエンコーダーは、3バイトの入力バッファと1024バイトの出力バッファを隠しています。これが NewEncoder がチャンクで書き、最後の部分ブロックが Close を通ってだけ外に出られる理由です。バッファこそが罠の理由です。
  • ドキュメントは Close を呼んだ後の書き込みはエラーだと述べていますが、ランタイムはその文章を強制しません。遅れてきた Write は受け入れられ、新しいブロックが追加され、真ん中にパディングがある文字列が生成されます。不正なbase64が、礼儀正しく生成され、エラー値は見当たりません。
  • Goのエンコーダーは、出力を76文字で折り返したことは決してありません - Python、Java、Nodeのエンコーダーと同様に、Goのエンコーダーは1メガバイトのデータで1行を生成します。あなたのMIME折り返しヘルパーは個人プロジェクトで、それはまた、メールbase64の改行はMIMEの慣行でありbase64の要求ではないことを覚える良い方法でもあります。
  • 2026年8月現在、pkg.go.dev では244,000を超えるパブリックパッケージが encoding/base64 をインポートしています。あなたのGoプログラムが何であれ、知っているかどうかに関わらず、どこかでbase64をやっていることはほぼ間違いありません。
  • Go 1の互換性の約束は、このパッケージに特別な力を持って適用されます:2013年に文字列をエンコードしたプログラムの出力は、今日のGo 1.27でバイト単位で同一です。Base64文字列は、Goでは、事実上不滅です。

繰り返し再発する間違い

Goのコードベースで繰り返し再発するエンコーディングの間違い。届く順番に近い順に:

  • ストリームエンコーダーの Close を忘れて、最後の1バイトか2バイトが欠けた文字列を出荷する。このバグは、長さが3の倍数の入力を使うすべてのテストを生き残り、それが本番環境へ届く方法です。
  • エンコーダーより先にファイルを閉じて、最後の部分ブロックがすでに消えたファイルハンドルへフッシュされる。出力はちょうど同じ量だけ切り詰められ、エラーは奇怪なサイズの入力でのみ現れます。
  • MIMEやメールの出力で76文字の改行を期待して、Goが1本の長い行を渡すのに混乱する。折り返しはチャネルの慣行で、Goではそれを適用するのはあなたのコードの仕事です。
  • URLの中で標準文字表を使って、その後、実はパーセントエンコーディングの問題である404と400を1午後かけて追いかけ回す。文字列がURLの中で生きるなら、URLEncoding または RawURLEncoding から始めましょう。
  • コンシューマーが禁止している場所にパディングを出す。JWTセグメント、一部のトークンフォーマット、少数の厳格なパーサー。rawの変種はまさにこのために存在し、相手の側のエラーメッセージは、あなたの文字列のまさに末尾のバイトオフセットであることが多いです。
  • シークレットをbase64化して保護と呼ぶ。それは保護ではありません。ヘッダー、トークン、“暗号化された”フィールド:誰にでも半秒で読めます。TLSを使い、プロトコルが求めるのがハッシュならハッシングを使い、base64にはその1つの誠実な仕事をさせましょう。
  • 制限を設定するときの33パーセントを忘れる。ボディサイズ、カラム幅、URLの予算、クォータチェック。計算は EncodedLen の呼び出し1つで、それを跳ねる代価は、本番環境での413か切り詰められたカラムです。
  • UTF-8でないテキストをエンコードして、壊れを忠実に運び出す。エンコードする前に golang.org/x/text でレガシー文字セットを正規化し、base64文字列がクリーンなバイトを乗せるようにしましょう。
  • 習慣から、またはリトライループから、クローズした後にエンコーダーに書き込む。エラーは起きず、出力は静かに不正になります。
  • 相手の側のデコーダーがGoと同じくらい寛容だと仮定する。Goはどこでも改行をスキップしますが、他の言語やパーサーは空白と行長に対して厳格です。だから、Goランタイムの気分ではなく、チャネルの慣行に合わせましょう。

裏返しの側

これがエンコーディング側の物語のすべてです。失敗しないメソッド1つ、文字列が旅するチャネルに合わせたエンコーダー4つ、必須の Close が1つのストリームエンコーダー、そしてあなたのデータを3分の1膨張させ、決して、絶対に行を折り返さないフォーマット。文字表は目的地から選び、エンコーダーを閉じ、サイズ計算を先にやっていれば、Goのbase64は2009年からそうである静かなゼロ依存のユーティリティのままです。

そして流量が反転するとき、あなたのプログラムがこれらの文字列の1つを受け取って開かなければならないとき、関連記事「GoでのBase64デコード」はその側を詳しく扱います。デコーダーの寛容性のルール、入力がどこで間違えるかを教えてくれるエラーのオフセット、うるさいプロトコルのための厳格モード、そして同じ4つのエンコーディングを別の方向から見たものです。

最終更新: 2026-10-10

関連記事: Go での Base64 デコード:完全ガイド