This page looks best with JavaScript enabled

Time and Time Zones in Go

 ·  ☕ 7 min read

1. Time and Time Zones

1.1 Time Standards

UTC, Coordinated Universal Time, is the current time standard, counted by atomic time.

GMT, Greenwich Mean Time, is the older time standard, which defines the moment the sun passes over the Royal Observatory in Greenwich, on the outskirts of London, as 12 noon.

UTC time is more accurate, but if your precision requirements are not strict, the two standards can be treated as equivalent.

1.2 Time Zone Division

Starting from the Greenwich prime meridian, a time zone is defined for every 15° of longitude going east or west, giving 24 time zones in total — 12 east and 12 west.

For administrative convenience, however, a country or a province is usually grouped into a single zone. Here are several times expressed in UTC:

  • UTC-6 (CST — North American Central Standard Time)
  • UTC+9 (JST — Japan Standard Time)
  • UTC+8 (CT/CST — China Standard Time)
  • UTC+5:30 (IST — India Standard Time)
  • UTC+3 (MSK — Moscow Time Zone)

1.3 Local Time

Local time is the current system time with its time zone, and it can be obtained from /etc/localtime. In practice /etc/localtime points to a time zone under the zoneinfo directory. Below is the result on MacOS; the path on Linux will be different:

1
2
3
ls -al  /etc/localtime

lrwxr-xr-x  1 root  wheel  39 Apr 26  2021 /etc/localtime -> /var/db/timezone/zoneinfo/Asia/Shanghai

2. Time and Serialization in Go

2.1 How Go Initializes the Time Zone

  1. Look up the TZ variable to get the time zone
  2. If there is no TZ, then use /etc/localtime
  3. If TZ="", then use UTC
  4. When TZ=“foo” or TZ=":foo", if the file that foo points to will be used to initialize the time zone, otherwise use /usr/share/zoneinfo/foo

Here is the source code of the Go implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
tz, ok := syscall.Getenv("TZ")
switch {
case !ok:
	z, err := loadLocation("localtime", []string{"/etc"})
	if err == nil {
		localLoc = *z
		localLoc.name = "Local"
		return
	}
case tz != "":
	if tz[0] == ':' {
		tz = tz[1:]
	}
	if tz != "" && tz[0] == '/' {
		if z, err := loadLocation(tz, []string{""}); err == nil {
			localLoc = *z
			if tz == "/etc/localtime" {
				localLoc.name = "Local"
			} else {
				localLoc.name = tz
			}
			return
		}
	} else if tz != "" && tz != "UTC" {
		if z, err := loadLocation(tz, zoneSources); err == nil {
			localLoc = *z
			return
		}
	}
}

2.2 Serializing a Time Field in Go

In Go you can serialize a Time field with “encoding/json”, and use Format to customize the time format. Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"encoding/json"
	"fmt"
	"time"
)

func main(){
	fmt.Println(time.Now())
	var a, _ := json.Marshal(time.Now())
	fmt.Println(string(a))
	a, _ = json.Marshal(time.Now().Format(time.RFC1123))
	fmt.Println(string(a))
	a, _ = json.Marshal(time.Now().Format("06-01-02"))
	fmt.Println(string(a))
}

Output:

1
2
3
4
5
2021-12-07 16:44:44.874809 +0800 CST m=+0.000070010
"2021-12-07T16:44:44.874937+08:00"
"Tue, 07 Dec 2021 16:44:44 CST"
"00-120-74 16:44:07"
"21-12-07"

2.3 Serializing a Time Field in a Go Struct

In a struct, if you serialize the struct directly with “encoding/json”, you get a time format like this: 2021-12-07T17:31:08.811045+08:00. There is no way to control the time format with the Format function.

So how do you control the time format inside a struct? See the following example:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package main

import (
	"fmt"
	"strings"
	"time"
	"unsafe"
	"encoding/json"

	jsoniter "github.com/json-iterator/go"
)

func main() {
	var json2 = NewJsonTime()
	var d = struct {
		Title string `json:"title"`
		StartedAt time.Time `json:"time"`
	}{
		Title: "this is title",
		StartedAt: time.Now(),
	}
	t1, _ := json.Marshal(d)
	fmt.Println(string(t1))
	t2, _ := json2.Marshal(d)
	fmt.Println(string(t2))
}

