メインコンテンツへスキップ
#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 コード検索で確認)。そのため、出力形式を単純に切り替えると、旧バージョンのバイナリが新しい形式を読めなくなります。

提案された解決策

  1. 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)とする。
  2. 同じ Go 1.N で UnmarshalText も追加する。こちらは GODEBUG の設定にかかわらず、旧 base64 形式と新形式の両方を受け付ける。
  3. 数リリースのあいだ様子を見て、新形式を読めるバイナリを広く行き渡らせる。
  4. 早くても 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 を使う案もありましたが、効果が小さいとして見送られました。

関連リンク