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

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

このコミットは、Go言語の標準ライブラリである encoding/json パッケージにおける、JSON配列のデコード処理に関するバグ修正とコードのクリーンアップを目的としています。具体的には、reflect パッケージのAPI変更によって残された不要な変数を除去し、配列とスライスの型チェックおよび動的なリサイズ処理の不具合を修正しています。これにより、JSONからGoの配列やスライスへのデコードがより堅牢かつ正確に行われるようになります。

コミット

commit 4a4c39e7d4f95ffcaa6971c35c4adeb740dcc515
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Dec 19 15:32:06 2011 -0500

    encoding/json: cleanup leftover variables in array decoding.
    
    An old update for API changes in reflect package left several
    helper variables that do not have a meaning anymore, and
    the type checking of arrays vs slices was broken.
    Fixes #2513.
    
    R=ultrotter, rsc
    CC=golang-dev, remy
    https://golang.org/cl/5488094

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

https://github.com/golang/go/commit/4a4c39e7d4f95ffcaa6971c35c4adeb740dcc515

元コミット内容

このコミットは、encoding/json パッケージの配列デコードロジックにおける、残存する不要な変数のクリーンアップと、配列とスライスの型チェックの不具合修正を目的としています。コミットメッセージによると、以前の reflect パッケージのAPI変更に伴う更新が原因で、意味を持たなくなったヘルパー変数がコード内に残されており、その結果、配列とスライスの型チェックが正しく機能していなかったとのことです。この問題は Issue #2513 として報告されており、このコミットによって修正されました。

変更の背景

Go言語の reflect パッケージは、プログラムの実行時に型情報を検査・操作するための強力な機能を提供します。encoding/json パッケージは、JSONデータをGoの構造体やプリミティブ型にデコードする際に、この reflect パッケージを内部的に利用して、動的な型変換やフィールドへの値の割り当てを行っています。

コミットメッセージにある「An old update for API changes in reflect package」とは、Go言語の進化に伴う reflect パッケージのAPI変更を指しています。Goは初期の段階で活発な開発が行われており、APIの変更は珍しいことではありませんでした。このようなAPI変更があった際、encoding/json パッケージのコードが完全に追従しきれず、古いAPIの利用を前提とした変数やロジックが残ってしまったと考えられます。

具体的には、JSONの配列をGoの配列(固定長)やスライス(可変長)にデコードする際、reflect パッケージを使ってターゲットの型を判別し、スライスであれば必要に応じてメモリを再割り当てして拡張する処理が行われます。このコミット以前のコードでは、古い reflect APIの残骸が原因で、この型判別やスライス拡張のロジックに不具合が生じていたと推測されます。Issue #2513 は、おそらくこの不具合によって特定のJSON配列が正しくデコードされない、あるいは予期せぬエラーが発生するといった具体的な問題を示していたでしょう。

このコミットは、これらの残存する問題を取り除き、encoding/json パッケージの堅牢性と正確性を向上させるために行われました。

前提知識の解説

Go言語の encoding/json パッケージ

encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。主に json.Marshal (Goの値をJSONにエンコード) と json.Unmarshal (JSONデータをGoの値にデコード) の2つの関数が中心となります。このコミットは json.Unmarshal の内部実装、特に配列のデコード部分に関わっています。

Go言語の reflect パッケージ

reflect パッケージは、Goのプログラムが自身の構造を検査・操作できるようにする機能を提供します。これにより、実行時に変数の型、値、構造体のフィールドなどを動的に調べたり、変更したりすることが可能になります。

  • reflect.Value: Goの変数の値を表す型です。reflect.ValueOf(x) で任意のGoの値 x から reflect.Value を取得できます。
  • reflect.Type: Goの変数の型情報を表す型です。reflect.TypeOf(x) で任意のGoの値 x から reflect.Type を取得できます。
  • Kind(): reflect.Value または reflect.Type のメソッドで、その値または型がプリミティブ型(Int, Stringなど)、構造体(Struct)、配列(Array)、スライス(Slice)、インターフェース(Interface)など、どのカテゴリに属するかを返します。
  • reflect.Arrayreflect.Slice: Kind() メソッドが返す定数で、それぞれGoの配列とスライスを表します。
  • Cap() (Capacity): スライスが現在保持できる要素の最大数を返します。
  • Len() (Length): スライスが現在保持している要素の数を返します。
  • MakeSlice(typ Type, len, cap int) Value: 指定された型、長さ、容量を持つ新しいスライスを作成します。
  • Copy(dst, src Value) int: src スライスの要素を dst スライスにコピーします。
  • SetLen(n int): スライスの長さを n に設定します。

