フューチャー技術ブログ

Go 1.20 timeパッケージのアップデート

top.png

The Gopher character is based on the Go mascot designed by Renée French.

Gopherのイラストはegonelbre/gophersを利用しました。

はじめに

TIG/EXユニット所属の宮永です。 Go 1.20連載の4本目です。

Go 1.20 Release Notesに記載のMinor changes to the libraryのtimeパッケージのアップデート4点について解説します。

1. layoutにDateTime,DateOnly,TimeOnlyが追加された

Proposalはこちらのissueでされています。

Goの日時表現ですが、他の多くの言語とは勝手が異なります。
今回のアップデートを確認する前に他の言語とGoとの日時表現の違いについて確認します。

例えば、Pythonでyyyy-mm-ddという形式でフォーマットをかける場合は以下の様にします。

Pythonの場合
from datetime import datetime
# 最新時刻を取得
t = datetime.now()
ymd = t.strftime("%Y-%m-%d")
print("時刻:", t)
print("%Y-%m-%dでフォーマット:", ymd)

Rustの場合は以下の様にします。

Rustの場合
use chrono::Utc;

fn main() {
// 最新時刻を取得
let t =Utc::now();
let ymd = t.format("%Y-%m-%d").to_string();
println!("時刻:{}",t);
println!("%Y-%m-%dでフォーマット:{}", ymd);
}

いずれの場合も文字列%Y-%m-%dを与えてフォーマットしています。
ではGoの場合はどうでしょうか。

Goの場合
package main

import (
"fmt"
"time"
)

func main() {
//最新時刻を取得
t := time.Now()
ymd := time.Now().Format("2006-01-02")
fmt.Println("時刻:", t)
fmt.Println("%Y-%m-%dでフォーマット:", ymd)
}

2006-01-02???

はじめて見たとき2006-01-02って何がなんだかわかりませんでした。
ググってみるとこんな記事がありました。

Goのtimeパッケージのリファレンスタイム(2006年1月2日)は何の日?

答えは単純だ。これはアメリカ式の時刻の順番なのだ。”1月2日午後3時4分5秒2006年”(つまり「自然な順番」で1, 2, 3, 4, 5, 6)を指しているのである。Go開発者のRob Pikeさんがgolang-nutsメーリングリストで、最初からよく考えていればこの順番にはしなかったと言っていたが(英語圏でもイギリスとかだと違う順番だしね)、もうその順番になっているので変えられないし、それにきちんと文章化もされているともコメントしていた。従ってこれは単純にそういうものだと思うしかなさそうだ。

そんな馬鹿な!と思いましたが受け入れるしかありません。正直2006-01-02の文字列、私は覚えられないので毎度コピペしてます。

▼おとなりのチームにも同じ悩みを抱えている方がいました
slack.png

(その気持ちとてもわかります!!!)

今回のアップデートでyyyy-mm-ddは以下の様にすることができます。

package main

import (
"fmt"
"time"
)

func main() {
//最新時刻を取得
t := time.Now()
ymd := time.Now().Format(time.DateOnly)
fmt.Println("時刻:", t)
fmt.Println("%Y-%m-%dでフォーマット:", ymd)
}

これなら私でも覚えられます。
些細なアップデートですが、待ち望んでいた方は多かったのではないでしょうか。

今回追加された定数ですが、それぞれの以下のように定義されています。

DateTime   = "2006-01-02 15:04:05"
DateOnly = "2006-01-02"
TimeOnly = "15:04:05"

2. Time.Compareメソッドの追加

Proposalはこちらのissueでされています。

Proposalを立てた方の主張をまとめると

「Time型の比較にはBefore()Equal()After()の3つがあるが、これはそれぞれ<==>に相当する。以前、以後を表す<=>=のメソッドもほしい!!」

ということです。たしかに以前、以後というメソッドがないため、Go1.20がリリースされる前までは
以前を表すのに!x.After(y)(>の否定)で<=となる)とするしかありませんでした。

x.Equal(y) || x.Before(y)でもいいですね

