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

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

ステータス変更: likely_accept accepted

要約

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

概要

net.HardwareAddr(MACアドレス)と net.IPMask に MarshalText / UnmarshalText を追加し、JSON などで人が読める文字列としてエンコード・デコードできるようにする提案です。既存データとの互換性を保つため、GODEBUG=netmarshal を使って段階的に移行します。当初は net.IPNet も対象でしたが、最終的に外されました。

ステータス変更

likely_accept → accepted
この提案は2025年4月に一度承認されています。しかし2026年8月、実装(CL 659315)に問題があるとして @rsc が再オープンしました。@seankhliao は、IPNet が構造体であるため次の問題があると指摘しました。

  • IPNet は現在、JSON では {"IP":..., "Mask":...} というオブジェクトとして出力されている
  • MarshalText を追加すると、出力が文字列に変わってしまう
  • UnmarshalText では、旧形式のオブジェクトを読み込めない
    2026年10月7日のレビューでは、IPNet には Marshal/Unmarshal 系のメソッドを一切追加しないことに決まりました。理由は、tip(開発版)では IP と IPMask が文字列化されるため、IPNet は何もしなくても {"IP": "1.2.3.4", "Mask": "255.255.255.0"} という十分読みやすい形になるからです。この修正版が likely accept を経て、合意に変化がなかったため10月14日に正式承認されました。@ianlancetaylor によると、修正後の内容はすでに tip に実装済みで、導入バージョンは Go 1.28(N = 28)です。

技術的背景

現状の問題点

net.HardwareAddr と net.IPMask の基底型は []byte です。そのため encoding/json はこれらを base64 文字列として扱います。一方、net.IP はすでに MarshalText を実装しているので、"192.168.0.1" のように読みやすい形で出力されます。この非対称さのせいで、"aa:bb:cc:dd:ee:ff" のような JSON を HardwareAddr にデコードしようとすると、illegal base64 data エラーになります。
一方で、base64 形式で JSON に保存しているコードも GitHub 上に実在します。このため、単純に出力形式を変えると互換性が壊れます(関連する互換性方針は #10275 で議論されています)。

提案された解決策

  1. Go 1.N(= 1.28): HardwareAddr と IPMask に MarshalText を追加する。ただし GODEBUG=netmarshal が有効な場合だけ読みやすい形式で出力し、デフォルト(オフ)では従来どおり base64 で出力する。
    • HardwareAddr は既存の String() と同じ形式(aa:bb:cc:dd:ee:ff)で出力する。
    • IPMask は IP アドレスの形式(255.255.255.0)で出力する。既存の IPMask.String() は ffffff00 という16進表記で、base64 と区別できないため採用しない。
  2. 同じく Go 1.N: UnmarshalText を追加する。GODEBUG の設定に関係なく、旧 base64 形式と新しい読みやすい形式の両方を受け付ける。
  3. 新しい UnmarshalText が普及するまで、数リリース待つ。
  4. 早くても Go 1.(N+2): netmarshal のデフォルトを 1 に切り替える。

これによって何ができるようになるか

  • 設定ファイル: JSON や YAML の設定に MAC アドレスやサブネットマスクを人が読める形で書き、そのまま構造体にデコードできます。
  • REST API: ネットワーク機器管理 API などで、net.HardwareAddr 型のフィールドを独自ラッパー型なしで使えます。
  • ログ・監視データ: 構造化ログに出力した MAC アドレスを、人がそのまま読めます。
  • MarshalText / UnmarshalText は汎用のインターフェースです。そのため JSON に限らず、YAML や TOML など encoding.TextMarshaler に対応したエンコーダーでも同じように動作します。

コード例

type Device struct {
    MAC  net.HardwareAddr `json:"mac"`
    Mask net.IPMask       `json:"mask"`
}
// Before: base64 になり、人が読める形式は読み込めない
b, _ := json.Marshal(Device{MAC: mac, Mask: net.CIDRMask(24, 32)})
// {"mac":"qrvM3e7/","mask":"////AA=="}
err := json.Unmarshal([]byte(`{"mac":"aa:bb:cc:dd:ee:ff"}`), &d)
// err: illegal base64 data at input byte 2
// ワークアラウンド: 独自型を定義して ParseMAC を呼ぶ
type MAC net.HardwareAddr
func (m *MAC) UnmarshalText(b []byte) error {
    hw, err := net.ParseMAC(string(b)); *m = MAC(hw); return err
}
// After (Go 1.28 以降): 新旧どちらの形式もそのままデコードできる
json.Unmarshal([]byte(`{"mac":"aa:bb:cc:dd:ee:ff","mask":"255.255.255.0"}`), &d) // OK
json.Unmarshal([]byte(`{"mac":"qrvM3e7/","mask":"////AA=="}`), &d)            // 旧形式も OK
// GODEBUG=netmarshal=1(将来はこれがデフォルト)の場合の出力
// {"mac":"aa:bb:cc:dd:ee:ff","mask":"255.255.255.0"}

議論のハイライト

  • 互換性の懸念で約6年停滞: 2019年に @rsc と @ianlancetaylor が「旧バージョンで出力した JSON を新バージョンで読めなくなる」という問題を指摘し、提案は長く保留されました。2025年に @ianlancetaylor が新旧両形式を読める実装を書き、議論が再開しました。
  • 3つの選択肢: @rsc は「(1) 新形式だけにする」「(2) Unmarshal で base64 も受け付ける」「(3) まず読み取り側を広く対応させ、出力形式の切り替えは後のリリースで行う」という、慎重さの異なる3案を示しました。base64 で JSON に保存している実例が見つかったため、最も慎重な (3) が採用されました。この方式では、新しいバイナリが出力したデータを古いバイナリが読めなくなる事態を避けられます。
  • 変更してよい基準: #10275 の方針では、既存のエンコード結果が「バイナリのゴミ(binary garbage)」に近い場合は変更してもよいとされています。HardwareAddr と IPMask の base64 はこれに当たりますが、time.Duration や url.URL は当たりません。
  • IPMask の形式: String() の16進表記は base64 と区別できないため、IP アドレス形式が選ばれました。
  • IPNet の除外: 構造体である IPNet に UnmarshalText を付けると、旧形式のオブジェクトを読めなくなります。MarshalJSON / UnmarshalJSON で対応する案(CL 844785)も検討されましたが、"1.2.3.4/24" は現状のオブジェクト形式と比べて読みやすさがわずかに上がるだけで、複雑さに見合わないと判断されました。また、マスクが連続していない場合は CIDR 表記で表せないことも理由です。json/v2 なら型ごとにエンコーダーを指定できるので、短い形式が必要な場合はそちらで対応できます。

関連リンク