encoding/json パッケージは、JSONの配列をGoの配列やスライスにデコードする際に、これらの reflect パッケージの機能を利用して、ターゲットの型が配列なのかスライスなのかを判別し、スライスであれば必要に応じてその長さを調整したり、容量を増やしたりします。

Go言語の配列 (Array) とスライス (Slice)

Go言語には、固定長の「配列 (Array)」と可変長の「スライス (Slice)」という2種類のシーケンス型があります。

  • 配列: [N]Type の形式で宣言され、要素数が固定です。例えば [3]int は3つの整数を格納できる配列です。
  • スライス: []Type の形式で宣言され、配列を基盤としていますが、長さが可変です。スライスは、基盤となる配列の一部を参照するビューのようなものです。JSONの配列をGoのデータ構造にデコードする場合、通常はスライスが使われます。なぜなら、JSONの配列の長さは事前に分からないことが多く、動的に要素を追加できるスライスの方が柔軟だからです。

UnmarshalTypeError

encoding/json パッケージがJSONデータをGoの型にデコードしようとした際に、JSONのデータ型とGoのターゲットの型が一致しない場合に発生するエラーです。例えば、JSONで数値が来ているのにGoのターゲットが文字列型だった場合などに発生します。

技術的詳細

このコミットの技術的詳細は、主に src/pkg/encoding/json/decode.go ファイル内の decodeState 構造体の value メソッドと array メソッドの変更に集約されます。

  1. value メソッド内のパニック修正: 以前のコードでは、d.scan.redotrue の場合に panic("redo") が発生していました。これは、スキャン状態の巻き戻し処理において、予期せぬ状態に陥った際に発生するデバッグ用のパニック、あるいは未実装のロジックだった可能性があります。このコミットでは、このパニックを回避し、d.scan.redofalse にリセットし、スキャンステップを stateBeginValue に戻すことで、正常な状態遷移を促しています。これにより、特定のJSON入力でデコードがクラッシュする問題を修正したと考えられます。

  2. array メソッド内の型チェックと変数クリーンアップ: array メソッドはJSONの配列をGoの配列またはスライスにデコードする主要なロジックを含んでいます。

    • 不要な変数の削除: 以前のコードでは iv, ok, av, sv といった複数の reflect.Value 型のヘルパー変数が使われていました。これらは reflect パッケージのAPI変更によって冗長になったか、あるいは誤ったロジックを招いていた可能性があります。このコミットでは、これらの変数を削除し、デコード対象の v reflect.Value を直接操作するように変更されています。これにより、コードが簡素化され、意図しない副作用が排除されました。

    • switch v.Kind() による明確な型判別: 以前は if av.Kind() != reflect.Array && av.Kind() != reflect.Slice のような条件分岐で型をチェックしていましたが、新しいコードでは switch v.Kind() を導入し、reflect.Interface, reflect.Array, reflect.Slice の各ケースを明示的に処理しています。

      • reflect.Interface の場合:d.arrayInterface() を呼び出して、インターフェースへのデコードを処理します。これは、ターゲットが interface{} 型の場合に、内部的に適切なスライス型を割り当ててデコードを進めるためのものです。
      • reflect.Array または reflect.Slice の場合:break して、後続の配列/スライスデコードロジックに進みます。
      • default の場合:UnmarshalTypeError を発生させ、ターゲットが配列でもスライスでもインターフェースでもない場合にエラーを報告します。 この変更により、型チェックのロジックがより明確になり、エラーハンドリングも改善されました。
    • スライス拡張ロジックの修正: JSON配列の要素をGoのスライスにデコードする際、スライスの容量が不足した場合に動的に拡張する必要があります。以前のコードでは、av.Cap()sv.IsValid() といった変数を使ってスライスの容量や有効性をチェックしていましたが、これが正しく機能していなかった可能性があります。 新しいコードでは、if v.Kind() == reflect.Slice でスライスであることを確認した上で、i >= v.Cap() で容量不足をチェックし、reflect.MakeSlicereflect.Copy を使って新しい、より大きなスライスを作成し、既存の要素をコピーしています。また、i >= v.Len() で長さが不足している場合に v.SetLen(i + 1) を呼び出してスライスの長さを適切に調整しています。これにより、JSON配列の要素数に応じてGoのスライスが正しく拡張されるようになりました。

    • 配列のゼロ埋めと空スライスの初期化: デコードされたJSON配列の要素数がGoの固定長配列の要素数よりも少なかった場合、残りの要素をゼロ値で埋める必要があります。また、空のJSON配列がデコードされた場合に、Goのスライスが正しく空のスライスとして初期化される必要があります。このコミットでは、これらのエッジケースに対するロジックも修正・簡素化されています。特に、if i == 0 && v.Kind() == reflect.Slice の条件で、空のスライスを reflect.MakeSlice(v.Type(), 0, 0) で正しく初期化するように変更されています。