今回Time.Compareメソッドの追加により以前、以後は以下の様に表現することが可能になりました。

  • 以前(xはyよりも前) ⇒ x.Compare(y)<=0

  • 以後(xはyよりも後) ⇒ x.Compare(y)>=0

CompareメソッドはGoの他のライブラリも同じようなルールで実装されているため、使い勝手も良さそうです。今後頻繁に使う機会が出てきそうです。

3. Parseでナノ秒以下の入力の精度は無視する様になった

issueはこちらです。

issueにかかれているコードをそのまま引用します。

package main

import (
"fmt"
"time"
)

func main() {
const in = "2021-09-29T16:04:33.0000000000Z"
fmt.Println(time.Parse(time.RFC3339, in))
fmt.Println(time.Parse(time.RFC3339Nano, in))
}

入力として0が10個並んでおり、厳密にいえばRFC3339Nanoが期待している桁数よりも多い状況です。
このコードの出力としては両者とも2021-09-29 16:04:33 +0000 UTC <nil>を期待していますが、実際には以下の様に出力されます。

output
2021-09-29 16:04:33 +0000 UTC <nil>
0001-01-01 00:00:00 +0000 UTC parsing time "2021-09-29T16:04:33.0000000000Z" as "2006-01-02T15:04:05.999999999Z07:00": cannot parse "0Z" as "Z07:00"

Go 1.20では、2つ目の例でエラーとならないように、ナノ秒以下の精度が入力の場合には無視するようになります。

4. Time.MarshalJSONメソッドのRFC3339への準拠がより厳格になった

あれTime.Marshalだけ?
Time.Unmarshalはいいの?

と思ったのですが、こちらのissueに経緯が書かれていました。

こちらのissueの対応でTime.UnmarshalTime.MarshalRFC3339への準拠がより厳格になったということですが、Time.Unmarshalの対応でAWS SDKのテストでエラーがでてしまうという事態になったようです。そのためTime.Unmarshalについてはロールバックされ、Go1.20ではTime.MarshalのみGo 1.20 Release Notesに記載されているということみたいです。

こちらのissueによると既存のParseにはいくつか問題があるようで、もともとはこの問題に対応するためにTime.Unmarshalで厳格な対応を入れたかったようです。
例えば、現行のParseでは少数部の.,を区別していないようで、どちらもエラーなくParseされるようです。

import (
"fmt"
"time"
)

func main() {
t1, _ := time.Parse(time.RFC3339, "0000-01-01T00:00:00.000Z")
fmt.Println(t1)
t2, _ := time.Parse(time.RFC3339, "0000-01-01T00:00:00,000Z")
fmt.Println(t2)
}
output
0000-01-01 00:00:00 +0000 UTC
0000-01-01 00:00:00 +0000 UTC

他には時間の桁が1桁を許容するという問題もあるようで、00:00:00.000Zと書くべきところを0:00:00.000ZとしてもParseされるようです。

package main

import (
"fmt"
"time"
)

func main() {
t1, _ := time.Parse(time.RFC3339, "0000-01-01T0:00:00.000Z")
fmt.Println(t1)
t2, _ := time.Parse(time.RFC3339, "0000-01-01T00:00:00.000Z")
fmt.Println(t2)
}

output
0000-01-01 00:00:00 +0000 UTC
0000-01-01 00:00:00 +0000 UTC

確かにこのままでは定義が明確ではなくなるため対応が必要そうですが、AWS SDKのテストでエラーがでてくるとなると影響は大きそうです。
timeはGo1.21でも変更がありそうですね。

まとめ

本記事ではGo 1.20リリースのtimeパッケージのアップデートについて解説しました

  • layoutにDateTime,DateOnly,TimeOnlyが追加された
  • Time.Compareメソッドの追加によって以前、以後が表現できるようになった
  • Parseでナノ秒以下の入力の精度は無視する様になった
  • Time.MarshalJSONメソッドのRFC3339への準拠がより厳格になった

個人的にはlayoutに定数が追加されたアップデートが地味に嬉しかったです。😀

次は辻さんの HTTP ResponseControllerです。