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

## 概要

Goのローカルのタイムゾーンが決まる仕組みを調べてみました。

まず現在の日付を以下のように出力するとタイムゾーンはJSTであることがわかります。

```go
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のポインタ型のグローバル変数があり、ローカルのタイムゾーンはそれが使われます。

```go
// 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`が呼ばれることが分かります。

```go
// 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](https://golang.org/src/time/zoneinfo_unix.go)で定義されています。

```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インターフェース](https://golang.org/pkg/fmt/#Stringer)の実装の中でも呼ばれています。Stringerインターフェースは、`fmt.Println`などの内部で呼ばれるインターフェースです。

```go
// 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`](https://golang.org/pkg/time/#FixedZone)で指定したタイムゾーンのLocation型の値を作ることができるので、これを`time.Local`にセットしてあげます。

```go
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`](https://golang.org/pkg/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".