これらの変更により、encoding/json パッケージは、JSON配列をGoの配列やスライスにデコードする際の堅牢性、正確性、そしてコードの保守性が大幅に向上しました。

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

このコミットで変更された主要なファイルは以下の2つです。

  1. src/pkg/encoding/json/decode.go
  2. src/pkg/encoding/json/decode_test.go

src/pkg/encoding/json/decode.go の変更

--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -228,7 +228,9 @@ func (d *decodeState) value(v reflect.Value) {
 		// Feed in an empty string - the shortest, simplest value -
 		// so that it knows we got to the end of the value.
 		if d.scan.redo {
-			panic("redo")
+			// rewind.
+			d.scan.redo = false
+			d.scan.step = stateBeginValue
 		}
 		d.scan.step(&d.scan, '"')
 		d.scan.step(&d.scan, '"')
@@ -317,25 +319,22 @@ func (d *decodeState) array(v reflect.Value) {
 	}
 	v = pv
 
-	// Decoding into nil interface?  Switch to non-reflect code.
-	iv := v
-	ok := iv.Kind() == reflect.Interface
-	if ok {
-		iv.Set(reflect.ValueOf(d.arrayInterface()))
-		return
-	}
-
 	// Check type of target.
-	av := v
-	if av.Kind() != reflect.Array && av.Kind() != reflect.Slice {
+	switch v.Kind() {
+	default:
 		d.saveError(&UnmarshalTypeError{"array", v.Type()})
 		d.off--
 		d.next()
 		return
+	case reflect.Interface:
+		// Decoding into nil interface?  Switch to non-reflect code.
+		v.Set(reflect.ValueOf(d.arrayInterface()))
+		return
+	case reflect.Array:
+	case reflect.Slice:
+		break
 	}
 
-	sv := v
-
 	i := 0
 	for {
 		// Look ahead for ] - can only happen on first iteration.
@@ -349,23 +348,25 @@ func (d *decodeState) array(v reflect.Value) {
 		d.scan.undo(op)
 
 		// Get element of array, growing if necessary.
-		if i >= av.Cap() && sv.IsValid() {
-			newcap := sv.Cap() + sv.Cap()/2
-			if newcap < 4 {
-				newcap = 4
+		if v.Kind() == reflect.Slice {
+			// Grow slice if necessary
+			if i >= v.Cap() {
+				newcap := v.Cap() + v.Cap()/2
+				if newcap < 4 {
+					newcap = 4
+				}
+				newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
+				reflect.Copy(newv, v)
+				v.Set(newv)
 			}
-			newv := reflect.MakeSlice(sv.Type(), sv.Len(), newcap)
-			reflect.Copy(newv, sv)
-			sv.Set(newv)
-		}
-		if i >= av.Len() && sv.IsValid() {
-			// Must be slice; gave up on array during i >= av.Cap().
-			sv.SetLen(i + 1)
+			if i >= v.Len() {
+				v.SetLen(i + 1)
+			}
 		}
 
 		// Decode into element.
-		if i < av.Len() {
-			d.value(av.Index(i))
+		if i < v.Len() {
+			d.value(v.Index(i))
 		} else {
 			// Ran out of fixed array: skip.
 			d.value(reflect.Value{})
@@ -382,19 +383,19 @@ func (d *decodeState) array(v reflect.Value) {
 		}
 	}
 
-	if i < av.Len() {
-		if !sv.IsValid() {
+	if i < v.Len() {
+		if v.Kind() == reflect.Array {
 			// Array.  Zero the rest.
-			z := reflect.Zero(av.Type().Elem())
-			for ; i < av.Len(); i++ {
-				av.Index(i).Set(z)
+			z := reflect.Zero(v.Type().Elem())
+			for ; i < v.Len(); i++ {
+				v.Index(i).Set(z)
 			}
 		} else {
-			sv.SetLen(i)
+			v.SetLen(i)
 		}
 	}
-	if i == 0 && av.Kind() == reflect.Slice && sv.IsNil() {
-		sv.Set(reflect.MakeSlice(sv.Type(), 0, 0))
+	if i == 0 && v.Kind() == reflect.Slice {
+		v.Set(reflect.MakeSlice(v.Type(), 0, 0))
 	}
 }

src/pkg/encoding/json/decode_test.go の変更

--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -74,6 +74,12 @@ var unmarshalTests = []unmarshalTest{\n 
 	// syntax errors
 	{`{"X": "foo", "Y"}`, nil, nil, &SyntaxError{"invalid character '}' after object key", 17}},
+	{`[1, 2, 3+]`, nil, nil, &SyntaxError{"invalid character '+' after array element", 9}},
+
+	// array tests
+	{`[1, 2, 3]`, new([3]int), [3]int{1, 2, 3}, nil},
+	{`[1, 2, 3]`, new([1]int), [1]int{1}, nil},
+	{`[1, 2, 3]`, new([5]int), [5]int{1, 2, 3, 0, 0}, nil},
 
 	// composite tests
 	{allValueIndent, new(All), allValue, nil},

コアとなるコードの解説

src/pkg/encoding/json/decode.go

  1. func (d *decodeState) value(v reflect.Value) メソッド内の変更 (L228-233):

    • 変更前: if d.scan.redo { panic("redo") }
    • 変更後:
      if d.scan.redo {
          // rewind.
          d.scan.redo = false
          d.scan.step = stateBeginValue
      }
      
    • 解説: d.scan.redo は、JSONスキャン中に特定の状態を巻き戻す必要があることを示すフラグです。以前は、このフラグが true の場合に無条件にパニックを起こしていました。これはデバッグ目的か、未実装のロジックだった可能性があります。この修正により、パニックを回避し、d.scan.redofalse にリセットし、スキャン状態を stateBeginValue (値の開始状態) に戻すことで、スキャン処理が正常に続行されるようにしました。これにより、特定の不正なJSON入力が原因でデコーダがクラッシュする問題を修正したと考えられます。
  2. func (d *decodeState) array(v reflect.Value) メソッド内の変更 (L317-337):

    • 変更前:
      // Decoding into nil interface?  Switch to non-reflect code.
      iv := v
      ok := iv.Kind() == reflect.Interface
      if ok {
          iv.Set(reflect.ValueOf(d.arrayInterface()))
          return
      }
      
      // Check type of target.
      av := v
      if av.Kind() != reflect.Array && av.Kind() != reflect.Slice {
          d.saveError(&UnmarshalTypeError{"array", v.Type()})
          d.off--
          d.next()
          return
      }
      
      sv := v
      
    • 変更後:
      switch v.Kind() {
      default:
          d.saveError(&UnmarshalTypeError{"array", v.Type()})
          d.off--
          d.next()
          return
      case reflect.Interface:
          // Decoding into nil interface?  Switch to non-reflect code.
          v.Set(reflect.ValueOf(d.arrayInterface()))
          return
      case reflect.Array:
      case reflect.Slice:
          break
      }
      
    • 解説:
      • 不要な変数の削除: iv, ok, av, sv といった複数の reflect.Value 型のヘルパー変数が削除されました。これらの変数は、以前の reflect パッケージのAPI変更の残骸であり、冗長であったり、混乱を招いたりしていました。デコード対象の v reflect.Value を直接操作することで、コードが大幅に簡素化され、可読性が向上しました。
      • switch v.Kind() による型チェックの明確化: 以前は if 文で配列とスライスの型をチェックしていましたが、switch v.Kind() を使用することで、ターゲットの型が reflect.Interfacereflect.Arrayreflect.Slice のいずれであるかをより明確に判別できるようになりました。
        • reflect.Interface の場合、d.arrayInterface() を呼び出して、インターフェースへのデコードを処理します。これは、interface{} 型の変数にJSON配列をデコードする際に、適切なスライス型を動的に割り当てるためのものです。
        • reflect.Array または reflect.Slice の場合、break して後続の配列/スライスデコードロジックに進みます。
        • default ケースでは、ターゲットが配列でもスライスでもインターフェースでもない場合に UnmarshalTypeError を発生させ、適切なエラーハンドリングを行います。この変更により、型チェックのロジックがより堅牢になりました。
  3. スライス拡張ロジックの修正 (L349-369):

    • 変更前: if i >= av.Cap() && sv.IsValid() { ... }if i >= av.Len() && sv.IsValid() { ... } のような条件でスライスの容量や長さをチェックし、sv を使ってスライスを拡張していました。
    • 変更後:
      if v.Kind() == reflect.Slice {
          // Grow slice if necessary
          if i >= v.Cap() {
              newcap := v.Cap() + v.Cap()/2
              if newcap < 4 {
                  newcap = 4
              }
              newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
              reflect.Copy(newv, v)
              v.Set(newv)
          }
          if i >= v.Len() {
              v.SetLen(i + 1)
          }
      }
      // Decode into element.
      if i < v.Len() {
          d.value(v.Index(i))
      } else {
          // Ran out of fixed array: skip.
          d.value(reflect.Value{})
      }
      
    • 解説:
      • スライス拡張のロジックが if v.Kind() == reflect.Slice のブロック内に移動し、デコード対象の v を直接操作するように変更されました。これにより、sv 変数の使用が不要になり、コードがより直接的になりました。
      • i >= v.Cap() でスライスの容量が不足しているかをチェックし、不足していれば reflect.MakeSlice で新しいスライスを作成し、reflect.Copy で既存の要素をコピーして v.Set(newv)v を新しいスライスに更新します。
      • i >= v.Len() でスライスの長さが不足しているかをチェックし、不足していれば v.SetLen(i + 1) で長さを1つ増やします。
      • これらの変更により、JSON配列の要素数に応じてGoのスライスが正しく動的に拡張されるようになり、以前のバグが修正されました。また、要素のデコードも v.Index(i) を直接使うことで簡素化されています。
  4. 配列のゼロ埋めと空スライスの初期化 (L382-390):

    • 変更前: if i < av.Len() { if !sv.IsValid() { ... } else { sv.SetLen(i) } }if i == 0 && av.Kind() == reflect.Slice && sv.IsNil() { sv.Set(reflect.MakeSlice(sv.Type(), 0, 0)) }
    • 変更後:
      if i < v.Len() {
          if v.Kind() == reflect.Array {
              // Array.  Zero the rest.
              z := reflect.Zero(v.Type().Elem())
              for ; i < v.Len(); i++ {
                  v.Index(i).Set(z)
              }
          } else {
              v.SetLen(i)
          }
      }
      if i == 0 && v.Kind() == reflect.Slice {
          v.Set(reflect.MakeSlice(v.Type(), 0, 0))
      }
      
    • 解説:
      • JSON配列の要素数がGoの固定長配列の要素数より少なかった場合、残りの要素をゼロ値で埋めるロジックが if v.Kind() == reflect.Array のブロック内に移動し、v を直接操作するように変更されました。
      • 空のJSON配列がデコードされた場合に、Goのスライスが正しく空のスライスとして初期化されるロジックも簡素化されました。sv.IsNil() のチェックが不要になり、if i == 0 && v.Kind() == reflect.Slice の条件で v.Set(reflect.MakeSlice(v.Type(), 0, 0)) を呼び出すことで、空のスライスが正しく初期化されるようになりました。

src/pkg/encoding/json/decode_test.go

  • 追加されたテストケース:
    • {[1, 2, 3+], nil, nil, &SyntaxError{"invalid character '+' after array element", 9}}: 不正なJSON配列の構文エラーをテストします。
    • {[1, 2, 3], new([3]int), [3]int{1, 2, 3}, nil}: JSON配列がGoの固定長配列に正しくデコードされることをテストします。
    • {[1, 2, 3], new([1]int), [1]int{1}, nil}: JSON配列の要素がGoの固定長配列の容量を超える場合に、余分な要素が無視されることをテストします。
    • {[1, 2, 3], new([5]int), [5]int{1, 2, 3, 0, 0}, nil}: JSON配列の要素がGoの固定長配列の容量より少ない場合に、残りの要素がゼロ値で埋められることをテストします。

これらのテストケースの追加は、修正された配列デコードロジックが期待通りに機能し、特に配列とスライスのサイズ調整やエラーハンドリングが正しく行われることを検証するために重要です。

関連リンク

参考にした情報源リンク