概要
Goのローカルのタイムゾーンが決まる仕組みを調べてみました。
まず現在の日付を以下のように出力するとタイムゾーンはJSTであることがわかります。
import (
"time"
"fmt"
)
func main() {
fmt.Println(time.Now().Format("2006-01-02 MST"))
}
$ go run main.go
2019-01-28 JST
これがどのように決まっているのかを調べた結果、goにおけるデフォルトのタイムゾーンを指定する方法は二つあることが分かりました。
- tz database のファイルを読み込み先のパスに配置する
- time.FixedZoneで指定した結果をローカルのタイムゾーンにする
ローカルのタイムゾーンが決まる仕組み
まず、ローカルのタイムゾーンが決まる仕組みをtimeパッケージのソースコードを読んで辿っていきます。
timeパッケージには、Localと言うLocationのポインタ型のグローバル変数があり、ローカルのタイムゾーンはそれが使われます。
// cf. https://golang.org/pkg/time/#Location
// Local represents the system's local time zone
var Local *Location = &localLoc
どのように初期化されるのか
LocalはlocalLoc経由で、func (*Location) get()
が呼ばれると初期化されます。
以下を見ると、呼ばれたLocationのポインタが&localLoc
であれば、一度だけinitLocal
が呼ばれることが分かります。
// cf. https://golang.org/src/time/zoneinfo.go?s=1994:2358
// localLoc is separate so that initLocal can initialize
// it even if a client has changed Local.
var localLoc Location
var localOnce sync.Once
func (l *Location) get() *Location {
if l == nil {
return &utcLoc
}
if l == &localLoc {
localOnce.Do(initLocal)
}
return l
}
time.initLocal
はzoneinfo_unix.goで定義されています。
// cf. https://golang.org/src/time/zoneinfo_unix.go
package time
import (
"runtime"
"syscall"
)
// Many systems use /usr/share/zoneinfo, Solaris 2 has
// /usr/share/lib/zoneinfo, IRIX 6 has /usr/lib/locale/TZ.
var zoneSources = []string{
"/usr/share/zoneinfo/",
"/usr/share/lib/zoneinfo/",
"/usr/lib/locale/TZ/",
runtime.GOROOT() + "/lib/time/zoneinfo.zip",
}
func initLocal() {
// consult $TZ to find the time zone to use.
// no $TZ means use the system default /etc/localtime.
// $TZ="" means use UTC.
// $TZ="foo" means use /usr/share/zoneinfo/foo.
// cf. https://golang.org/pkg/syscall/#Getenv
// 環境変数から読み込む
tz, ok := syscall.Getenv("TZ")
switch {
case !ok:
// locadLocationは第二引数のパス一覧から順番に第一引数のファイルを読み込んでLocationを返す
// OSのタイムゾーンを設定しているファイルは /etc/localtime に置かれることが多いようです
z, err := loadLocation("localtime", []string{"/etc/"})
if err == nil {
localLoc = *z
localLoc.name = "Local"
return
}
case tz != "" && tz != "UTC":
if z, err := loadLocation(tz, zoneSources); err == nil {
localLoc = *z
return
}
}
// Fall back to UTC.
localLoc.name = "UTC"
}
これを見ると以下の順序でタイムゾーンを決めるファイルを読み込んで、localLoc
にLocationをセットしています。
-
syscall.Getenv("TZ")
で文字列を取得できた場合- それが空文字でもUCTでもない場合
-
zoneSources
で指定されたパス以下のその名前の tz database のファイルから決める - 見つからなければUTCとする
-
- それが空文字もしくはUCTの場合は、UTCとする
- それが空文字でもUCTでもない場合
-
syscall.Getenv("TZ")
で文字列を取得できなかった場合-
/etc/localtime
にある tz database のファイルから決める - 見つからなければ、UTCとする
-
このようにLocalはlocalLoc経由で、func (*Location) get()
が呼ばれるとinitialLocal
内で初期化されることが分かりました。
いつ初期化されるのか
では、func (*Location) get()
はどのタイミングで呼ばれて初期化するかというと、いくつかの場所で実装されていますが、Stringerインターフェースの実装の中でも呼ばれています。Stringerインターフェースは、fmt.Println
などの内部で呼ばれるインターフェースです。
// cf. https://golang.org/src/time/zoneinfo.go?s=2358:2555
// String returns a descriptive name for the time zone information,
// corresponding to the name argument to LoadLocation or FixedZone.
func (l *Location) String() string {
return l.get().name
}
このため、一度標準出力されると get() => initialLocal() => tz database ファイルの読み込み => Localに値がセットされる という順番でタイムゾーンが決まることが分かります。
このことからローカルのタイムゾーンを任意で指定するには、上記で読み込まれるパスに tz database ファイルを配置すればいいことが分かります。
しかし、Goのアプリケーション専用のDockerコンテナでない限りシステムのグローバルなファイルを置き換えたり、追加するのは避けたいはずです。また、そのような方法だと実行環境によってタイムゾーンが変わることになるので、複数人で開発することが難しくなります。
time.FixedZoneで指定する
そのための手段としてコードでローカルのタイムゾーンを指定する方法もあります。
具体的にはtime.FixedZone
で指定したタイムゾーンのLocation型の値を作ることができるので、これをtime.Local
にセットしてあげます。
import (
"time"
"fmt"
)
func main() {
// タイムゾーンの名前とUTCとの差分となる秒数を引数で渡す
time.Local = time.FixedZone("Local", -8*60*60)
fmt.Println(time.Now().Format("2006-01-02 MST"))
}
タイムゾーンがLocalになっていることが確認できます。
$ go run main.go
2019-01-27 Local
なお、第一引数の名前をLocal
にしたのは、time.LoadLocation
が呼ばれた時に指定したグローバル変数のtime.Local
を返すようにするためです。
c.f https://golang.org/pkg/time/#LoadLocation
LoadLocation returns the Location with the given name.
If the name is "" or "UTC", LoadLocation returns UTC. If the name is "Local", LoadLocation returns Local.
Otherwise, the name is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York".