#29678likely_accept
net: add MarshalText/UnmarshalText to HardwareAddr, IPNet, IPMask
ステータス変更: active likely_accept
要約
AIによる要約であり、誤りを含む場合があります。
概要
net.HardwareAddr(MACアドレス)と net.IPMask に MarshalText/UnmarshalText を追加し、JSON などへ人が読める形式で書き出せるようにする提案です。既存データとの互換性を保つため、GODEBUG=netmarshal を使って段階的に切り替えます。今回の再審議で、当初の対象だった net.IPNet は外されました。
ステータス変更
active → likely_accept
この提案は2025年4月に一度承認されていました。その後、実装CL 659315 は2026年8月にマージされましたが、@rsc が「実装がまだ正しくない」として issue を再オープンしました。続いて @seankhliao が IPNet の問題を指摘しました。
IPNetは構造体なので、現在は{"IP": "...", "Mask": "..."}というJSONオブジェクトとして出力されている。- ここに
UnmarshalTextを追加しても、エンコーダは文字列を期待するようになるため、既存のオブジェクト形式を読めなくなる。
@ianlancetaylor は、MarshalJSON/UnmarshalJSONを使う代案を CL 844785 で試作しました。しかし2026年10月7日のレビュー会議では、「IPNetには Marshal/Unmarshal 系メソッドを一切追加しない」と決まりました。理由は次のとおりです。 - tip(開発中の最新版)では、
IPとIPMaskが人が読める形式になったことで、IPNetの出力はすでに{"IP":"1.2.3.4","Mask":"255.255.255.0"}と十分読みやすい。 "1.2.3.4/24"形式にしても読みやすさはわずかしか上がらず、追加の複雑さに見合わない。- json/v2 を使えば、型ごとにエンコーダ/デコーダを指定して短い形式を選べる。
- CIDR 表記(
/24のようにプレフィックス長で書く形式)では、連続していないマスクを表せない。
この修正を反映した内容で、改めて likely accept となりました。
技術的背景
現状の問題点
HardwareAddr と IPMask は、どちらも基底型が []byte です。そのため encoding/json はこれらを base64 文字列として出力します。net.IP には MarshalText があるので "192.0.2.1" のように出力されますが、MAC アドレスは次のようになります。
mac, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff")
b, _ := json.Marshal(mac)
// "qrvM3e7/" ← base64。人には読めない
var m net.HardwareAddr
json.Unmarshal([]byte(`"aa:bb:cc:dd:ee:ff"`), &m)
// error: illegal base64 data at input byte 2
一方で、この base64 形式で実際にデータをやり取りしているコードも存在します(@seankhliao の GitHub コード検索で確認)。そのため、出力形式を単純に切り替えると、旧バージョンのバイナリが新しい形式を読めなくなります。
提案された解決策
- Go 1.N で
HardwareAddrとIPMaskにMarshalTextを追加する。出力形式はGODEBUG=netmarshalで切り替え、Go 1.N の時点ではデフォルトを off(従来の base64 出力)とする。HardwareAddrの新形式はString()と同じaa:bb:cc:dd:ee:ff。IPMaskのString()は16進文字列(ffffff00)で、base64 と区別できない。そのため新形式は IP アドレス表記(255.255.255.0)とする。
- 同じ Go 1.N で
UnmarshalTextも追加する。こちらはGODEBUGの設定にかかわらず、旧 base64 形式と新形式の両方を受け付ける。 - 数リリースのあいだ様子を見て、新形式を読めるバイナリを広く行き渡らせる。
- 早くても Go 1.(N+2) で
netmarshalのデフォルトを 1 に切り替える。
@ianlancetaylor が示した計画では、GODEBUG設定は最終的に削除される予定です。
これによって何ができるようになるか
- 設定ファイル: JSON や YAML の設定で、MAC アドレスやネットマスクを
"aa:bb:cc:dd:ee:ff"、"255.255.255.0"のように直接書いて読み込める。 - API: ネットワーク機器の管理 API や DHCP・在庫管理システムで、人や他言語のクライアントが読める JSON を出力できる。
- TextMarshaler 対応エンコーダ全般:
encoding.TextMarshaler(テキストへの変換を定義するインターフェース)を使うエンコーダであれば、YAML や TOML などでも同じ恩恵が得られる。
コード例
type Iface struct {
MAC net.HardwareAddr `json:"mac"`
Mask net.IPMask `json:"mask"`
}
// Before: base64 になり、独自型でラップする必要があった
type MAC net.HardwareAddr
func (m MAC) MarshalText() ([]byte, error) { return []byte(net.HardwareAddr(m).String()), nil }
func (m *MAC) UnmarshalText(b []byte) error {
hw, err := net.ParseMAC(string(b)); *m = MAC(hw); return err
}
// After: 標準型のまま扱える(GODEBUG=netmarshal=1、または既定値が切り替わった後)
b, _ := json.Marshal(Iface{MAC: mac, Mask: net.CIDRMask(24, 32)})
// {"mac":"aa:bb:cc:dd:ee:ff","mask":"255.255.255.0"}
// 旧形式の {"mac":"qrvM3e7/", ...} も引き続き Unmarshal できる
議論のハイライト
- 互換性の懸念で長期停滞: 2019年に @rsc が「既存の base64 エンコードを壊せない」と指摘し、提案は約6年間保留されました。2025年に @ianlancetaylor が新旧両形式を読める
UnmarshalTextを実装したことで、議論が再開しました。 - #10275 の基準: 「旧エンコードが実質的にバイナリのゴミである場合に限り変更してよい」という過去の合意が判断基準になりました。
time.Durationやurl.URLは対象外とされています。 - 「新しいバイナリが書き、古いバイナリが読む」問題: この問題への対策として、まず読み込み側だけを先に普及させ、出力形式は後から切り替える3段階方式が採用されました。
IPMaskの表記:String()の16進表記は base64 と区別できないため、IP アドレス形式にすることになりました。IPNetの除外(今回の変更点): 構造体であるため、UnmarshalTextでは旧形式のオブジェクトを読めません。MarshalJSONを使う案もありましたが、効果が小さいとして見送られました。