28

I'm trying to modify golang timezone for my application

I have took a look at time package, initializing timezone happens in

time/zoneinfo_unix.go @ initLocal

The function simply tries to read environment variable TZ and if it's valid it loads it

and if it's not it falls back /etc/localtime and if it's not valid it falls back to UTC


what i have tried so far

1- works fine -But i don't want to use either of those approaches - :

  • in my docker file i pass an ENV to the container, TZ = Africa/Cairo
  • getting into the container bash, running $ export TZ = Africa/Cairo

2- Didn't work

  • in my app initialization (the app initialization is in a separate package that is being imported in the main), i use os.SetEnv("TZ", "Africa/Cairo")

When i simplify the main and use os.SetEnv("TZ", "Africa/Cairo") without importing any other packages other than "os - time" it works as expected


Any ideas about how to make the second approache work ?

Docker image: golang:1.11.2

Abdelrahman Magraby
  • 1,053
  • 1
  • 12
  • 16
  • Calling `os.Setenv()` first thing in `main()` seems to work, see here: https://play.golang.org/p/pckEDxd6p8F. – icza Jan 25 '19 at 10:36
  • Thanks for your response, I tried this out and it works only when the main is very simple as the example you provided, but in my case the main is not that simple and it uses a external package for app initialization – Abdelrahman Magraby Jan 25 '19 at 10:47
  • Then put it in an `init()` func in the main pkg? And just to add my two p's - it seems perfectly fine to me to set the env var in the container... – Havelock Jan 25 '19 at 11:08
  • 2
    @Havelock That wouldn't work if the `main` package imports another package which uses the `time` package, that would be executed before the main's `init()` could run. – icza Jan 25 '19 at 11:09
  • 1
    Since you're referring to docker I'm guessing this is a service and not a CLI tool. If you care what the default time zone is, consider that a defect and rework your design such that it is system-TZ-agnostic. Services should accept inputs in any timezone, handle them in UTC, store them in UTC, and render them in any timezone. System timezone should be wholly irrelevant. – Adrian Jan 25 '19 at 15:27

4 Answers4

34

You can achieve what you want from inside your app using os.Setenv("TZ", "Africa/Cairo"), what matters is that you must call this before any other package uses anything from the time package.

How to ensure that? Create a package that does nothing else except sets the timezone (later you may add other things to it, but for our example that's enough).

Like this:

package tzinit

import (
    "os"
)

func init() {
    os.Setenv("TZ", "Africa/Cairo")
}

Import this tzinit package first thing in your main package like this:

package main

import _ "path/to/tzinit"

// Your other, "regular" imports:
import (
    "fmt"
    "os"
    "time"
    ...
)

And so setting the TZ env var will happen before any other package could access the time package.

Note that I used a separate import declaration just for tzinit, and the reason for this is because many code editors / IDEs will rearrange your imports alphabetically, this will ensure that importing tzinit will remain the first import.

A word of warning.

The Spec: Package initialization states the requirements and rules of initializing packages, and the order in which imports are processed is not specified (only thing guaranteed is that all referenced package will be initialized recursively before it can be used). This means that although current compilers process them as listed, you cannot rely on this for 100%. There's also the issue of having multiple source files even for the main package, supplying them in different order to the compiler may also change the initialization order. The spec has this as a "recommendation":

To ensure reproducible initialization behavior, build systems are encouraged to present multiple files belonging to the same package in lexical file name order to a compiler.

So to be on the safe side, best would be to set the TZ environment variable before the Go app is launched.

icza
  • 389,944
  • 63
  • 907
  • 827
  • Thanks a lot for your answer – Abdelrahman Magraby Jan 25 '19 at 11:48
  • You cannot rely on the order of the import statements. The import order is for the package as a whole, rather than the order in a single file. (Actually they are currently in order based on the parse order of the package files, but I don't think that order is specified) – JimB Jan 25 '19 at 18:42
  • @JimB Yes, wanted to add that as a warning, but didn't have time for that. Doing it now. – icza Jan 25 '19 at 20:29
  • I was having problems with a Go app not getting the timezone info right, and it turns out there was a "tzdata" package missing. Adding that package and setting the TZ variable could help solve the problem. – user87312 Jun 02 '19 at 11:00
  • Setting the TZ environment variable through a .env file which is used by docker compose did the trick for me - thanks! The docker container where I have my golang code running in is using Debian as base image – Kurt Oct 13 '22 at 12:30
30

Adding my anser here for people who stumbled on this page. There's a global variable in time package, use it like this in main.go

package main

import "time"

func main() {
    loc, err := time.LoadLocation("Africa/Cairo")
    // handle err
    time.Local = loc // -> this is setting the global timezone
}

Your system must have timezone database installed. In docker, you must apt get/apk add tzdata. But if you're using go1.15, you can also embed the timezone database without installing tzdata on system.

package main

import (
    "time"
    _ "time/tzdata"
)

func main() {
    loc, err := time.LoadLocation("Africa/Cairo")
    // handle err
    time.Local = loc // -> this is setting the global timezone
}
yeka
  • 341
  • 3
  • 2
  • 1
    This is recommended if you prefer to change timezone settings programmatically instead of changing environment variables. – Michael Dec 23 '20 at 04:13
  • 3
    But this may lead to data races with some `time.*` functions (I get them sometimes in my tests despite I change location in `init()` function). – zored Jul 04 '21 at 10:46
  • 2
    yes, this may lead to data races --> https://github.com/golang/go/issues/34814#issuecomment-540596197 – joelazar Jul 28 '21 at 10:43
3

I may be late but setting timezone in a global env is not a reliable approach. It should be set globally in a variable or in a struct. The below is an example of timezone set in a variable. Also in Go Playground

package main

import (
    "fmt"
    "log"
    "time"
)

func main() {
    if err := setTimezone("America/Los_Angeles"); err != nil {
        log.Fatal(err) // most likely timezone not loaded in Docker OS
    }
    t := getTime(time.Now())
    fmt.Println(t)
}

var loc *time.Location

func setTimezone(tz string) error {
    location, err := time.LoadLocation(tz)
    if err != nil {
        return err
    }
    loc = location
    return nil
}

func getTime(t time.Time) time.Time {
    return t.In(loc)
}
Dan Esparza
  • 28,047
  • 29
  • 99
  • 127
Taku
  • 5,639
  • 2
  • 42
  • 31
1

Be aware that if you want to deploy your app on AWS time.LoadLocation may return an error because it can't find database file. From the docs:

LoadLocation looks for the IANA Time Zone database in the following locations in order:

  • the directory or uncompressed zip file named by the ZONEINFO environment variable
  • on a Unix system, the system standard installation location
  • $GOROOT/lib/time/zoneinfo.zip
  • the time/tzdata package, if it was imported

You should import time/tzdata package to resolve the issue:

import _ "time/tzdata" // Important!

func main() {
    location, err := time.LoadLocation("Europe/Berlin")
    if err != nil {
        log.Fatal().Msgf("Err loading location: %v", err)
    }
    time.Local = location
}
joeyave
  • 316
  • 3
  • 11