メインコンテンツへスキップ
#29678active

net: add MarshalText/UnmarshalText to HardwareAddr, IPNet, IPMask

新規提案

要約

AIによる要約であり、誤りを含む場合があります。

概要

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 整理)は以下の通りです。

  1. Go 1.N: MarshalText を追加するが、GODEBUG=netmarshal=0(デフォルト)の間は従来のbase64エンコードにフォールバックする。同時に UnmarshalText は新形式(可読形式)と旧形式(base64)の両方を受理する。
  2. IPMask は String() の出力がbase64と区別できないため、IPアドレス形式(IPMaskをIPとして表現)を可読形式として採用。
  3. 数リリース後(Go 1.(N+2))、GODEBUG=netmarshal のデフォルトを1に切り替え、可読形式がデフォルトになる。
  4. さらに数リリース後に 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月に再オープンという長期経緯をたどっている。

関連リンク