[インデックス 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.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
--- 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.go
のMap
型のString()
メソッドは、expvar.Map
のインスタンスをJSON形式の文字列としてシリアライズする役割を担っています。このメソッド内で、マップの各キーと値のペアをループ処理し、fmt.Fprintf
を使ってbytes.Buffer
に書き込んでいます。
変更前のコードでは、fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value)
という行がありました。ここで、kv.Key
はマップのキーを表す文字列です。%s
フォーマット動詞は、kv.Key
の値をそのまま文字列として展開します。その結果、例えばkv.Key
がmy"key
という値だった場合、出力される文字列は"my"key"
となり、これはJSONのキーとして不正です。JSONの仕様では、文字列内のダブルクォーテーションは\"
のようにエスケープされる必要があります。
変更後のコードでは、この行がfmt.Fprintf(&b, "%q: %v", kv.Key, kv.Value)
に変更されました。ここで使用されている%q
フォーマット動詞は、Goの文字列リテラルとして安全にエスケープされた形式で文字列を出力します。具体的には、文字列をダブルクォーテーションで囲み、内部のダブルクォーテーションやバックスラッシュ、改行などの特殊文字を自動的にエスケープします。これにより、kv.Key
がmy"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.Map
のString()
メソッドが正しくJSON形式のキーを生成できるかを検証します。%q
フォーマット動詞が適用されることで、green "midori"
というキーはJSON出力時に"green \"midori\""
のように適切にエスケープされることが期待されます。テストでは、このエスケープされたキーを使ってマップから値を取得し、期待される値と比較することで、修正が正しく機能していることを確認しています。
関連リンク
- Go
expvar
package documentation: https://pkg.go.dev/expvar - Go
fmt
package documentation: https://pkg.go.dev/fmt - JSON (JavaScript Object Notation) Official Website: https://www.json.org/json-en.html
参考にした情報源リンク
- Go
expvar
package documentation: https://pkg.go.dev/expvar - Go
fmt
package documentation: https://pkg.go.dev/fmt - Stack Overflow: What is the difference between %s and %q in Go's fmt package?: https://stackoverflow.com/questions/20010664/what-is-the-difference-between-s-and-q-in-gos-fmt-package
- JSON (JavaScript Object Notation) Official Website: https://www.json.org/json-en.html
- Go issue tracker (general search for context, though specific issue 7761 was not found publicly): https://github.com/golang/go/issuesI have generated the detailed technical explanation in Markdown format, following all the specified sections and details. I have also included the GitHub URL and the original commit content. I have explained the background, prerequisite knowledge, technical details, core code changes, and related links. I have also addressed the issue #7761 by explaining that it was likely an internal issue or one no longer publicly accessible, and focused on the technical problem it aimed to solve.
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.go
のMap
型のString()
メソッドは、expvar.Map
のインスタンスをJSON形式の文字列としてシリアライズする役割を担っています。このメソッド内で、マップの各キーと値のペアをループ処理し、fmt.Fprintf
を使ってbytes.Buffer
に書き込んでいます。
変更前のコードでは、fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value)
という行がありました。ここで、kv.Key
はマップのキーを表す文字列です。%s
フォーマット動詞は、kv.Key
の値をそのまま文字列として展開します。その結果、例えばkv.Key
がmy"key
という値だった場合、出力される文字列は"my"key"
となり、これはJSONのキーとして不正です。JSONの仕様では、文字列内のダブルクォーテーションは\"
のようにエスケープされる必要があります。
変更後のコードでは、この行がfmt.Fprintf(&b, "%q: %v", kv.Key, kv.Value)
に変更されました。ここで使用されている%q
フォーマット動詞は、Goの文字列リテラルとして安全にエスケープされた形式で文字列を出力します。具体的には、文字列をダブルクォーテーションで囲み、内部のダブルクォーテーションやバックスラッシュ、改行などの特殊文字を自動的にエスケープします。これにより、kv.Key
がmy"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.Map
のString()
メソッドが正しくJSON形式のキーを生成できるかを検証します。%q
フォーマット動詞が適用されることで、green "midori"
というキーはJSON出力時に"green \"midori\""
のように適切にエスケープされることが期待されます。テストでは、このエスケープされたキーを使ってマップから値を取得し、期待される値と比較することで、修正が正しく機能していることを確認しています。
関連リンク
- Go
expvar
package documentation: https://pkg.go.dev/expvar - Go
fmt
package documentation: https://pkg.go.dev/fmt - JSON (JavaScript Object Notation) Official Website: https://www.json.org/json-en.html
参考にした情報源リンク
- Go
expvar
package documentation: https://pkg.go.dev/expvar - Go
fmt
package documentation: https://pkg.go.dev/fmt - Stack Overflow: What is the difference between %s and %q in Go's fmt package?: https://stackoverflow.com/questions/20010664/what-is-the-difference-between-s-and-q-in-gos-fmt-package
- JSON (JavaScript Object Notation) Official Website: https://www.json.org/json-en.html
- Go issue tracker (general search for context, though specific issue 7761 was not found publicly): https://github.com/golang/go/issues