Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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型の相互変換メソッドがあって便利
  • 微妙にこれらは互換性のない区間があるため、変換時にエラーが起きうるインターフェースになっている
  • とはいえ、普通はエラーが起きない気がする。
Hiraku
PHP, Go界隈をうろうろしています。最近はgRPCと戦ってる。 特に明示していなければ、記事中のソースコード片は `CC-0 1.0` とします。出典表示無しで自由にコピペして頂いて構いません。 ただ、記事自体をコピペされるのは嫌なので、ソースコード部分以外の文章は通常通り全ての著作権を私が保持するものとします。 引用を超える範囲のコピペは止めて下さい。
http://blog.tojiru.net/
mercari
フリマアプリ「メルカリ」を、グローバルで開発しています。
https://tech.mercari.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away