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

Golangのローカルのタイムゾーンが決まる仕組みと指定方法

More than 1 year has passed since last update.

概要

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におけるデフォルトのタイムゾーンを指定する方法は二つあることが分かりました。

  1. tz database のファイルを読み込み先のパスに配置する
  2. 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.initLocalzoneinfo_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とする
  • 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".

freee
スモールビジネスのバックオフィス業務をテクノロジーで自動化し、日本のスモールビジネスを元気にする
http://www.freee.co.jp/
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