はじめに
CSIGの棚井です。
本ブログは、Go 1.22 Release Notes の内容を紹介する「フューチャー技術ブログ Go 1.22リリース連載」の4本目の記事です。
今回は encoding
のアップデートを取り上げます。
- encoding/base32,base64,hex
- encoding/json
また、本ブログは release-branch.go1.22 での動作をベースとしています。
$ go version |
encoding/base32, base64, hex
TL;DR(1)
- encoding/base32, encoding/64, encoding/hex に、byte slice を利用したエンコード関数とデコード関数が追加されまし
- エンコード関数: AppendEncode
- デコード関数: AppendDecode
- base32.Encoding と base64.Encoding の Padding に「マイナスの値」を指定すると、panic を起こすようになりました
- 1.21 以前は「文字化け」していたので、今回の 1.22 で fix されました
アップデート内容(1)
Go 1.22 のリリースノート原文には、以下の説明があります。
The new methods AppendEncode and AppendDecode added to each of the Encoding types in the packages encoding/base32, encoding/base64, and encoding/hex simplify encoding and decoding from and to byte slices by taking care of byte slice buffer management.
該当する issue は「encoding: provide append-like variants #53693」です。issue には、以下のような説明があります。
- 最近は、Append ライクな API が提供されるようになっている
- というのも、Put ライクな API では、予め「バッファのサイズ」を確認した上で、適切なバッファを用意してから、Put 操作を呼び出す必要があるから
- encoding/hex, encoding/base32, encoding/base64 には、まだ Append ライクな API がないので追加しましょう
ちなみに、今回追加された Append ライクなエンコード関数とデコード関数の追加は、こちらのコミット(encoding: add AppendEncode and AppendDecode)にて確認できます。
base32
, base64
, hex
のそれぞれに同じ機能の関数が追加されているので、今回は base32
での処理内容をメインに、AppendEncode
と AppendDecode
を見ていきます。
// AppendEncode appends the base32 encoded src to dst |
// AppendDecode appends the base32 decoded src to dst |
base32 変換について
Go での base32
のエンコード・デコード処理は、もちろんソースコード内に実装されています。
私の場合、実装コードとテストコードを眺めるだけでは、いまいち処理内容がつかめなかったので、base32 のエンコード・デコード処理を「手計算」で実施してみました。
以下、「Hello」という文字列を、base32 の値にエンコードするまでの流れです。
(1)「Hello」を Ascii コードに変換する
まず、「Hello」の文字列を、1つずつ Asciiコード に変換します。
H | e | l | l | o | |
---|---|---|---|---|---|
10進数 | 72 | 101 | 108 | 108 | 111 |
16進数 | 0×48 | 0×65 | 0×6c | 0×6c | 0×6f |
(2) 8 ビットのバイナリに変換する
続いて、IP アドレスやサブネットマスクで見慣れた「2進数、バイナリ値」に変換します。
H | e | l | l | o | |
---|---|---|---|---|---|
10進数 | 72 | 101 | 108 | 108 | 111 |
16進数 | 0×48 | 0×65 | 0×6c | 0×6c | 0×6f |
binary | 01001000 | 01100101 | 01101100 | 01101100 | 01101111 |
(3) バイナリ値を連結する
(2)で変換したバイナリ値を連結すると、8 × 5 = 40 ビットのバイナリストリームができあがります。
H | e | l | l | o | |
---|---|---|---|---|---|
binary | 01001000 | 01100101 | 01101100 | 01101100 | 01101111 |
↓
0100100001100101011011000110110001101111
(4) バイナリストリームを 5ビットごとに区切る
0と1で連結された文字列を、5つごとに区切ります。
0100100001100101011011000110110001101111
↓
01001, 00001, 10010, 10110, 11000, 11011, 00011, 01111
(5) 5ビットの値ごとに、10進数へと変換する
2進数の値を10進数に変換します。
binary per 5 bits | 01001 | 00001 | 10010 | 10110 | 11000 | 11011 | 00011 | 01111 |
---|---|---|---|---|---|---|---|---|
10進数 | 9 | 1 | 18 | 22 | 24 | 27 | 3 | 31 |
(6) 10進数を base32 変換する
10進数の値それぞれを、1つずつ base32 で変換(エンコード)します。
binary per 5 bits | 01001 | 00001 | 10010 | 10110 | 11000 | 11011 | 00011 | 01111 |
---|---|---|---|---|---|---|---|---|
10進数 | 9 | 1 | 18 | 22 | 24 | 27 | 3 | 31 |
base32 | J | B | S | W | Y | 3 | D | P |
エンコードされたそれぞれの文字を結合してできる文字列が、最終的な base32変換された値となります。
# 元の文字列 -> base32エンコード後の文字列 |
検証用に、以下の Go コードを動かしてみると、base32 変換後の値が一致することも確認できます。
package main |
$ go run base32_encode.go "Hello" |
また、base32は「5バイト(40ビット)」ごとに分割して、5バイトからの不足分は「=
」によりパディングするルールがあります。
- 1バイト不足 -> 「
=
」を1つパディング - 2バイト不足 -> 「
=
」を3つパディング - 3バイト不足 -> 「
=
」を4つパディング - 4バイト不足 -> 「
=
」を6つパディング - 不足なし -> パディングなし
例えば「Golang」の文字列であれば、
8bit × 6文字
= 48 bit
= 6 byte
= 5 byte + 1 byte
= 5 byte + (5 byte - 4 byte) ← 4バイト不足
なので、「=
」は6つ追加されます。
$ go run base32_encode.go "Golang" |
ちなみに、「Hello」の文字列はちょうど5バイトなので、パディングは発生しません。
AppendEncode, AppendDecode を動かしてみる
それでは、今回追加されたエンコード関数とデコード関数を動かしてみます。
package main |
提案元の issue に記載されているように「Append ライク」な動作ということなので、関数の呼び出し側でバッファを意識せずとも「Append」が可能です。
以下のサンプルコードは、こちらの「テストコード」をもとに作成しました。
package main |
パディング値に「negative value」を代入してみる
こちらのコミット(encoding: require unique alphabet for base32 and base64)で修正された「パディングの受け取る値」について、以前のバージョンでの挙動と比較しながら確認してみます。
base32 エンコードでは、5バイトごとに分割した際の「不足分」が、「=
」によりパディングされるというルールがありました。
Go の実装では、パディングの値は「こちら」で定義されています。
const ( |
パディングの値を「$
」に指定すると、base32エンコードの結果が以下のようになります。
package main |
リリースノートには以下の記載があります。
The methods base32.Encoding.WithPadding and base64.Encoding.WithPadding now panic if the padding argument is a negative value other than NoPadding.
試しに WithPadding
へ -2
を代入して動かしてみます。
package main |
$ go build -trimpath -o invalid_padding invalid_padding.go |
Go 1.22 で動かすと、リリースノートの記載通り panic が起きました。
比較検証として、Go 1.21 で動かしてみると、panic にはならず「パディングの値が文字化けして」表示されました。
このバグを踏むケースがあまりイメージできませんが、1.22 で回避されるようになりました。
$ go run invalid_padding.go |
encoding/json
TL;DR(2)
- これまでのマーシャリングとエンコードでは、
'\b'
と'\f'
はそれぞれ、\u0008
と\u000c
に変換されていました - Go 1.22 から、
'\b'
は\b
に、'\f'
は\f
に変換されるようになりました - これにより、
RFC 8259
で定義された5つの制御文字全てへの対応が完了しました
アップデート内容(2)
リリースノートには、1行だけ説明があります。
Marshaling and encoding functionality now escapes ‘\b’ and ‘\f’ characters as \b and \f instead of \u0008 and \u000c.
本アップデートは、こちらの「コミットログ(encoding/json: encode \b and \f as ‘\b’ and ‘\f’ in JSON strings)」に詳しい説明があります。
EFC 8259
には5つの制御文字があり、
- https://go.dev/cl/4678046 で
\r
とn
に対応 - https://go.dev/cl/162340043 で
\t
に対応
する中で、残りの \b
と \f
に対応したのが今回のアップデートのようです。
また、コミットログには以下の記載があります。
This change is to prepare the path forward for a potential v2 “json” package, which has more consistent encoding of JSON strings.
「v2 json package への準備」とのことなので、今回のリリースにて math/rand/v2 が追加されたこともり、今後のアップデートが楽しみだなと思いました。
おわりに
本ブログでは、encoding
パッケージへの追加機能、bugfix 内容を紹介しました。
今回の連載記事の内容を調べるなかで「アップデートの背景を、issue を通して知る」ことの面白さに気付きました。
コードを読む中で、以外と小さな単位のコミットがマージされているケースも見つかりましたので、私もできるところから OSSにコミットしていきたいなと思いました。