[インデックス 11696] ファイルの概要
このコミットは、Go言語の標準ライブラリにおけるtime
パッケージとencoding/xml
パッケージの改善に焦点を当てています。具体的には、time.Time
型のJSONマーシャリング処理のクリーンアップ、RFC3339Nano
という新しい時間フォーマット定数の追加、そしてencoding/xml
パッケージがtime.Time
型を特別に扱うように変更された点が挙げられます。これにより、XMLエンコーディングにおいてtime.Time
型が適切に処理されるようになり、Go 1リリース前の暫定的な対応として、基本的な型であるtime.Time
のXMLシリアライズ・デシリアライズが改善されました。
コミット
- コミットハッシュ:
1d8250c8b0987bd67e5253803e50bbd7997a3d23
- Author: Russ Cox rsc@golang.org
- Date: Tue Feb 7 23:37:25 2012 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1d8250c8b0987bd67e5253803e50bbd7997a3d23
元コミット内容
time: clean up MarshalJSON, add RFC3339 method
encoding/xml: handle time.Time as recognized type
The long term plan is to define an interface that time.Time
can implement and that encoding/xml can call, but we are
not going to try to define that interface before Go 1.
Instead, special-case time.Time in package xml, because
it is such a fundamental type, as a stop-gap.
The eventual methods will behave this way.
Fixes #2793.
R=golang-dev, r, r, n13m3y3r
CC=golang-dev
https://golang.org/cl/5634051
変更の背景
この変更の背景には、Go言語のtime.Time
型がJSONおよびXMLのエンコーディング/デコーディングにおいて、より標準的かつ柔軟に扱われるようにするという目的があります。
-
time.Time
のJSONマーシャリングの改善: 以前のtime.Time.MarshalJSON
の実装は、年が4桁に満たない場合にゼロパディングを手動で行うなど、やや複雑でした。また、ナノ秒以下の精度を持つタイムスタンプの末尾のゼロをトリムするロジックも手動で実装されていました。このコミットでは、これらの処理をtime.Format
メソッドと新しいRFC3339Nano
フォーマット定数に委ねることで、コードの簡潔性と堅牢性を向上させています。これにより、JSON出力がRFC3339に準拠しつつ、必要に応じてナノ秒精度を保持し、かつ不要なゼロを省略できるようになります。 -
encoding/xml
でのtime.Time
の特別扱い: Go 1のリリースを控える中で、encoding/xml
パッケージがtime.Time
型を適切に処理するための汎用的なインターフェース(例:Marshaler
やUnmarshaler
のようなもの)を定義する時間的余裕がないという判断がありました。しかし、time.Time
は非常に基本的な型であるため、XMLエンコーディングで適切に扱われることが強く求められていました。 そこで、長期的な解決策(汎用インターフェースの導入)を待つのではなく、一時的な措置(stop-gap)として、encoding/xml
パッケージ内でtime.Time
型を特別に認識し、処理するように変更されました。これにより、time.Time
がXML要素や属性としてマーシャリング・アンマーシャリングされる際に、RFC3339形式(ナノ秒精度を含む)で自動的に変換されるようになります。コミットメッセージにある「The eventual methods will behave this way.」という記述は、将来的に導入されるであろう汎用インターフェースも、このコミットで実装されたtime.Time
のXML処理と同様の振る舞いをすることを意図していることを示唆しています。 -
Issue #2793の修正: このコミットは、GoのIssueトラッカーで報告されていたIssue 2793を修正するものです。このIssueは、
encoding/xml
がtime.Time
型を適切に扱えないという問題提起でした。
これらの変更は、Go言語の標準ライブラリが提供するデータ型とエンコーディングメカニズムの間の整合性を高め、開発者が日付と時刻のデータをJSONやXMLでより簡単に、かつ標準に準拠した形で扱えるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と関連する標準について理解しておく必要があります。
-
time.Time
型: Go言語の標準ライブラリtime
パッケージで提供される、特定の時点を表す構造体です。日付、時刻、タイムゾーン情報を含みます。Goにおける時間操作の基本となります。 -
encoding/json
パッケージ: Goのデータ構造とJSONデータの間でエンコード(マーシャリング)およびデコード(アンマーシャリング)を行うためのパッケージです。json.Marshaler
およびjson.Unmarshaler
インターフェースを実装することで、カスタムのJSON変換ロジックを定義できます。time.Time
型はデフォルトでこのインターフェースを実装しており、JSONに変換される際に特定のフォーマット(通常はRFC3339)で文字列化されます。 -
encoding/xml
パッケージ: Goのデータ構造とXMLデータの間でエンコード(マーシャリング)およびデコード(アンマーシャリング)を行うためのパッケージです。xml.Marshaler
、xml.Unmarshaler
、xml.MarshalerAttr
、xml.UnmarshalerAttr
などのインターフェースを実装することで、カスタムのXML変換ロジックを定義できます。このコミット以前は、time.Time
型はencoding/xml
によって特別に認識されていませんでした。 -
json.Marshaler
インターフェース:MarshalJSON() ([]byte, error)
メソッドを持つインターフェースです。このインターフェースを実装する型は、json.Marshal
関数によってJSONに変換される際に、このメソッドが呼び出され、その戻り値がJSONデータとして使用されます。 -
RFC3339: "Date and Time on the Internet: Timestamps"というIETF標準(RFC 3339)で定義された、日付と時刻の文字列表現フォーマットです。ISO 8601のプロファイルであり、インターネットプロトコルやWeb APIで広く利用されます。 一般的な形式は
YYYY-MM-DDTHH:MM:SSZ
またはYYYY-MM-DDTHH:MM:SS±HH:MM
です。末尾のZ
はUTC(協定世界時)を示し、±HH:MM
はUTCからのオフセットを示します。秒の小数点以下(フラクショナル秒)も許容されますが、その精度はRFC3339自体では厳密に定義されていません。 -
RFC3339Nano: これはRFC3339の公式な拡張ではありませんが、Go言語の
time
パッケージで導入された、RFC3339形式にナノ秒精度(最大9桁)のフラクショナル秒を含めるためのフォーマット定数です。例えば、2006-01-02T15:04:05.999999999Z07:00
のように表現されます。より高精度な時間表現が必要な場合に利用されます。 -
reflect
パッケージ: Goのランタイムリフレクション機能を提供するパッケージです。プログラムの実行中に、変数や型の情報を動的に検査・操作することができます。このコミットでは、reflect.TypeOf(time.Time{})
を使用してtime.Time
型のreflect.Type
を取得し、XMLエンコーディング時に型がtime.Time
であるかどうかを動的にチェックするために使用されています。 -
Goのインターフェース: Goにおけるインターフェースは、メソッドのシグネチャの集合を定義する型です。特定のインターフェースのすべてのメソッドを実装する型は、そのインターフェースを満たすと見なされます。
encoding/xml
やencoding/json
のようなパッケージは、これらのインターフェースを利用して、カスタムのエンコーディング/デコーディングロジックを型に提供させることができます。このコミットでは、encoding/xml
がtime.Time
を特別扱いする「stop-gap」として、インターフェースを介した汎用的な解決策の代わりに、リフレクションを用いた直接的な型チェックを行っています。
技術的詳細
このコミットで行われた技術的な変更は、主に以下の3つの領域に分けられます。
-
time
パッケージの変更:MarshalJSON
の簡素化:time.Time
型のMarshalJSON
メソッドが大幅に簡素化されました。以前は、年のゼロパディングやナノ秒の末尾ゼロトリムを手動で行っていましたが、新しい実装ではt.Format("
"+ RFC3339Nano +
"")
という一行で処理されるようになりました。これは、後述するRFC3339Nano
定数とFormat
メソッドの改善によって可能になりました。RFC3339Nano
定数の追加:time
パッケージに新しいフォーマット定数RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
が追加されました。これは、RFC3339形式にナノ秒精度(最大9桁)のフラクショナル秒を含めるためのものです。Format
メソッドの改善:time.Format
メソッドが、フラクショナル秒のフォーマットにおいて、.000
(指定された桁数でゼロパディング)と.999
(指定された桁数で、末尾のゼロをトリム)の両方をサポートするように拡張されました。これにより、MarshalJSON
がRFC3339Nano
を使用する際に、不要な末尾ゼロが自動的に削除されるようになります。また、年のフォーマット(stdLongYear
)も、負の年や4桁未満の年に対して適切にゼロパディングされるように改善されました。UnmarshalJSON
の修正:UnmarshalJSON
メソッドも、Parse
関数に渡すフォーマット文字列がバッククォート文字列リテラル(raw string literal)を使用するように変更され、エスケープが不要になりました。
-
encoding/xml
パッケージの変更:time.Time
の特別扱い:encoding/xml
パッケージのマーシャリング(marshal.go
)とアンマーシャリング(read.go
)ロジックに、time.Time
型を特別に処理するコードが追加されました。- マーシャリング:
marshalSimple
およびmarshalStruct
関数内で、reflect.TypeOf(time.Time{})
を使用して値の型がtime.Time
であるかをチェックし、もしそうであればval.Interface().(time.Time).Format(time.RFC3339Nano)
を使ってRFC3339Nano形式で文字列化するように変更されました。これにより、time.Time
型のフィールドがXML要素や属性として出力される際に、適切なフォーマットで表現されます。 - アンマーシャリング:
unmarshal
関数内で、XMLから読み取ったデータがtime.Time
型のフィールドに割り当てられる場合、time.Parse(time.RFC3339, string(src))
を使ってRFC3339形式でパースするように変更されました。これにより、XMLから読み取られた日付/時刻文字列がtime.Time
オブジェクトに正しく変換されます。
- マーシャリング:
- テストケースの追加/修正:
encoding/xml
のテストファイル(atom_test.go
,marshal_test.go
,read_test.go
)に、time.Time
型のXMLマーシャリング/アンマーシャリングに関するテストケースが追加または修正されました。特にread_test.go
では、Feed
構造体のUpdated
フィールドの型がカスタムのTime string
型から標準のtime.Time
型に変更され、XML属性としての時間もサポートするように修正されています。
-
リフレクションの使用:
encoding/xml
パッケージでは、time.Time
型を識別するためにreflect.TypeOf(time.Time{})
が使用されています。これは、Go 1リリース前に汎用的なインターフェースを導入する代わりに、特定の型を直接チェックするという「stop-gap」戦略の一環です。timeType
という変数にreflect.TypeOf(time.Time{})
の結果をキャッシュすることで、型チェックのパフォーマンスを最適化しています。
これらの変更により、Go言語のtime.Time
型は、JSONとXMLの両方で、より一貫性のある、標準に準拠した、そして高精度な方法で扱えるようになりました。
コアとなるコードの変更箇所
src/pkg/time/time.go
(MarshalJSONの変更)
// MarshalJSON implements the json.Marshaler interface.
// Time is formatted as RFC3339.
func (t Time) MarshalJSON() ([]byte, error) {
if y := t.Year(); y < 0 || y >= 10000 {
return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
}
return []byte(t.Format(`"` + RFC3339Nano + `"`)), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
// Time is expected in RFC3339 format.
func (t *Time) UnmarshalJSON(data []byte) (err error) {
// Fractional seconds are handled implicitly by Parse.
*t, err = Parse(`"`+RFC3339+`"`, string(data))
return
}
src/pkg/time/format.go
(RFC3339Nanoの追加とFormatメソッドの改善)
const (
ANSIC = "Mon Jan _2 15:04:05 2006"
UnixDate = "Mon Jan _2 15:04:05 MST 2006"
RubyDate = "Mon Jan 02 15:04:05 -0700 2006"
RFC822 = "02 Jan 06 1504 MST"
RFC822Z = "02 Jan 06 1504 -0700" // RFC822 with numeric zone
RFC850 = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST"
RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
RFC3339 = "2006-01-02T15:04:05Z07:00"
RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" // <-- NEW
Kitchen = "3:04PM"
// Handy time stamps.
Stamp = "Jan _2 15:04:05"
StampMilli = "Jan _2 15:04:05.000"
StampMicro = "Jan _2 15:04:05.000000"
StampNano = "Jan _2 15:04:05.000000000"
)
// ... (nextStdChunk function modified to handle .0 and .9 for fractional seconds) ...
// formatNano formats a fractional second, as nanoseconds.
func formatNano(nanosec, n int, trim bool) string { // <-- 'trim' parameter added
// ...
if trim { // <-- New logic for trimming trailing zeros
for n > 0 && s[n-1] == '0' {
n--
}
if n == 0 {
return ""
}
}
return "." + s[:n]
}
// ... (Format method modified to use formatNano with 'trim' parameter) ...
src/pkg/encoding/xml/marshal.go
(time.Timeのマーシャリング対応)
import (
// ...
"time" // <-- NEW
)
// ...
var timeType = reflect.TypeOf(time.Time{}) // <-- NEW
func (p *printer) marshalSimple(typ reflect.Type, val reflect.Value) error {
// Normally we don't see structs, but this can happen for an attribute.
if val.Type() == timeType { // <-- NEW: Check if type is time.Time
p.WriteString(val.Interface().(time.Time).Format(time.RFC3339Nano)) // <-- NEW: Format as RFC3339Nano
return nil
}
// ...
}
func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {
if val.Type() == timeType { // <-- NEW: Check if type is time.Time
p.WriteString(val.Interface().(time.Time).Format(time.RFC3339Nano)) // <-- NEW: Format as RFC3339Nano
return nil
}
// ...
}
src/pkg/encoding/xml/read.go
(time.Timeのアンマーシャリング対応)
import (
// ...
"time" // <-- NEW
)
// ...
func (p *Decoder) unmarshal(val reflect.Value, start *StartElement) error {
// ...
if typ == timeType { // <-- NEW: Check if type is time.Time
saveData = v
break
}
// ...
}
func copyValue(dst reflect.Value, src []byte) (err error) {
// ...
case reflect.Struct: // <-- NEW: Handle struct types
if t.Type() == timeType { // <-- NEW: Check if type is time.Time
tv, err := time.Parse(time.RFC3339, string(src)) // <-- NEW: Parse as RFC3339
if err != nil {
return err
}
t.Set(reflect.ValueOf(tv))
}
}
return nil
}
コアとなるコードの解説
time.Time.MarshalJSON
の変更
以前のMarshalJSON
は、年のパディングやナノ秒のトリミングを手動で行う複雑なロジックを含んでいました。このコミットでは、そのロジックがtime.Format
メソッドと新しいRFC3339Nano
定数に集約されました。
if y := t.Year(); y < 0 || y >= 10000 { ... }
: 年がRFC3339で許容される0〜9999の範囲外である場合にエラーを返すチェックは残されています。return []byte(t.Format(
"+ RFC3339Nano +
")), nil
: この一行が変更の核心です。RFC3339Nano
は、ナノ秒精度を含むRFC3339形式のレイアウト文字列です。time.Format
メソッドは、このレイアウト文字列に従ってtime.Time
値を文字列に変換します。format.go
の変更により、RFC3339Nano
に含まれる.999999999
のようなフラクショナル秒の指定は、末尾のゼロを自動的にトリムするようになりました。例えば、12:34:56.789000000
は12:34:56.789
とフォーマットされます。- 全体をバッククォート文字列リテラルで囲むことで、JSONの文字列リテラルとして適切に引用符で囲まれた形式(例:
"2006-01-02T15:04:05.123Z"
)が生成されます。
この変更により、MarshalJSON
のコードは非常に簡潔になり、time.Format
の柔軟性と正確性を最大限に活用しています。
time.Format
メソッドとRFC3339Nano
の追加
RFC3339Nano
定数:time
パッケージにRFC3339Nano
が追加されたことで、開発者はナノ秒精度を必要とするRFC3339形式の文字列を簡単に生成できるようになりました。formatNano
関数のtrim
パラメータ:formatNano
関数にtrim
というブール値のパラメータが追加されました。trim
がtrue
の場合(RFC3339Nano
のように.999...
形式のレイアウトが指定された場合)、生成されるフラクショナル秒の文字列から末尾のゼロが削除されます。これにより、12:34:56.789000000Z
のような冗長な出力ではなく、12:34:56.789Z
のような簡潔な出力が得られます。trim
がfalse
の場合(.000...
形式のレイアウトが指定された場合)、指定された桁数でゼロパディングが行われます。
nextStdChunk
の改善:time.Format
がレイアウト文字列を解析する際に、.0
(ゼロパディング)と.9
(末尾ゼロトリム)の両方のフラクショナル秒の指定を適切に識別できるようになりました。- 年のフォーマットの改善:
stdLongYear
(2006
)のフォーマットロジックが強化され、負の年や4桁未満の年(例:0001
年)も適切にゼロパディングされるようになりました。
これらの変更は、time.Format
の柔軟性を高め、JSONやXMLなどの外部システムとの連携において、より正確で標準に準拠した時間表現を可能にします。
encoding/xml
におけるtime.Time
の特別扱い
encoding/xml
パッケージは、Go 1リリース前に汎用的なインターフェースを導入する代わりに、time.Time
型を直接認識して処理するように変更されました。
var timeType = reflect.TypeOf(time.Time{})
:time.Time
型のreflect.Type
オブジェクトを一度だけ取得し、timeType
変数にキャッシュしています。これにより、型チェックのたびにリフレクションのオーバーヘッドが発生するのを防ぎます。- マーシャリング (
marshal.go
):marshalSimple
とmarshalStruct
の両方で、val.Type() == timeType
という条件で、現在処理している値の型がtime.Time
であるかをチェックしています。- もし
time.Time
型であれば、val.Interface().(time.Time).Format(time.RFC3339Nano)
を使って、その値をRFC3339Nano
形式の文字列に変換し、XML出力に書き込んでいます。これにより、time.Time
型のフィールドがXML要素のテキストコンテンツやXML属性の値として適切にシリアライズされます。
- アンマーシャリング (
read.go
):unmarshal
関数内で、XMLから読み取ったデータがtime.Time
型のフィールドに割り当てられるべき場合(typ == timeType
)、そのデータを一時的に保存するロジックが追加されました。copyValue
関数(XMLのテキストコンテンツをGoのフィールドにコピーする役割)のreflect.Struct
ケースに、time.Time
型を特別に処理するロジックが追加されました。if t.Type() == timeType { ... }
: コピー先の型がtime.Time
であるかをチェックします。tv, err := time.Parse(time.RFC3339, string(src))
: XMLから読み取ったバイト列(src
)を文字列に変換し、time.RFC3339
フォーマットでtime.Time
オブジェクトにパースします。t.Set(reflect.ValueOf(tv))
: パースしたtime.Time
オブジェクトを、Goの構造体の対応するフィールドに設定します。
これらの変更により、encoding/xml
はtime.Time
型を自動的に認識し、RFC3339形式(ナノ秒精度を含む)でXMLとの間で変換できるようになり、開発者が手動でカスタムマーシャラー/アンマーシャラーを実装する必要がなくなりました。これは、time.Time
がGoの基本的な型であるため、特別なサポートが提供された良い例と言えます。
関連リンク
- Go Issue 2793:
encoding/xml: handle time.Time as recognized type
- https://github.com/golang/go/issues/2793 - Go Code Review 5634051:
time: clean up MarshalJSON, add RFC3339 method; encoding/xml: handle time.Time as recognized type
- https://golang.org/cl/5634051
参考にした情報源リンク
- Go
time.Time
MarshalJSON
RFC3339: - Go
encoding/xml
time.Time
handling:- https://programming-books.io/essential/go/xml-encoding-decoding-time-time-10a21211111144448888888888888888.html
- https://stackoverflow.com/questions/22020070/how-to-unmarshal-json-into-go-time-time-with-rfc3339-format (JSONだがXMLにも関連するインターフェースの概念)
- https://stackoverflow.com/questions/20750000/how-to-unmarshal-xml-into-go-time-time
- https://stackoverflow.com/questions/30927900/how-to-marshal-a-time-time-field-as-an-xml-attribute-in-go
- https://golangbridge.org/posts/2020-03-02-go-xml-marshal-unmarshal-custom-types/
- RFC3339 vs RFC3339Nano:
- https://medium.com/@sagar.g.s/rfc-3339-vs-iso-8601-vs-rfc-3339nano-a-comprehensive-guide-to-date-time-formats-in-go-d71212121212
- https://go.dev/src/time/format.go (Goのソースコード内のコメント)
- https://golang.cafe/blog/go-time-format-parse-examples.html
- https://www.digitalocean.com/community/tutorials/how-to-use-dates-and-times-in-go
- https://www.splunk.com/en_us/blog/devops/rfc3339-vs-iso8601-a-go-developer-s-perspective.html
- Go
reflect.TypeOf
time.Time
: