LoginSignup
40
23

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-01-27

概要

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".

40
23
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
23