Posted at

protocol buffersのTimestamp, DurationとGoのtimeパッケージ型の相互変換

protocol buffers IDL再入門 - Qiita

protobufにはwell-known typesと呼ばれるよく使われる型があらかじめ用意されており、その中には日時と期間がある。

google.protobuf.TimestampはTimezoneを持たない時刻。日時、時刻、ナノ秒まで記録できる。TimezoneはUTCに揃えられていると認識される。

google.protobuf.Durationは期間。○○秒、〇〇時間、などの概念を表す。これもナノ秒まで記録できる。

一方でGoにおいては、これらの概念に対応するものといえば標準パッケージ time の中にある、 time.Timetime.Duration であろう。

https://golang.org/pkg/time/

当然、相互に変換したくなるので、便利なパッケージが用意されている。


時刻型の相互変換


*tspb.Timestamp → time.Time

github.com/golang/protobuf/ptypes#Timestamp を使う。

func Timestamp(ts *tspb.Timestamp) (time.Time, error)


time.Time → *tspb.Timestamp

github.com/golang/protobuf/ptypes#TimestampProto を使う。

func TimestampProto(t time.Time) (*tspb.Timestamp, error)

ちなみに、便利関数として func TimestampNow() *tspb.Timestamp なども用意されている。わざわざ ts, _ := TimestampProto(time.Now()) なんてしなくてもサクッと現在時刻を作成できる。


どういうときにエラーが起きるのか?

両者とも、errorが返ってくるインターフェースになっている。これは、微妙にtime.Timeとtspb.Timestampに非互換性があることに起因するようだ。

例えばGoの time.Date のyearにメチャクチャ大きな数字や負の数を入れても、動作する。西暦1億年みたいなSFレベルの未来日付や紀元前レベルの過去の日付であっても扱えるというわけだ。

func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time

一方で、protobufのTimestamp型は 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. が表現できる幅だと明確に書いてある。

https://github.com/golang/protobuf/blob/6c65a5562fc06764971b7c5d05c76c75e84bdbf7/ptypes/timestamp/timestamp.proto

protobufはシステム間で互換性を保ちながら情報をやり取りする目的のため、様々なシステムで妥当に扱える範囲を設定した、ということなのだろう。例えばMySQLのDateTime型は上限が 9999-12-31 23:59:59 だった。

なので、Go側で超未来や超過去のtime.Timeを作って、*tspb.Timestamp に変換しようとしたらエラーになるし、Timestamp()にいきなり範囲を超えた *tspb.Timestamp を無理やり渡すとそれもエラーになる。

他にもいくつかバリデーションはしているけど、詳細はptypesのソースコードを見て欲しい。

https://github.com/golang/protobuf/blob/6c65a5562fc06764971b7c5d05c76c75e84bdbf7/ptypes/timestamp.go#L63-L77

とはいえ、通常のシステムで扱っている日時では起きないだろうから、あまり丁寧にエラーハンドリングしなくても大丈夫かもしれない。


期間型の相互変換


*durpb.Duration → time.Duration

github.com/golang/protobuf/ptypes#Duration

func Duration(p *durpb.Duration) (time.Duration, error)


time.Duration → *durpb.Duration

github.com/golang/protobuf/ptypes#DurationProto

func DurationProto(d time.Duration) *durpb.Duration


どういうときにエラーが起きるのか?

おや。timestampと違って、time.Durationから変換するときにエラーが返ってこない。

protobufの場合、扱える範囲はプラマイ1万年と書いてある。 "approximately" なのは、うるう年などを雑にしか考慮していないためだと思われる。


Range is approximately +-10,000 years.


Timestampが1万年いけるので、相応の時間をDurationでも扱えるようにしたのだと思う。

一方で、Goのtime.Durationは内部的にナノ秒をint64で保持しているだけである。なので実は扱える範囲はだいたい290年間ぐらいにしかならず、全然time.Durationよりも表現の幅がせまい。

// A Duration represents the elapsed time between two instants

// as an int64 nanosecond count. The representation limits the
// largest representable duration to approximately 290 years.
type Duration int64

time.Duration*durpb.Duration でエラーが起きようもないので、エラー判定も省略されている。


まとめ


  • 時間系は、Goの標準型とprotobufのwell-known型の相互変換メソッドがあって便利

  • 微妙にこれらは互換性のない区間があるため、変換時にエラーが起きうるインターフェースになっている

  • とはいえ、普通はエラーが起きない気がする。