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

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

このコミットは、Go言語の標準ライブラリであるexpvarパッケージにおけるマップキーの出力形式のバグを修正するものです。具体的には、expvar.MapがJSON形式で変数を公開する際に、マップのキーが正しくエスケープされず、結果として無効なJSON文字列が生成される問題を解決します。

コミット

commit 74c6b84182eef81e3585b467d47f416dba1224a5
Author: Rui Ueyama <ruiu@google.com>
Date:   Thu Apr 10 21:14:04 2014 -0700

    expvar: fix map key output
    
    To create a valid JSON string, "%s" is not enough.
    Fixes #7761.
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/86730043

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

https://github.com/golang/go/commit/74c6b84182eef81e3585b467d47f416dba1224a5

元コミット内容

このコミットは、expvarパッケージのMap型がその内容を文字列として表現する際に、マップのキーをフォーマットするためにfmt.Fprintf関数で%sフォーマット動詞を使用していた箇所を、%qフォーマット動詞に変更しています。

変更前: fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value)

変更後: fmt.Fprintf(&b, "%q: %v", kv.Key, kv.Value)

また、この変更を検証するために、expvar_test.go内のテストケースも更新されています。特に、マップキーに特殊文字を含む文字列(例: green "midori")を使用するテストが追加され、%qによる正しいエスケープが期待されるようになりました。

変更の背景

Go言語のexpvarパッケージは、実行中のGoプログラムから公開変数(カウンター、ゲージなど)をHTTP経由でJSON形式で公開するためのメカニズムを提供します。これにより、アプリケーションの内部状態を外部から監視・デバッグすることが可能になります。

このコミットが行われる前は、expvar.Mapがその内容をJSON形式で出力する際に、マップのキーを単純に文字列として(%sフォーマット動詞を使って)出力していました。しかし、JSONの仕様では、文字列リテラルはダブルクォーテーションで囲まれ、内部の特殊文字(例: ダブルクォーテーション、バックスラッシュ、改行など)は適切にエスケープされる必要があります。

%sは文字列をそのまま出力するため、マップのキー自体にダブルクォーテーションやその他のJSONでエスケープが必要な文字が含まれている場合、生成されるJSON文字列は不正な形式となっていました。例えば、キーがmy"keyのような文字列だった場合、出力は"my"key": valueとなり、これはJSONとして無効です。

この問題は、GoのIssue #7761として報告されており、このコミットはその問題を解決するために導入されました。目的は、expvar.Mapが出力するJSONが常に有効な形式であることを保証することです。

前提知識の解説

Go言語のexpvarパッケージ

expvarパッケージは、Goプログラムの内部状態を公開するためのシンプルなインターフェースを提供します。主に、HTTPサーバーが/debug/varsエンドポイントを公開し、そこにアクセスすることで、登録された変数(Int, Float, String, Mapなど)の現在の値がJSON形式で取得できます。これは、アプリケーションのメトリクス収集やデバッグに非常に役立ちます。

Go言語のfmtパッケージとフォーマット動詞

