Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 17690] ファイルの概要

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、JSONエンコーダの内部的な挙動に関するバグ修正です。具体的には、json.Marshal が値のアドレス可能性(reflect.Value.CanAddr())を誤ってキャッシュし、その結果、実行時にパニックを引き起こす可能性があった問題を解決します。エンコーダの決定を、型のアドレス可能性ではなく、実行時の値のアドレス可能性に基づいて動的に行うように変更されました。

コミット

commit 0f3ea75020cf7dda64805fe9aeef26be60cf16cd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Sep 23 19:57:19 2013 -0700

    encoding/json: don't cache value addressability when building first encoder
    
    newTypeEncoder (called once per type and then cached) was
    looking at the first value seen of that type's addressability
    and caching the encoder decision.  If the first value seen was
    addressable and a future one wasn't, it would panic.
    
    Instead, introduce a new wrapper encoder type that checks
    CanAddr at runtime.
    
    Fixes #6458
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/13839045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0f3ea75020cf7dda64805fe9aeef26be60cf16cd

元コミット内容

encoding/json パッケージにおいて、最初のエンコーダ構築時に値のアドレス可能性をキャッシュしないようにする変更です。 newTypeEncoder 関数は、型ごとに一度だけ呼び出され、その結果がキャッシュされます。この関数は、その型で最初に見られた値のアドレス可能性を調べてエンコーダの決定をキャッシュしていました。もし最初に見られた値がアドレス可能であったにもかかわらず、後続の値がアドレス可能でなかった場合、パニックが発生していました。 この問題を解決するため、実行時に CanAddr をチェックする新しいラッパーエンコーダ型が導入されました。 この変更は Issue #6458 を修正します。

変更の背景

Go言語の encoding/json パッケージは、Goの値をJSON形式にエンコードする機能を提供します。このパッケージは効率性を高めるために、一度エンコードされた型のエンコーダをキャッシュする仕組みを持っています。具体的には、newTypeEncoder 関数が特定の型に対するエンコーダを生成し、これが typeEncoder によってキャッシュされます。

問題は、newTypeEncoder がエンコーダを生成する際に、その型で「最初に見られた値」のアドレス可能性(reflect.Value.CanAddr())に基づいてエンコーダの挙動を決定し、その決定をキャッシュしていた点にありました。

Goの reflect パッケージでは、reflect.Value が表す値がアドレス可能であるかどうかを CanAddr() メソッドで確認できます。アドレス可能であるとは、その値がメモリ上の特定のアドレスを持ち、ポインタを取得できることを意味します。例えば、構造体のフィールドやスライスの要素は通常アドレス可能ですが、マップの値やインターフェースの値は直接的にはアドレス可能ではありません。

json.Marshalerencoding.TextMarshaler といったインターフェースを実装する型は、JSONエンコード時に特別な処理を必要とします。これらのインターフェースは、値そのものが実装することもあれば、その値へのポインタが実装することもあります。例えば、type MyType struct {}MyType 自身ではなく *MyTypejson.Marshaler を実装している場合、MyType のインスタンスをエンコードする際には、そのインスタンスのアドレスを取得してポインタとして json.Marshaler メソッドを呼び出す必要があります。

従来の newTypeEncoder は、エンコーダを生成する際に、引数として渡された vx reflect.Value (その型の「例となる値」) の CanAddr() の結果を見て、アドレスが必要な Marshaler 実装が存在するかどうかを判断していました。もし vx がアドレス可能で、かつ vx.Addr().Interface().(Marshaler) が成功した場合、アドレスを必要とするエンコーダ(addrMarshalerEncoder など)を生成してキャッシュしていました。

しかし、この「例となる値」は、必ずしもその型でエンコードされるすべての値のアドレス可能性を代表するものではありませんでした。例えば、json.Marshal(&myStruct) のようにポインタを渡した場合、myStruct はアドレス可能ですが、json.Marshal(myStruct) のように値を直接渡した場合、myStruct はアドレス可能ではない(コピーされた値が渡されるため)可能性があります。

