1. Time and Time Zone

1.1 Time standard

UTC, Universal Time, is the current standard of time and is measured in atomic time.

GMT, Greenwich Mean Time, is the former time standard, which specifies that the sun passes the Royal Greenwich Observatory in the suburbs of London, England, at 12:00 noon each day.

UTC time is more accurate, but if accuracy is not required, the two standards can be considered equivalent.

1.2 Time Zones

From the Greenwich Prime Meridian, one time zone is divided for every 15° interval of longitude to the east or west, so there are 24 time zones in total, 12 to the east and 12 to the west.

However, for administrative convenience, a country or a province is usually divided together. Here are some of the times expressed in UTC:

  • UTC-6 (CST - Central North American Standard Time)
  • UTC+9 (JST - Japan Standard Time)
  • UTC+8 (CT/CST - Central Standard Time)
  • UTC+5:30 (IST - Indian Standard Time)
  • UTC+3 (MSK - Moscow time zone)

1.3 Local time

Local time is the current system time with time zone, which can be obtained from /etc/localtime. In fact /etc/localtime points to a time zone in the zoneinfo directory. Here is the result on MacOS, the path will be different on Linux.

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 time zones

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

Here is the source code for 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 Serialization of Go time fields

The Time field can be serialized in Go using “encoding/json”, and the time format can be customized using Format. Here is an 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 results:

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 Time Fields in Go Structs

If you serialize a structure directly with “encoding/json”, you will get a time format like this: 2021-12-07T17:31:08.811045+08:00. You cannot control the time format with the Format function.

So, how to control the time format in the structure? Please 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 = true
		} 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 results:

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"}

The main purpose here is to use the “github.com/json-iterator/go” package to control Go’s serialization of time fields by specifying the time fields with the keys startedat, finishedat, and the serialization using the timeFormat := time.RFC1123Z format and locale, _ := time.LoadLocation("Asia/Shanghai") time zone.

3. Setting the time zone in various environments

3.1 In Linux

Execute the command:

1
timedatectl set-timezone Asia/Shanghai

Or set the TZ environment variable:

1
2
TZ='Asia/Shanghai'
export TZ

Both can set the time zone.

3.1 In Docker

When creating an image, setting the TZ variable directly in Dockerfile may cause problems.

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

Reason: Our common Linux systems, such as Ubuntu and CentOS, have time zones stored in the /usr/share/zoneinfo/ directory, while the alpine image does not.

Therefore the alpine image needs to install some additional packages.

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 mount the time zone description file of the host directly.

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