【Go】タイムゾーンで沼った話
はじめに
現在携わっているプロジェクトの実装の中で、「現在日時とレコードに登録されている日時の比較」という部分があります。
その中で、タイムゾーンが合わず正確な比較ができずに悩んだので、その解決策について書こうと思います!
この記事でわかること
-
timestamp without time zoneの扱いと、タイムゾーンずれが起きる理由がわかる - GORMを用いたDB接続時にタイムゾーンを設定しない場合の挙動が理解できる
- Goで時刻を比較するときに正確な結果を得るための対処法が学べる
環境
- go version go1.25.0 darwin/arm64
- PostgreSQL 17.4(DockerでDBコンテナを起動)
前提
今回は以下のような簡易的なユーザー管理テーブルを用意しました。
created_at(登録日時)が timestamp(0) without time zone となっているのがポイントです。
# \d users
| Column | Type | Collation | Nullable | Default |
|------------|-------------------------------|-----------|----------|--------------------------------------|
| id | bigint | | not null | nextval('users_id_seq'::regclass) |
| name | text | | not null | |
| created_at | timestamp(0) without time zone| | | |
# SELECT * FROM users;
id | name | created_at
----+------+---------------------
1 | Taro | 2025-10-27 19:00:00
時間の比較
ここでは、ユーザーの登録日時と現在日時(執筆時点)を比較し、現在時刻が後であれば差分を出力する処理を行います。
type User struct {
ID int `gorm:"primaryKey"`
Name string `gorm:"not null"`
CreatedAt time.Time `gorm:"type:timestamp(0) without time zone"`
}
func main() {
ctx := context.Background()
user, _ := gorm.G[User](db).Limit(1).Take(ctx)
now := time.Now().Truncate(time.Second)
if now.After(user.CreatedAt) {
diff := now.Sub(user.CreatedAt)
fmt.Println("差分: ", diff)
}
}
これを実行するとどうなるでしょうか?
答えは、「差分は出力されない」です。
なぜなら、登録日時がUTC、現在日時がJSTで取得されており、タイムゾーンが一致しておらす正しく比較できないからです。
# 登録日時と現在日時を標準出力
CreatedAt: 2025-10-26 11:29:31 +0000 UTC
Now: 2025-10-26 11:29:31.475645 +0900 JST m=+0.057814584
time.Now()について
timeパッケージのNow()メソッドには、以下のようなコメントが書かれています。
Now returns the current local time.
Now()メソッドはOS(またはコンテナ)のローカルタイムゾーンに従って時刻を取得します。
今回の環境ではシステムのタイムゾーンが JST だったため、now は JST で取得されました。
比較ができない原因
1. DBコンテナのタイムゾーン設定
ここでまず考えられるのは、DBのタイムゾーン設定がJSTになっていないかもしれないということです。PostgreSQLではデフォルトではUTCで設定されていることが多いです。show timezone;で現在のタイムゾーン設定を確認することができます。
# show timezone;
-[ RECORD 1 ]--------
TimeZone | Asia/Tokyo
DBのタイムゾーンはAsia/Tokyoとなっており問題ありません。
2. DB接続時のタイムゾーンが設定されていない
調査の結果、DB接続用DSNにタイムゾーン指定がなかったことが原因でした。
// DB接続に関わる部分のみ抜粋
cfg := DBConfig{
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
Host: os.Getenv("DB_HOST"),
Port: os.Getenv("DB_PORT"),
Name: os.Getenv("DB_NAME"),
}
dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name)
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
GORMのドキュメントを読んでみると、タイムゾーンの設定をする記述がされています。
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
これに従って、タイムゾーンの設定をして実行してみました。
# go run main.go
CreatedAt: 2025-10-27 19:00:00 +0900 JST
Now: 2025-10-27 23:02:56 +0900 JST
差分: 4h2m56s
なんと、差分が出力されたではありませんか!
タイムゾーンが揃ったことで、正しく差分を比較できるようになりました!
timestamp without time zoneに注意する!
timestamp without time zone はその名の通り、タイムゾーンを持たない型のことを指します。
そうなると、タイムゾーンはクライアント側の設定に依存することになります。
DBがtimestamp without time zoneを返すとき、GORMは接続時のタイムゾーン設定を使い、それをGoのtime.Timeに変換します。
このとき、タイムゾーン指定がなければ、PostgreSQLは通常UTCととして扱う傾向があります。
今回の場合、DSNでタイムゾーンを設定していなかったため、UTCだとクライアント側で判断し、JSTで登録した時間がUTCとして扱われたことで、正確に比較ができませんでした。
まとめ
今回は、時間の比較をする際にタイムゾーンに気をつけないと意図せぬ比較が実行される可能性があることを見ていきました。
大事なのは、
- DBの設定に合わせて、DSNにもタイムゾーンを付与すること
-
timestamp without time zoneは文字通りタイムゾーン情報を保持せず、クライアント側の設定に依存することを意識すること
この2点になりそうです。
Discussion