もし、最初に json.Marshal(&myStruct) が呼び出され、myStruct がアドレス可能であると判断されて addrMarshalerEncoder がキャッシュされたとします。その後、json.Marshal(myStruct) のようにアドレス可能ではない myStruct のコピーが渡された場合、キャッシュされた addrMarshalerEncoderv.CanAddr()false である値に対して v.Addr() を呼び出そうとし、Goのランタイムパニック(panic: reflect.Value.Addr of unaddressable value)を引き起こしていました。

このコミットは、このアドレス可能性の誤ったキャッシュが引き起こすパニックを防ぐために行われました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と encoding/json パッケージの内部動作に関する知識が必要です。

  1. reflect パッケージ:

    • reflect.Type: Goの型の静的な情報(名前、種類、メソッドなど)を表します。reflect.TypeOf(v) で取得できます。
    • reflect.Value: Goの実行時の値の動的な情報(実際の値、アドレス可能性、フィールド、メソッドなど)を表します。reflect.ValueOf(v) で取得できます。
    • reflect.Value.CanAddr(): reflect.Value が表す値がアドレス可能であるかどうかを返します。true の場合、Addr() メソッドを呼び出してその値へのポインタを取得できます。
    • reflect.Value.Addr(): reflect.Value が表す値へのポインタを reflect.Value として返します。CanAddr()false の場合に呼び出すとパニックします。
    • reflect.Value.Elem(): ポインタが指す要素の reflect.Value を返します。
    • reflect.Type.Implements(u reflect.Type): reflect.Type がインターフェース u を実装しているかどうかを返します。
  2. encoding/json パッケージのエンコーダキャッシュ:

    • encoding/json パッケージは、パフォーマンス最適化のために、Goの型ごとにJSONエンコーダをキャッシュします。これにより、同じ型の値を複数回エンコードする際に、エンコーダの生成コストを削減できます。
    • typeEncoder(t reflect.Type) 関数が、特定の reflect.Type に対応する encoderFunc を取得します。もしキャッシュに存在しない場合、newTypeEncoder を呼び出して新しいエンコーダを生成し、キャッシュに格納します。
    • encoderFuncfunc(e *encodeState, v reflect.Value, quoted bool) というシグネチャを持つ関数で、実際のエンコード処理を行います。
  3. json.Marshaler インターフェース:

    type Marshaler interface {
        MarshalJSON() ([]byte, error)
    }
    

    このインターフェースを実装する型は、MarshalJSON メソッドを自分で定義することで、JSONへのエンコード方法をカスタマイズできます。

  4. encoding.TextMarshaler インターフェース:

    type TextMarshaler interface {
        MarshalText() (text []byte, err error)
    }
    

    このインターフェースを実装する型は、MarshalText メソッドを自分で定義することで、テキスト形式(JSON文字列、数値、真偽値など)へのエンコード方法をカスタマイズできます。

  5. アドレスを必要とするインターフェース実装: Goでは、インターフェースは値型でもポインタ型でも実装できます。

    • type MyType struct {}func (m MyType) MarshalJSON() ([]byte, error) を持つ場合、MyTypejson.Marshaler を実装します。
    • type MyType struct {}func (m *MyType) MarshalJSON() ([]byte, error) を持つ場合、*MyTypejson.Marshaler を実装します。この場合、MyType の値をエンコードするには、その値へのポインタを取得する必要があります。

技術的詳細

このコミットの核心は、エンコーダの生成時に値のアドレス可能性を静的に判断してキャッシュするのではなく、実行時に値のアドレス可能性を動的にチェックするメカニズムを導入した点にあります。