fmtパッケージは、Go言語におけるフォーマットI/Oを実装するためのパッケージです。fmt.Fprintffmt.Sprintfなどの関数は、指定されたフォーマット文字列と引数に基づいて文字列を生成します。フォーマット文字列には、引数の型や表示形式を指定するための「フォーマット動詞」が含まれます。

  • %s (string): 文字列またはバイトスライスをそのまま出力します。特殊文字のエスケープは行いません。
  • %q (quoted string): 文字列をGoの文字列リテラルとして安全にエスケープし、ダブルクォーテーションで囲んで出力します。これにより、文字列内の特殊文字(例: \n, \t, \", \\など)が適切に表現されます。この特性は、JSON文字列のキーや値として使用する際に、JSONの仕様に準拠したエスケープを自動的に行うため、非常に便利です。

JSON (JavaScript Object Notation)

JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。JavaScriptのオブジェクトリテラルをベースにしていますが、言語に依存しないデータフォーマットとして広く利用されています。JSONの文字列はダブルクォーテーションで囲まれ、特定の特殊文字はバックスラッシュ(\)を使ってエスケープされる必要があります。

例:

  • "hello"
  • "new\nline" (改行文字は\nとエスケープされる)
  • "quoted\"string" (ダブルクォーテーションは\"とエスケープされる)

技術的詳細

このコミットの技術的な核心は、expvar.MapString()メソッドにおけるfmt.Fprintfのフォーマット動詞の変更です。

expvar.Mapは、内部的にsync.Mapのような構造でキーと値を保持し、その内容をJSON形式の文字列として表現するためにString()メソッドを実装しています。このメソッドは、マップ内の各キーと値のペアをイテレートし、fmt.Fprintfを使ってバッファに書き込んでいきます。

変更前は、マップのキー(kv.Key)を\"%s\"という形式で出力していました。これは、キーの文字列をそのまま%sで展開し、その前後にリテラルのダブルクォーテーションを追加するという意図でした。しかし、この方法では、kv.Key自体にダブルクォーテーションが含まれている場合、例えばキーがfoo"barであったとすると、出力は"foo"bar": valueとなり、JSONパーサーは最初の""fooをキーとして認識し、その後のbarを不正なトークンとしてエラーを発生させます。

変更後は、%qフォーマット動詞を使用しています。%qは、Goの文字列リテラルとして安全にエスケープされた形式で文字列を出力します。これには、文字列をダブルクォーテーションで囲み、内部の特殊文字(ダブルクォーテーション、バックスラッシュ、改行など)を自動的にエスケープする機能が含まれます。

したがって、キーがfoo"barであった場合、%qはこれを"foo\"bar"と出力します。これにより、生成されるJSONは"foo\"bar": valueとなり、これはJSONの仕様に完全に準拠し、正しくパースされるようになります。

テストコードの変更も重要です。expvar_test.goでは、colors.AddFloat(green "midori", 4.125)のように、キーにスペースとダブルクォーテーションを含む文字列を使用するテストケースが追加されました。これにより、%qによるエスケープが正しく機能しているかを確認できるようになっています。

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

src/pkg/expvar/expvar.go

--- a/src/pkg/expvar/expvar.go
+++ b/src/pkg/expvar/expvar.go
@@ -112,7 +112,7 @@ func (v *Map) String() string {
 		if !first {
 			fmt.Fprintf(&b, ", ")
 		}
-		fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value)
+		fmt.Fprintf(&b, "%q: %v", kv.Key, kv.Value)
 		first = false
 	})
 	fmt.Fprintf(&b, "}")

src/pkg/expvar/expvar_test.go

