GoでJSTのタイムゾーンを扱う方法
本記事はGoでJSTのタイムゾーンを指定する方法を紹介します。
現在時刻を取得するには time.Now()
を使うことになります。time.Now()
はデフォルトではローカルな時刻が取得できます。例えばAWS Lambda上ではUTCの時刻が取得できます。日本のロケーションで動作するアプリケーションを前提にすると、アプリケーションによっては時刻をJSTで統一したほうがシンプルで扱いやすい、といったケースもあるでしょう。本記事ではGoのアプリケーションでJSTの時刻を取得する方法を紹介します。
*time.Location
の取得方法
まずタイムゾーン (time.Location
) について説明します。Goではグローバル変数として *time.Local
変数があります。アプリケーションで更新することはアンチパターンとされているため、変数を上書きすることはないでしょう。
// Local represents the system's local time zone. // On Unix systems, Local consults the TZ environment // variable to find the time zone to use. No TZ means // use the system default /etc/localtime. // TZ="" means use UTC. // TZ="foo" means use file foo in the system timezone directory. var Local *Location = &localLoc
*time.Location
を取得するには主に以下のAPIを使います。LoadLocationFromTZData
を使用する機会は多くはないでしょう。
time.FixedZone
time.LoadLocation
LoadLocationFromTZData
time.FixedZone
を用いて *time.Location
を生成することができます。
myJST := time.FixedZone("Asia/Tokyo", 9*60*60)
または time.LoadLocation
でロケーション名を指定して *time.Location
を取得します。
jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) }
time.LoadLocation
は外部依存があるため scratch などの軽量コンテナ上では動作しません。ソースはランタイムごとに異なりますが、例えばUNIX系の場合は zoneinfo_unix.go から確認できます。
以下のようにして確認することができます。
- Dockerfile
FROM golang:1.15 as builder WORKDIR /go/src/app COPY go.mod go.sum ./ RUN go mod download COPY ./main.go ./ ARG CGO_ENABLED=0 ARG GOOS=linux ARG GOARCH=amd64 RUN go build -o /go/bin/main -ldflags '-s -w' -trimpath FROM scratch as runner COPY --from=builder /go/bin/main /app/main ENTRYPOINT ["/app/main"]
- main.go
package main import "time" func main() { _, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } }
- コンテナのビルド&実行
$ docker build -t jst-time-test1 -f Dockerfile . $ docker run --rm jst-time-test1
- 実行結果
panic: unknown time zone Asia/Tokyo goroutine 1 [running]: main.main() tzsample/light/main.go:8 +0x65
time.LoadLocation
を使ってタイムゾーンを取得する場合は、アプリケーションが動作するランタイム上で動作するか確認する必要があります。ランタイムにタイムゾーンの情報が含まれない場合は
などといった回避方法があります。
タイムゾーンを指定して時刻を取得する方法
続いて現在時刻を取得する方法です。time.Now()
で取得する時刻にロケーションを指定する方法は以下の2種類の方法があります。
※ "Asia/Tokyo"
のタイムゾーンを指定する文字列は環境変数から値を設定することももちろん可能です。
方法1.時刻をパースする際に In
でロケーションを指定する
もっともポピュラーな変換方法です。
jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } nowJST := time.Now().In(jst) fmt.Printf("nowJST: %v\n", nowJST.Format(time.RFC3339)) // nowJST: 2009-11-11T08:00:00+09:00
https://play.golang.org/p/BRrCCx4qpcP
方法2. time
パッケージが保持しているグローバル変数を更新する
time
パッケージが保持しているグローバル変数 time.Local
をJSTで更新する方法です。グローバル変数を更新することで間接的に time.Now()
で取得できるタイムゾーンを差し替えることができます。
func init() { jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } time.Local = jst } func main() { now := time.Now() fmt.Printf("now: %v\n", now.Format(time.RFC3339)) // now: 2009-11-11T08:00:00+09:00 }
https://play.golang.org/p/UkMP0dxlOyE
方法1と方法2を比較すると、方法1では時刻を取得するすべての実装箇所から In
を用いてタイムゾーンの変換をする必要があります。変換漏れがあると時刻がローカルな時刻とJSTと混在することになるため、一見方法2が一律指定できて、不具合が少ないと考えるかも知れません。
しかし方法2は筋がいい実装と言えません。もちろんミュータブルなグローバル変数を更新することで実現できますが、意図せずいろいろなところに影響が及ぶ可能性があります。実際にRuss Coxも次のようにほとんどの場合は間違いである、と述べています。
I've updated the title. What you're asking for is
``` package time
// SetLocal changes the current meaning of time.Local to match loc. func SetLocal(loc *Location) I don't believe this is a good idea. In general but especially in Go, coordination via global mutable state is almost always a mistake and difficult to correct once you realize it. ```
If there are higher-level APIs that force the use of time.Local, we should probably address those instead. For example, if package log is what you are concerned about, it would probably be okay to file a separate issue for adding log.SetLocation and log.(*Logger).SetLocation.
https://github.com/golang/go/issues/10701#issuecomment-148777945
実際、以下のように init
内で初期化した time.Local
の変数とテスト中に time.Local
を読み込むと DATA RACE が発生する、という事象もあります。私も再現しました。
go test -raceでinitで初期化したtime.Localのグローバル変数とtest中のtime.Localの読み込みがDATA RACEする問題にハマっとる。init()で初期化しているのでそもそもraceしないと思うし、time.Afterで参照しているtime.Localとバッティングしているのが謎…
— freedomな人 (@tzm_freedom) December 14, 2020
方法1'.ロケーション付で時刻が取得できる関数を生成
方法1の拡張版です。In
で変換するが、漏れのないように時刻を取得する関数を新たに生成して、その関数経由で時刻を取得する方法です。時刻を取得するときにアプリケーション側で宣言した nowFunc
を使う必要がありますが、方法2と比較すると安全な方法です。
var nowFunc func() time.Time func init() { jst, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } nowFunc = func() time.Time { return time.Now().In(jst) } } func main() { now := nowFunc() fmt.Printf("now: %v\n", now.Format(time.RFC3339)) // now: 2009-11-11T08:00:00+09:00 }
https://play.golang.org/p/uy6zILsixiZ
Songmu/flextime
などの時刻を差し替えることができるライブラリなどを用いれば、上記同様に時刻のタイムゾーン変換ができます。以下は Songmu/flextime
の NowFunc
を用いて時刻を差し替えています。
func init() { tz, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } flextime.NowFunc(func() time.Time { return time.Now().In(tz) }) } func main() { now := flextime.Now() fmt.Printf("now: %v\n", now.Format(time.RFC3339)) // now: 2009-11-11T08:00:00+09:00 }
https://play.golang.org/p/srJJReRbun_M
文字列から time.Time
に変換する方法
日付の文字列 2021-01-01
から time.Time
型に変換するには以下のAPIを使います。
time.Parse
time.ParseInLocation
time.Parse
はタイムゾーンが文字列に含まれていない場合、デフォルトでUTCに変換します。
func main() { d := "2021-01-30" nowDate, err := time.Parse("2006-01-02", d) if err != nil { panic(err) } fmt.Printf("nowDate: %v", nowDate.Format(time.RFC3339)) // nowDate: 2021-01-30T00:00:00Z }
変換時にタイムゾーンが重要な場合は time.ParseInLocation
で明示的にタイムゾーンを指定して文字列から変換しましょう。
func main() { d := "2021-01-30" tz, err := time.LoadLocation("Asia/Tokyo") if err != nil { panic(err) } nowDateJST, err := time.ParseInLocation("2006-01-02", d, tz) if err != nil { panic(err) } fmt.Printf("nowDateJST: %v", nowDateJST.Format(time.RFC3339)) // nowDateJST: 2021-01-30T00:00:00+09:00 }
まとめ
*time.Location
のグローバル変数を更新してタイムゾーンを変換する方法は危険なのでやめましょう。現在時刻をJSTで取得する場合は In
を使って変換する方法が良いです。ラッパーをかますと、安全にかつ漏れが少なく変換することができます。
-
Go1.15からビルド時に
-tags timetzdata
を付与してビルドすることで、タイムゾーン情報を埋め込むことができるようになりました。詳細は https://golang.org/pkg/time/tzdata/ に記載されています。↩