func NewJsonTime() jsoniter.API {
	var jt = jsoniter.ConfigCompatibleWithStandardLibrary
	jt.RegisterExtension(&CustomTimeExtension{})
	return jt
}

type CustomTimeExtension struct {
	jsoniter.DummyExtension
}

func (extension *CustomTimeExtension) UpdateStructDescriptor(structDescriptor *jsoniter.StructDescriptor) {
	for _, binding := range structDescriptor.Fields {
		var typeErr error
		var isPtr bool
		name := strings.ToLower(binding.Field.Name())
		if name == "startedat" {
			isPtr = false
		} else if name == "finishedat" {
			isPtr = false
		} else {
			continue
		}

		timeFormat := time.RFC1123Z
		locale, _ := time.LoadLocation("Asia/Shanghai")

		binding.Encoder = &funcEncoder{fun: func(ptr unsafe.Pointer, stream *jsoniter.Stream) {
			if typeErr != nil {
				stream.Error = typeErr
				return
			}

			var tp *time.Time
			if isPtr {
				tpp := (**time.Time)(ptr)
				tp = *(tpp)
			} else {
				tp = (*time.Time)(ptr)
			}

			if tp != nil {
				lt := tp.In(locale)
				str := lt.Format(timeFormat)
				stream.WriteString(str)
			} else {
				stream.Write([]byte("null"))
			}
		}}
		binding.Decoder = &funcDecoder{fun: func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
			if typeErr != nil {
				iter.Error = typeErr
				return
			}

			str := iter.ReadString()
			var t *time.Time
			if str != "" {
				var err error
				tmp, err := time.ParseInLocation(timeFormat, str, locale)
				if err != nil {
					iter.Error = err
					return
				}
				t = &tmp
			} else {
				t = nil
			}

			if isPtr {
				tpp := (**time.Time)(ptr)
				*tpp = t
			} else {
				tp := (*time.Time)(ptr)
				if tp != nil && t != nil {
					*tp = *t
				}
			}
		}}
	}
}

type funcDecoder struct {
	fun jsoniter.DecoderFunc
}

func (decoder *funcDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
	decoder.fun(ptr, iter)
}

type funcEncoder struct {
	fun         jsoniter.EncoderFunc
	isEmptyFunc func(ptr unsafe.Pointer) bool
}

func (encoder *funcEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
	encoder.fun(ptr, stream)
}

func (encoder *funcEncoder) IsEmpty(ptr unsafe.Pointer) bool {
	if encoder.isEmptyFunc == nil {
		return false
	}
	return encoder.isEmptyFunc(ptr)
}

Output:

1
2
{"title":"this is title","time":"2021-12-07T17:31:08.811045+08:00"}
{"title":"this is title","time":"Tue, 07 Dec 2021 17:31:08 +0800"}

Here the main tool is the “github.com/json-iterator/go” package, which controls how Go serializes time fields. Through the extension it provides, you target the time fields whose keys are startedat and finishedat, and specify the format timeFormat := time.RFC1123Z and the time zone locale, _ := time.LoadLocation("Asia/Shanghai") for serialization.

3. Setting the Time Zone in Various Environments

3.1 On Linux

Run the command:

1
timedatectl set-timezone Asia/Shanghai

Or set the TZ environment variable:

1
2
TZ='Asia/Shanghai'
export TZ

Either one sets the time zone.

3.1 In Docker

When building an image, setting the TZ variable directly in the Dockerfile may run into problems:

1
2
3
FROM alpine
ENV TZ='Asia/Shanghai'
COPY ./time.go .

The error: panic: time: missing Location in call to Time.In

The reason: the Linux distributions we commonly use, such as Ubuntu and CentOS, store the various time zones under the /usr/share/zoneinfo/ directory, while the alpine image does not.

So the alpine image needs some extra packages installed.

1
2
3
4
5
FROM alpine

RUN apk add tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

When running the container, you can simply mount the host’s time zone description file:

1
docker run -it --rm -v /etc/localtime:/etc/localtime:ro nginx

3.2 In Kubernetes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
  name: test
  namespace: default
spec:
  restartPolicy: OnFailure
  containers:
    - name: nginx
      image: nginx-test
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - name: date-config
          mountPath: /etc/localtime
      command: ["sleep", "60000"]
  volumes:
    - name: date-config
      hostPath:
        path: /etc/localtime

Here the host’s time zone file is mounted into the Pod.

4. References


微信公众号
WRITTEN BY
微信公众号