主な変更点は以下の通りです。

  1. newTypeEncoder のシグネチャ変更:

    • 変更前: func newTypeEncoder(t reflect.Type, vx reflect.Value) encoderFunc vx (例となる値) を受け取っていたため、その vx のアドレス可能性に依存したエンコーダを生成する可能性がありました。
    • 変更後: func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc vx 引数が削除され、代わりに allowAddr というブール値が導入されました。これは、このエンコーダがアドレス可能な値も処理できるべきかどうかを示すフラグです。これにより、エンコーダ生成時に特定の値のアドレス可能性に依存するのをやめ、型の特性と、そのエンコーダがアドレスを扱う必要があるかどうかの一般的なフラグに基づいてエンコーダを生成するようになりました。
  2. condAddrEncoder 型の導入:

    • 新しい構造体 condAddrEncoder が導入されました。
    type condAddrEncoder struct {
        canAddrEnc, elseEnc encoderFunc
    }
    
    • この構造体は、2つの encoderFunc を持ちます。
      • canAddrEnc: 値がアドレス可能 (v.CanAddr() == true) な場合に呼び出されるエンコーダ。
      • elseEnc: 値がアドレス可能でない (v.CanAddr() == false) 場合に呼び出されるエンコーダ。
    • condAddrEncoderencode メソッドは、エンコード対象の reflect.ValueCanAddr() を実行時にチェックし、その結果に基づいて適切なエンコーダ(canAddrEnc または elseEnc)に処理を委譲します。
  3. newCondAddrEncoder 関数の導入:

    • func newCondAddrEncoder(canAddrEnc, elseEnc encoderFunc) encoderFunc
    • この関数は、condAddrEncoder のインスタンスを生成し、その encode メソッドを encoderFunc として返します。これにより、アドレス可能性のチェックと適切なエンコーダの選択がカプセル化されます。
  4. Marshaler および TextMarshaler の処理ロジックの変更:

    • newTypeEncoder 内で MarshalerTextMarshaler インターフェースの実装をチェックする際、t.Implements(marshalerType) (値がインターフェースを実装しているか) と reflect.PtrTo(t).Implements(marshalerType) (ポインタがインターフェースを実装しているか) の両方を考慮するようになりました。
    • もしポインタがインターフェースを実装している場合(例: *MyTypejson.Marshaler を実装)、newCondAddrEncoder を使用して、実行時に値がアドレス可能かどうかをチェックし、アドレス可能であれば addrMarshalerEncoder (または addrTextMarshalerEncoder) を、そうでなければ newTypeEncoder(t, false) で生成されたエンコーダ(アドレスを必要としないエンコーダ)を呼び出すようにしました。これにより、同じ型でも、値として渡された場合とポインタとして渡された場合の両方に対応できるようになります。
  5. newStructEncoder, newMapEncoder, newSliceEncoder, newArrayEncoder, newPtrEncoder の変更:

    • これらの関数も newTypeEncoder と同様に、vx reflect.Value 引数を削除し、reflect.Type のみを受け取るように変更されました。これにより、エンコーダ生成時に具体的な値のアドレス可能性に依存するのを完全に排除しました。
    • newStructEncoder では、フィールドのエンコーダを生成する際に typeByIndex(t, f.index) を使用して、フィールドの型情報のみに基づいてエンコーダを取得するように変更されました。
  6. typeByIndex ヘルパー関数の追加:

    • func typeByIndex(t reflect.Type, index []int) reflect.Type
    • このヘルパー関数は、構造体のフィールドのインデックスパスに基づいて、そのフィールドの reflect.Type を取得します。ポインタ型の場合も適切に Elem() を呼び出して処理します。

これらの変更により、encoding/json は、エンコーダをキャッシュする際に、値のアドレス可能性という動的な特性を考慮に入れる必要がなくなり、エンコード時に初めてその値のアドレス可能性をチェックするようになりました。これにより、異なるアドレス可能性を持つ同じ型の値が混在しても、パニックを起こすことなく正しくエンコードできるようになりました。

コアとなるコードの変更箇所

  • src/pkg/encoding/json/encode.go:
    • encoderFunc のシグネチャ変更: _ bool から quoted bool へ。
    • valueEncoder 関数から v reflect.ValueType() を直接取得するように変更。
    • typeEncoder 関数のシグネチャ変更: vx reflect.Value 引数を削除。
    • newTypeEncoder 関数のシグネチャ変更: vx reflect.Value 引数を削除し、allowAddr bool 引数を追加。
    • newTypeEncoder 内での Marshaler および TextMarshaler のチェックロジックの変更と newCondAddrEncoder の使用。
    • newTypeEncoder 内の switch vx.Kind()switch t.Kind() に変更。
    • newStructEncoder, newMapEncoder, newSliceEncoder, newArrayEncoder, newPtrEncoder のシグネチャ変更: vx reflect.Value 引数を削除。
    • newStructEncoder 内でフィールドエンコーダを取得するロジックの変更。
    • condAddrEncoder 構造体と newCondAddrEncoder 関数の追加。
    • typeByIndex ヘルパー関数の追加。
  • src/pkg/encoding/json/encode_test.go:
    • TestIssue6458 という新しいテストケースの追加。

