[インデックス 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.TimeMarshalJSONRFC3339: - Go
encoding/xmltime.Timehandling:- 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.TypeOftime.Time: