net: add MarshalText/UnmarshalText to HardwareAddr, IPNet, IPMask
要約
概要
net.HardwareAddr(MACアドレス)、net.IPNet、net.IPMask に MarshalText/UnmarshalText を実装し、JSON・YAMLなどのテキストベースの形式で人間が読める文字列表現(例: "aa:bb:cc:dd:ee:ff")として直列化できるようにする提案です。
ステータス変更
(未設定/Accepted後に再オープン) → active
2019年に @agnivade が「net.IP は json.Marshaler を実装しているのに net.HardwareAddr は []byte のデフォルト動作(base64エンコード)になってしまい非対称だ」と提起したのが発端です。2025年4月に一度「accepted」となりましたが、実装を進める中で @rsc が「net.IPNet は現在 encoding/json により構造体(オブジクト)としてマーシャルされるため、MarshalText を単純に追加すると出力が強制的に文字列(ダブルクォート)でラップされ、UnmarshalText 側もJSONオブジェクトを受け取れず壊れてしまう」という実装上の問題を実装CL(go.dev/cl/659315)で指摘し、2026年8月25日にissueを再オープンしました。これを受けて2026年9月23日のレビュー会議で改めて議論の対象とし、9月24日に proposal review board の active カラムへ追加され、週次のproposal reviewで継続審議されることになりました。
技術的背景
現状の問題点
net.HardwareAddr は内部的に []byte のエイリアス型であるため、encoding/json でマーシャルすると自動的にbase64文字列になります。
addr, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff")
b, _ := json.Marshal(addr)
fmt.Println(string(b)) // => "qrvM3e4=" (base64、人間には読めない)
同様に net.IPMask も []byte ベースでbase64になり、net.IPNet は IP・Mask フィールドを持つ構造体としてマーシャルされるため、各フィールドがbase64のネストしたオブジェクトになってしまいます。
提案された解決策
アクセプト時点で合意された移行計画(@aclements 整理)は以下の通りです。
- Go 1.N:
MarshalTextを追加するが、GODEBUG=netmarshal=0(デフォルト)の間は従来のbase64エンコードにフォールバックする。同時にUnmarshalTextは新形式(可読形式)と旧形式(base64)の両方を受理する。 IPMaskはString()の出力がbase64と区別できないため、IPアドレス形式(IPMaskをIPとして表現)を可読形式として採用。- 数リリース後(Go 1.(N+2))、
GODEBUG=netmarshalのデフォルトを1に切り替え、可読形式がデフォルトになる。 - さらに数リリース後に
GODEBUG自体を削除。
ただしnet.IPNetは構造体としてマーシャルされる現状があるため、MarshalText/UnmarshalTextだけでは対応できず、MarshalJSON/UnmarshalJSONを別途実装して「オブジェクト形式」と「文字列形式」の両方をハンドルする必要がある、という点が2026年8月の再オープンの核心的な技術課題です(@seankhliao の指摘)。
これによって何ができるようになるか
- MACアドレスやIPネットワーク情報をJSON/YAML設定ファイルやAPIレスポンスに人間が読める形で直接埋め込める
- デバッグ時にログやAPIレスポンスをそのまま目視確認できる(base64のデコード作業が不要)
- ネットワーク管理ツールや構成管理システムで、これまで独自に
String()/ParseMAC()を呼び出して手動変換していた処理が不要になる
コード例
// Before: base64になってしまい人間が読めない、手動変換が必要
addr, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff")
b, _ := json.Marshal(addr)
// => "qrvM3e4="
type wrapper struct{ Addr string }
w := wrapper{Addr: addr.String()} // 手動でStringを呼ぶワークアラウンド
j, _ := json.Marshal(w)
// After: MarshalText/UnmarshalText実装後(提案が完了すれば)
addr, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff")
b, _ := json.Marshal(addr)
// => "aa:bb:cc:dd:ee:ff"
var addr2 net.HardwareAddr
json.Unmarshal(b, &addr2) // 新旧どちらの形式も受理
議論のハイライト
- 後方互換性が最大の論点: @rsc・@ianlancetaylor は「あるGoバージョンでマーシャルした値を別バージョンでアンマーシャルできなくなる」ことを一貫して懸念し、実際に本番でJSON経由の
HardwareAddrを運用しているコードが GitHub コード検索で見つかった(@seankhliao 調査)ことから、いきなり形式を変えずGODEBUGによる段階的移行が必要と判断された。 IPMaskの曖昧性:IPMask.String()の16進出力はbase64と区別がつかないため、IPアドレス形式で表現するという設計判断がなされた。encoding/json/v2との切り分け: 一部コメントで「json/v2限定で新形式にすればよいのでは」という代替案も出たが、std libのencoding/json自体を変える方向で議論が進んだ。- 一度acceptedになった後の再オープン: 実装段階で
net.IPNetが構造体としてマーシャルされる点への対応が不十分であることが判明し、MarshalTextだけでなくMarshalJSON/UnmarshalJSONの追加実装が必要と判明。これによりproposalの詳細設計自体の見直しが必要となり、再度activeとして審議されることになった。 - タイムライン: 初期issue提起(2019年)から6年以上、複数回のオンホールドを経て2025年4月にaccepted、その後実装の技術的困難により2026年8月に再オープンという長期経緯をたどっている。