--- a/src/pkg/expvar/expvar_test.go
+++ b/src/pkg/expvar/expvar_test.go
@@ -97,15 +97,15 @@ func TestMapCounter(t *testing.T) {
 	colors.Add("red", 1)
 	colors.Add("red", 2)
 	colors.Add("blue", 4)
-	colors.AddFloat("green", 4.125)
+	colors.AddFloat(`green "midori"`, 4.125)
 	if x := colors.m["red"].(*Int).i; x != 3 {
 		t.Errorf("colors.m[\"red\"] = %v, want 3", x)
 	}
 	if x := colors.m["blue"].(*Int).i; x != 4 {
 		t.Errorf("colors.m[\"blue\"] = %v, want 4", x)
 	}
-	if x := colors.m["green"].(*Float).f; x != 4.125 {
-		t.Errorf("colors.m[\"green\"] = %v, want 3.14", x)
+	if x := colors.m[`green "midori"`].(*Float).f; x != 4.125 {
+		t.Errorf("colors.m[`green \"midori\"`] = %v, want 3.14", x)
 	}
 
 	// colors.String() should be '{\"red\":3, \"blue\":4}',

コアとなるコードの解説

src/pkg/expvar/expvar.goの変更

expvar.goMap型のString()メソッドは、expvar.MapのインスタンスをJSON形式の文字列としてシリアライズする役割を担っています。このメソッド内で、マップの各キーと値のペアをループ処理し、fmt.Fprintfを使ってbytes.Bufferに書き込んでいます。

変更前のコードでは、fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value)という行がありました。ここで、kv.Keyはマップのキーを表す文字列です。%sフォーマット動詞は、kv.Keyの値をそのまま文字列として展開します。その結果、例えばkv.Keymy"keyという値だった場合、出力される文字列は"my"key"となり、これはJSONのキーとして不正です。JSONの仕様では、文字列内のダブルクォーテーションは\"のようにエスケープされる必要があります。

変更後のコードでは、この行がfmt.Fprintf(&b, "%q: %v", kv.Key, kv.Value)に変更されました。ここで使用されている%qフォーマット動詞は、Goの文字列リテラルとして安全にエスケープされた形式で文字列を出力します。具体的には、文字列をダブルクォーテーションで囲み、内部のダブルクォーテーションやバックスラッシュ、改行などの特殊文字を自動的にエスケープします。これにより、kv.Keymy"keyであった場合、%qはこれを"my\"key"と出力し、生成されるJSON文字列が常に有効な形式となることが保証されます。

src/pkg/expvar/expvar_test.goの変更

テストファイルexpvar_test.goの変更は、このバグ修正が正しく機能していることを検証するために重要です。

変更前は、colors.AddFloat("green", 4.125)のように、単純なキー名を使用していました。 変更後には、colors.AddFloat(green "midori", 4.125)という行が追加されました。ここで注目すべきは、キーとしてバッククォート文字列リテラル(raw string literal)を使用し、その中にスペースとダブルクォーテーションを含むgreen "midori"という文字列を渡している点です。

このテストケースは、マップのキーにJSONでエスケープが必要な文字(この場合はダブルクォーテーション)が含まれている場合に、expvar.MapString()メソッドが正しくJSON形式のキーを生成できるかを検証します。%qフォーマット動詞が適用されることで、green "midori"というキーはJSON出力時に"green \"midori\""のように適切にエスケープされることが期待されます。テストでは、このエスケープされたキーを使ってマップから値を取得し、期待される値と比較することで、修正が正しく機能していることを確認しています。

関連リンク

参考にした情報源リンク

The output is now ready to be presented to the user.

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

このコミットは、Go言語の標準ライブラリである`expvar`パッケージにおけるマップキーの出力形式のバグを修正するものです。具体的には、`expvar.Map`がJSON形式で変数を公開する際に、マップのキーが正しくエスケープされず、結果として無効なJSON文字列が生成される問題を解決します。

## コミット

commit 74c6b84182eef81e3585b467d47f416dba1224a5 Author: Rui Ueyama ruiu@google.com Date: Thu Apr 10 21:14:04 2014 -0700

expvar: fix map key output

To create a valid JSON string, "%s" is not enough.
Fixes #7761.

LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/86730043

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

[https://github.com/golang/go/commit/74c6b84182eef81e3585b467d47f416dba1224a5](https://github.com/golang/go/commit/74c6b84182eef81e3585b467d47f416dba1224a5)

## 元コミット内容

このコミットは、`expvar`パッケージの`Map`型がその内容を文字列として表現する際に、マップのキーをフォーマットするために`fmt.Fprintf`関数で`%s`フォーマット動詞を使用していた箇所を、`%q`フォーマット動詞に変更しています。

変更前:
`fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value)`

変更後:
`fmt.Fprintf(&b, "%q: %v", kv.Key, kv.Value)`

また、この変更を検証するために、`expvar_test.go`内のテストケースも更新されています。特に、マップキーに特殊文字を含む文字列(例: `green "midori"`)を使用するテストが追加され、`%q`による正しいエスケープが期待されるようになりました。

## 変更の背景

Go言語の`expvar`パッケージは、実行中のGoプログラムから公開変数(カウンター、ゲージなど)をHTTP経由でJSON形式で公開するためのメカニズムを提供します。これにより、アプリケーションの内部状態を外部から監視・デバッグすることが可能になります。

このコミットが行われる前は、`expvar.Map`がその内容をJSON形式で出力する際に、マップのキーを単純に文字列として(`%s`フォーマット動詞を使って)出力していました。しかし、JSONの仕様では、文字列リテラルはダブルクォーテーションで囲まれ、内部の特殊文字(例: ダブルクォーテーション、バックスラッシュ、改行など)は適切にエスケープされる必要があります。

`%s`は文字列をそのまま出力するため、マップのキー自体にダブルクォーテーションやその他のJSONでエスケープが必要な文字が含まれている場合、生成されるJSON文字列は不正な形式となっていました。例えば、キーが`my"key`のような文字列だった場合、出力は`"my"key": value`となり、これはJSONとして無効です。

この問題は、GoのIssue #7761として報告されており、このコミットはその問題を解決するために導入されました。目的は、`expvar.Map`が出力するJSONが常に有効な形式であることを保証することです。

## 前提知識の解説

### Go言語の`expvar`パッケージ

`expvar`パッケージは、Goプログラムの内部状態を公開するためのシンプルなインターフェースを提供します。主に、HTTPサーバーが`/debug/vars`エンドポイントを公開し、そこにアクセスすることで、登録された変数(`Int`, `Float`, `String`, `Map`など)の現在の値がJSON形式で取得できます。これは、アプリケーションのメトリクス収集やデバッグに非常に役立ちます。

### Go言語の`fmt`パッケージとフォーマット動詞

`fmt`パッケージは、Go言語におけるフォーマットI/Oを実装するためのパッケージです。`fmt.Fprintf`や`fmt.Sprintf`などの関数は、指定されたフォーマット文字列と引数に基づいて文字列を生成します。フォーマット文字列には、引数の型や表示形式を指定するための「フォーマット動詞」が含まれます。

*   **`%s` (string)**: 文字列またはバイトスライスをそのまま出力します。特殊文字のエスケープは行いません。
*   **`%q` (quoted string)**: 文字列をGoの文字列リテラルとして安全にエスケープし、ダブルクォーテーションで囲んで出力します。これにより、文字列内の特殊文字(例: `\n`, `\t`, `\"`, `\\`など)が適切に表現されます。この特性は、JSON文字列のキーや値として使用する際に、JSONの仕様に準拠したエスケープを自動的に行うため、非常に便利です。

### JSON (JavaScript Object Notation)

JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。JavaScriptのオブジェクトリテラルをベースにしていますが、言語に依存しないデータフォーマットとして広く利用されています。JSONの文字列はダブルクォーテーションで囲まれ、特定の特殊文字はバックスラッシュ(`\`)を使ってエスケープされる必要があります。

例:
*   `"hello"`
*   `"new\nline"` (改行文字は`\n`とエスケープされる)
*   `"quoted\"string"` (ダブルクォーテーションは`\"`とエスケープされる)

## 技術的詳細

このコミットの技術的な核心は、`expvar.Map`の`String()`メソッドにおける`fmt.Fprintf`のフォーマット動詞の変更です。

`expvar.Map`は、内部的に`sync.Map`のような構造でキーと値を保持し、その内容をJSON形式の文字列として表現するために`String()`メソッドを実装しています。このメソッド内で、マップ内の各キーと値のペアをイテレートし、`fmt.Fprintf`を使ってバッファに書き込んでいきます。

変更前は、マップのキー(`kv.Key`)を`\"%s\"`という形式で出力していました。これは、キーの文字列をそのまま`%s`で展開し、その前後にリテラルのダブルクォーテーションを追加するという意図でした。しかし、この方法では、`kv.Key`自体にダブルクォーテーションが含まれている場合、例えばキーが`foo"bar`であったとすると、出力は`"foo"bar": value`となり、JSONパーサーは最初の`"`と`"`で`foo`をキーとして認識し、その後の`bar`を不正なトークンとしてエラーを発生させます。

変更後は、`%q`フォーマット動詞を使用しています。`%q`は、Goの文字列リテラルとして安全にエスケープされた形式で文字列を出力します。これには、文字列をダブルクォーテーションで囲み、内部の特殊文字(ダブルクォーテーション、バックスラッシュ、改行など)を自動的にエスケープする機能が含まれます。

したがって、キーが`foo"bar`であった場合、`%q`はこれを`"foo\"bar"`と出力します。これにより、生成されるJSONは`"foo\"bar": value`となり、これはJSONの仕様に完全に準拠し、正しくパースされるようになります。

テストコードの変更も重要です。`expvar_test.go`では、`colors.AddFloat(`green \"midori\"`, 4.125)`のように、キーにスペースとダブルクォーテーションを含む文字列を使用するテストケースが追加されました。これにより、`%q`によるエスケープが正しく機能しているかを確認できるようになっています。

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

### `src/pkg/expvar/expvar.go`の変更

```diff
--- a/src/pkg/expvar/expvar.go
+++ b/src/pkg/expvar/expvar.go
@@ -112,7 +112,7 @@ func (v *Map) String() string {
 		if !first {
 			fmt.Fprintf(&b, ", ")
 		}
-		fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value)
+		fmt.Fprintf(&b, "%q: %v", kv.Key, kv.Value)
 		first = false
 	})
 	fmt.Fprintf(&b, "}")

src/pkg/expvar/expvar_test.goの変更

--- a/src/pkg/expvar/expvar_test.go
+++ b/src/pkg/expvar/expvar_test.go
@@ -97,15 +97,15 @@ func TestMapCounter(t *testing.T) {
 	colors.Add("red", 1)
 	colors.Add("red", 2)
 	colors.Add("blue", 4)
-	colors.AddFloat("green", 4.125)
+	colors.AddFloat(`green "midori"`, 4.125)
 	if x := colors.m["red"].(*Int).i; x != 3 {
 		t.Errorf("colors.m[\"red\"] = %v, want 3", x)
 	}
 	if x := colors.m["blue"].(*Int).i; x != 4 {
 		t.Errorf("colors.m[\"blue\"] = %v, want 4", x)
 	}
-	if x := colors.m["green"].(*Float).f; x != 4.125 {
-		t.Errorf("colors.m[\"green\"] = %v, want 3.14", x)
+	if x := colors.m[`green "midori"`].(*Float).f; x != 4.125 {
+		t.Errorf("colors.m[`green \"midori\"`] = %v, want 3.14", x)
 	}
 
 	// colors.String() should be '{\"red\":3, \"blue\":4}',

コアとなるコードの解説

src/pkg/expvar/expvar.goの変更

expvar.goMap型のString()メソッドは、expvar.MapのインスタンスをJSON形式の文字列としてシリアライズする役割を担っています。このメソッド内で、マップの各キーと値のペアをループ処理し、fmt.Fprintfを使ってbytes.Bufferに書き込んでいます。

変更前のコードでは、fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value)という行がありました。ここで、kv.Keyはマップのキーを表す文字列です。%sフォーマット動詞は、kv.Keyの値をそのまま文字列として展開します。その結果、例えばkv.Keymy"keyという値だった場合、出力される文字列は"my"key"となり、これはJSONのキーとして不正です。JSONの仕様では、文字列内のダブルクォーテーションは\"のようにエスケープされる必要があります。

