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

## deprecated

https://pkg.go.dev/github.com/golang/protobuf/ptypes
にある通り、google.golang.org/protobufに今は対応する関数があるため、この記事に書いてあるコードは移行するべきである。

----
(以下、元の記事)

[protocol buffers IDL再入門 - Qiita](https://qiita.com/Hiraku/items/7c8f56e8564158c895f9)

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

* [google.protobuf.Timestamp](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto)
* [google.protobuf.Duration](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/duration.proto)

google.protobuf.TimestampはTimezoneを持たない時刻。日時、時刻、ナノ秒まで記録できる。TimezoneはUTCに揃えられていると認識される。
google.protobuf.Durationは期間。○○秒、〇〇時間、などの概念を表す。これもナノ秒まで記録できる。

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

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

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

## 時刻型の相互変換

### *tspb.Timestamp → time.Time

[github.com/golang/protobuf/ptypes#Timestamp](https://godoc.org/github.com/golang/protobuf/ptypes#Timestamp) を使う。

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

### time.Time → *tspb.Timestamp

[github.com/golang/protobuf/ptypes#TimestampProto](https://godoc.org/github.com/golang/protobuf/ptypes#TimestampProto) を使う。

```go
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レベルの未来日付や紀元前レベルの過去の日付であっても扱えるというわけだ。

```go
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](https://godoc.org/github.com/golang/protobuf/ptypes#Duration)

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

### time.Duration → *durpb.Duration

[github.com/golang/protobuf/ptypes#DurationProto](https://godoc.org/github.com/golang/protobuf/ptypes#DurationProto)

```go
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よりも表現の幅がせまい。

```go
// 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型の相互変換メソッドがあって便利
* 微妙にこれらは互換性のない区間があるため、変換時にエラーが起きうるインターフェースになっている
* とはいえ、普通はエラーが起きない気がする。