如何获取本地时区的完整名称?

4
例如,服务器时区为“欧洲/马德里”,我执行以下操作:
now := time.Now()
fmt.Printf("%v", now.Location().String()) // prints "Local"

zone, _ := now.Zone()
fmt.Printf("%v", zone) // prints "CEST"

但我想要"Europe/Madrid"

时区: https://zh.wikipedia.org/wiki/Tz_database

编辑

提供完整跨平台(Windows、Linux、Mac)答案: https://github.com/thlib/go-timezone-local

所有功劳归功于 colm.anseoMrFuppes 提供以下答案:


结果“Europe/Madrid”是不明确的,同一时区中至少有十几个不同的国家。 - icza
是的,但运行go程序的设备具有非常特定的时区运行,在这种情况下是“欧洲/马德里”,因此我在这里看不到任何歧义,我只想知道正在运行的时区是什么。 - Timo Huovinen
不确定您使用的操作系统是什么,但 zoneinfo 文件只存储偏移量和简称,例如 CEST,它不包含用户友好的长名称。操作系统通过文件名 路径 加载此信息。路径后缀包含长名称。 - colm.anseo
我使用Windows、Linux和Mac,在Linux上运行@colm.anseo的解决方案可以给我提供../usr/share/zoneinfo/Europe/Madrid,这正是我想要的...但我希望用Go语言实现。 - Timo Huovinen
1
@icza,像IANA的“Europe/Madrid”这样的时区并不特定于国家,而是地理区域。当你使用“BST”或“IST”这样的缩写时,歧义就开始了。 - FObersteiner
2个回答

2

IANA时区数据库在大多数操作系统上都可用(*)。Go标准库也内置了它:

runtime.GOROOT() + "/lib/time/zoneinfo.zip"

操作系统决定时区的选择方式(通过名称)以及该名称是否被记录在任何地方:
ls -al /etc/localtime

# MacOS
/etc/localtime -> /var/db/timezone/zoneinfo/America/New_York

# Linux
/etc/localtime -> ../usr/share/zoneinfo/America/New_York

在上述情况下,可以推断出名称。
注意:Go默认使用本地操作系统时间的/etc/localtimetimezone.go),但是可以通过TZ环境变量进行覆盖。
因此,可以通过符号链接目标路径来推断本地操作系统时区的名称。
// tz.go

//go:build !windows
// +build !windows

const localZoneFile = "/etc/localtime" // symlinked file - set by OS
var ValidateLocationName = true        // set to false to disable time.LoadLocation validation check

func LocalTZLocationName() (name string, err error) {

    var ok bool
    if name, ok = os.LookupEnv("TZ"); ok {
        if name == "" { // if blank - Go treats this as UTC
            return "UTC", nil
        }
        if ValidateLocationName {
            _, err = time.LoadLocation(name) // optional validation of name
        }
        return
    }

    fi, err := os.Lstat(localZoneFile)
    if err != nil {
        err = fmt.Errorf("failed to stat %q: %w", localZoneFile, err)
        return
    }

    if (fi.Mode() & os.ModeSymlink) == 0 {
        err = fmt.Errorf("%q is not a symlink - cannot infer name", localZoneFile)
        return
    }

    p, err := os.Readlink(localZoneFile)
    if err != nil {
        return
    }

    name, err = inferFromPath(p) // handles 1 & 2 part zone names
    return
}

func inferFromPath(p string) (name string, err error) {

    dir, lname := path.Split(p)

    if len(dir) == 0 || len(lname) == 0 {
        err = fmt.Errorf("cannot infer timezone name from path: %q", p)
        return
    }

    _, fname := path.Split(dir[:len(dir)-1])

    if fname == "zoneinfo" {
        name = lname // e.g. /usr/share/zoneinfo/Japan
    } else {
        name = fname + string(os.PathSeparator) + lname // e.g. /usr/share/zoneinfo/Asia/Tokyo
    }

    if ValidateLocationName {
        _, err = time.LoadLocation(name) // optional validation of name
    }

    return
}

// tz_windows.go
//go:build windows
// +build windows

func LocalTZLocationName() (string, error) {
    return "", fmt.Errorf("local timezone name inference not available on Windows")
}

(*) 对于Windows,根据Go源代码(zoneinfo_windows.go),时区信息是从Go标准库加载的,原因是:

// BUG(brainman,rsc): On Windows, the operating system does not provide complete
// time zone information.
// The implementation assumes that this year's rules for daylight savings
// time apply to all previous and future years as well.

Windows注册表键:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\TimeZoneInformation

存储本地时区-但似乎不存储完整的位置名称:

TimeZoneKeyName=Eastern Standard Time

非常有帮助,谢谢。为了完整起见,看到如何在Windows上获取时区将是很好的。 - Timo Huovinen
如果在接下来的几天内没有找到答案,如果我使用您的解决方案创建一个在所有平台上获取时区的Go函数,并将其作为此问题的已接受答案发布,您是否同意? - Timo Huovinen
太棒了!唯一缺少的是添加一个如何使用你的 LocalTZLocationName 函数的示例,并且优雅地处理 TZ 环境变量。 - Timo Huovinen
修复了区域名称可能为一部分或两部分的错误。还添加了如果设置了TZ环境变量的支持。 - colm.anseo
@colm.anseo,希望你不介意,我已经将你的答案整合到一个仓库中:https://github.com/thlib/go-timezone-local - Timo Huovinen

1
在Windows上,这个过程也不是特别复杂。如@colm.anseo所提到的,您可以读取相应的注册表键来获取Windows使用的时区名称,然后将其映射到适当的IANA时区名称。
从注册表中读取tz。
package main

import (
    "log"

    "golang.org/x/sys/windows/registry"
)

var winTZtoIANA = map[string]string{
    // just includes my time zone; to be extended...
    "W. Europe Standard Time": "Europe/Berlin",
}

func main() {

    k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\TimeZoneInformation`, registry.QUERY_VALUE)
    if err != nil {
        log.Fatal(err)
    }
    defer k.Close()

    winTZname, _, err := k.GetStringValue("TimeZoneKeyName")
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("local time zone in Windows: %v\n", winTZname)

    if IANATZname, ok := winTZtoIANA[winTZname]; ok {
        log.Printf("local time zone IANA name: %v\n", IANATZname)
    } else {
        log.Fatalf("could not find IANA tz name for %v", winTZname)
    }

}

或者,您可以阅读tzutil /g的输出

cmd := exec.Command("tzutil", "/g")
winTZname, err := cmd.Output() // note: winTZname is []byte, need to convert to string
if err != nil {
    // handle error
}

您可以在此处找到一份全面的映射。它基于Lennart Regebro的tzlocal包,该包解决了Python中给定的问题。

有趣的方法,非常感谢您。 - Timo Huovinen
@TimoHuovinen 我找到了另一种方法,使用Windows的 tzutil 命令。我已经在 这里 添加了它。这是一种不必为每个平台单独编译就能让它们工作的好方法。 - FObersteiner
1
希望你不介意,我把你的答案整理到一个仓库里了: https://github.com/thlib/go-timezone-local - Timo Huovinen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接