コアとなるコードの解説

このコミットの主要な変更は、encoding/json パッケージがエンコーダを生成し、キャッシュする方法の根本的な見直しにあります。

  1. typeEncoder(t reflect.Type): この関数は、特定の reflect.Type に対応する encoderFunc を取得する役割を担います。変更前は typeEncoder(t reflect.Type, vx reflect.Value) でしたが、vx (例となる値) が削除され、型情報のみに基づいてエンコーダを取得するようになりました。これにより、キャッシュキーが型のみになり、値のアドレス可能性に依存しなくなりました。

  2. newTypeEncoder(t reflect.Type, allowAddr bool): この関数は、実際に型 t のエンコーダを構築します。

    • allowAddr 引数は、このエンコーダがアドレス可能な値も処理できるべきかどうかを示します。これは、json.Marshal(&x) のようにポインタが渡された場合に、そのポインタが指す値がアドレス可能であるかどうかを考慮する必要があることを示唆します。
    • Marshaler / TextMarshaler の処理: 最も重要な変更点の一つです。以前は vx.Interface().(Marshaler)vx.Addr().Interface().(Marshaler) のように、具体的な値 vx を使ってインターフェース実装をチェックしていました。 変更後は、t.Implements(marshalerType) (値が実装) と reflect.PtrTo(t).Implements(marshalerType) (ポインタが実装) の両方をチェックします。 もし *TMarshaler を実装している場合(つまり、値 T をエンコードするにはアドレスが必要な場合)、newCondAddrEncoder を使用してエンコーダを生成します。
      if t.Kind() != reflect.Ptr && allowAddr { // 値型で、かつアドレスを考慮する必要がある場合
          if reflect.PtrTo(t).Implements(marshalerType) { // ポインタがMarshalerを実装している場合
              return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false))
          }
      }
      
      ここで newCondAddrEncoder が使われることで、エンコード時に v.CanAddr() をチェックし、アドレス可能であれば addrMarshalerEncoder (ポインタ経由で MarshalJSON を呼び出すエンコーダ) を、そうでなければ newTypeEncoder(t, false) で生成されたエンコーダ(アドレスを必要としないエンコーダ)を呼び出すように動的に切り替わります。これにより、同じ型でも、アドレス可能かどうかに応じて適切なエンコードパスが選択されるようになります。
  3. condAddrEncodernewCondAddrEncoder: これらは、実行時のアドレス可能性チェックをカプセル化するための新しいメカニズムです。 condAddrEncoder は、canAddrEnc (アドレス可能な場合のエンコーダ) と elseEnc (アドレス可能でない場合のエンコーダ) の2つの encoderFunc を保持します。 その encode メソッドは、渡された reflect.Value v に対して v.CanAddr() を呼び出し、結果に応じて canAddrEnc または elseEnc を実行します。 これにより、エンコーダがキャッシュされる時点ではアドレス可能性を固定せず、実際のエンコード時に動的に判断できるようになりました。

  4. newStructEncoder などの変更: newStructEncoder や他の複合型(マップ、スライス、配列、ポインタ)のエンコーダ生成関数も、vx reflect.Value 引数を削除し、reflect.Type のみを受け取るようになりました。これにより、エンコーダの生成が型情報のみに依存するようになり、特定のインスタンスのアドレス可能性に影響されることがなくなりました。 特に newStructEncoder では、フィールドのエンコーダを生成する際に se.fieldEncs[i] = typeEncoder(typeByIndex(t, f.index)) となり、フィールドの型情報に基づいてエンコーダを取得するようになりました。

  5. typeByIndex: 構造体のネストされたフィールドの型を reflect.Type から取得するためのヘルパー関数です。reflect.Value を使わずに型情報のみでフィールドの型を辿れるようにするために導入されました。

これらの変更により、encoding/json は、エンコーダのキャッシュが値のアドレス可能性に起因するパニックを引き起こすことなく、より堅牢に動作するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (encoding/json, reflect パッケージ)
  • コミットメッセージとコードの差分
  • Go言語のインターフェースとポインタに関する一般的な知識