変更後のコードでは、この行がfmt.Fprintf(&b, "%q: %v", kv.Key, kv.Value)に変更されました。ここで使用されている%qフォーマット動詞は、Goの文字列リテラルとして安全にエスケープされた形式で文字列を出力します。具体的には、文字列をダブルクォーテーションで囲み、内部のダブルクォーテーションやバックスラッシュ、改行などの特殊文字を自動的にエスケープします。これにより、kv.Keymy"keyであった場合、%qはこれを"my\"key"と出力し、生成されるJSON文字列が常に有効な形式となることが保証されます。

src/pkg/expvar/expvar_test.goの変更

テストファイルexpvar_test.goの変更は、このバグ修正が正しく機能していることを検証するために重要です。

変更前は、colors.AddFloat("green", 4.125)のように、単純なキー名を使用していました。 変更後には、colors.AddFloat(green "midori", 4.125)という行が追加されました。ここで注目すべきは、キーとしてバッククォート文字列リテラル(raw string literal)を使用し、その中にスペースとダブルクォーテーションを含むgreen "midori"という文字列を渡している点です。

このテストケースは、マップのキーにJSONでエスケープが必要な文字(この場合はダブルクォーテーション)が含まれている場合に、expvar.MapString()メソッドが正しくJSON形式のキーを生成できるかを検証します。%qフォーマット動詞が適用されることで、green "midori"というキーはJSON出力時に"green \"midori\""のように適切にエスケープされることが期待されます。テストでは、このエスケープされたキーを使ってマップから値を取得し、期待される値と比較することで、修正が正しく機能していることを確認しています。

関連リンク

参考にした情報源リンク