Go 编程: 时区问题与内嵌资源

Golang 语言在最近一次版本(Go 1.15)升级中,就时区问题在系统库中内嵌了时区资源包time/tzdata。同时,鉴于 Go 1.16 版本中可能会对内嵌资源提案进行发布,顺便就时区问题与内嵌资源做个简单的笔记。

时区问题

不同时区同一时刻,按各自时区所展示的时钟时间是不同,但是,同一时刻按 UNIX 时间戳表示时,其值是相同的。所以,通常涉及不同时区的应用程序在时间的存储上常常会使用 UNIX 时间戳进行存储。

所谓时区问题,就是将具体时刻按照对应时区展示相应的时钟时间的问题。

在 Go 语言中,解决时区问题的关键语句如下:

import "time"

location, err := time.LoadLocation("Asia/Shanghai")
// 打印 Asia/Shanghai 时区当前的时钟时间
fmt.Println(time.Now().In(location))

但是以上语句的问题就是,在加载 time.LoadLocation 语句时可能会失败。因为对应系统如果没有安装相应的时区数据时,该语句就会异常。在正常的操作系统上,这个问题基本不会发生;之所以出现错误,是因为如今大部分程序都是通过容器的方式进行发布,而容器的基础镜像如果没有安装时区数据就会报错。所以,在 Go 1.15 之前,发布容器镜像时,我们会在基础镜像上增加相应的时区数据:

FROM alpine
# 安装时区数据并设置系统时区
RUN  apk --update --no-cache add tzdata \ 
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

如今在 Go 1.15 版本中,系统库直接将时区数据内嵌到系统包中,不再需要在基础镜像中安装时区数据,而是将时区数据之间打包进目标程序中。

import (
    "time"
    _ "time/tzdata" //内嵌时区数据资源
)

location, err := time.LoadLocation("Asia/Shanghai")
// 打印 Asia/Shanghai 时区当前的时钟时间
fmt.Println(time.Now().In(location))

这种方式需要修改原有程序,import时区数据包,对于老程序而言不是很友好。所以,Go 1.15 中还提供了编译选项的方式内嵌时区数据资源的办法,即增加-tags timetzdata编译选项即可。

内嵌资源

简单看一下,time/tzdata包的实现就知道时区数据是如何通过二进制的方式内嵌进入目标程序的。

系统包time/tzdata/tzdata.go文件中,系统会通过 go generate 标记注释来生成time/tzdata/zipdata.go文件。

//go:generate go run generate_zipdata.go

再看看系统文件/usr/local/go/src/time/tzdata/generate_zipdata.go的实现:

// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build ignore

// This program generates zipdata.go from $GOROOT/lib/time/zoneinfo.zip.
package main

import (
	"bufio"
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
)

// header is put at the start of the generated file.
// The string addition avoids this file (generate_zipdata.go) from
// matching the "generated file" regexp.
const header = `// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

` + `// Code generated by generate_zipdata. DO NOT EDIT.

// This file contains an embedded zip archive that contains time zone
// files compiled using the code and data maintained as part of the
// IANA Time Zone Database.
// The IANA asserts that the data is in the public domain.

// For more information, see
// https://www.iana.org/time-zones
// ftp://ftp.iana.org/tz/code/tz-link.htm
// http://tools.ietf.org/html/rfc6557

package tzdata

const zipdata = `

func main() {
	// We should be run in the $GOROOT/src/time/tzdata directory.
	data, err := ioutil.ReadFile("../../../lib/time/zoneinfo.zip")
	if err != nil {
		die("cannot find zoneinfo.zip file: %v", err)
	}

	of, err := os.Create("zipdata.go")
	if err != nil {
		die("%v", err)
	}

	buf := bufio.NewWriter(of)
	buf.WriteString(header)

	ds := string(data)
	i := 0
	const chunk = 60
	for ; i+chunk < len(data); i += chunk {
		if i > 0 {
			buf.WriteRune('\t')
		}
		fmt.Fprintf(buf, "%s +\n", strconv.Quote(ds[i:i+chunk]))
	}
	fmt.Fprintf(buf, "\t%s\n", strconv.Quote(ds[i:]))

	if err := buf.Flush(); err != nil {
		die("error writing to zipdata.go: %v", err)
	}
	if err := of.Close(); err != nil {
		die("error closing zipdata.go: %v", err)
	}
}

func die(format string, args ...interface{}) {
	fmt.Fprintf(os.Stderr, format+"\n", args...)
	os.Exit(1)
}

很明显,这样直接写一个专门的程序将资源嵌入到代码的方式非常拙劣。一旦资源数过多,就无法写个程序草草了事了,而是需要一个统一的解决方案。

在官方提出内嵌资源提案前,就已经有很多很好的实现方案,具体实现在提案中都有提及,这里不再罗列了。

简单贴下提案的方法:

//内嵌资源提案
package server

// content holds our static web server content.
//go:embed image/* template/*
//go:embed html/index.html
var content embed.Files

Go 1.16 还是值得期待一下的。

阅读