Home     Interfacing     Software     About     Sitemap   
 EN   
Quick links

Time (to come) and Go

When you’re in the embedded development world, time is an important issue. Some processes may need a real-time response. When logging data from systems, it is important that the timestamp is consistent to make problem solving later easier. For some programs it is necessary to accurately determine the execution time of a script. Time is so important in many of my projects that I run a network of publicly accessible NTP-time servers with one server synchronized through GPS with an accuracy of a few microseconds.

In C, the implementation of time is a mess. Time in the time_t type is originally defined as the number of seconds since Jan. 1st 1970, stored in a 32 bit signed integer. Two billion seconds sounds as a lot, but the counter actually wraps around in January 2038, which is just a handful years away from now. I can understand that the developers of C never thought that their language would still be around more than sixty years after its birth, but I am quite sure we have reached that date before many 32 bit implementations of C have died.

The time package in Go

Go was developed after the Y2K bug, and the basic error of representing time in just 32 bits was avoided. Time is in Go implemented in the time package. Importing this package in a program gives access to several types of time functions for different purposes.

Wall Clock

Time is not just one simple thing. We have to distinguish between absolute time and relative time. Absolute time is a time representation which can be compared with time in the real world. This is often referred to as a Wall Clock. Relative time is a time compared with another time inside the application. Relative times are used for delays, timeouts, calculating the execution time of a routine etc.

While the wall clock represented by the system clock seems a straight forward way to represent time, that is not always the case in practical situations. The value of the wall clock depends on the location on the earth, and the way the system clock is synchronized with the outside world. Different time zones have different time representations for the same moment, and the use of daylight savings time (DST) in a number of countries creates holes in the time wall clock time representation when the time advances in the spring, and duplicate times when the clock is set back in the autumn. That can cause interesting problems when the wall clock is used for timestamps in a log file for example. Besides that, the DST behavior is not fixed for a specific timezone or country, but varies depending on political decisions.

To circumvent these wall clock problems, computers often run their system clock in a standardized clock which is timezone and DST independent, and use offset calculations to display the time to the users. This standardized clock is UTC, or Coordinated Universal Time. You may have noticed that UTC is not the straightforward abbreviation of “coordinated universal time”. That’s because the abbreviation UTC is the result as a compromise, just as the definition of this standardized time itself. The French speaking world proposed “temps universel coordonné” which abbreviates to TUC which was different from the English CUT. UTC was accepted as a compromise because it didn’t favor any particular language. UTC practically represents the at Greenwich, although it should not be confused with GMT, which is a timezone which happens to have the same time as UTC.

Monotonic Time

Wall clocks running in UTC can be used to compare time inside a Go program with external time, but it may not be the best time to do internal time referencing for delays or execution time calculations. The reason is that not all system clocks are continuously synchronized with an external reference clock. For systems where a protocol like NTP is used for synchronization, the system clock may be save to use for relative time inside an application. But if a less accurate protocol is used like NTP, or time is periodically manually set by a system operator, steps in time (both forward and backward) may occur which causes the relative time to become unreliable, or may even cause deadlock or crashes in an application.

To prevent program errors to happen when the wall clock jumps forward or backward, a monotonic clock should be used when calculating relative time. Monotonic clocks often provide a relative time not directly related with the wall clock, but which is guaranteed to count only in the positive time direction, never backwards. Monotonic clocks can be used for measuring time, rather than telling the time.

The function time.Now() and the Time type

The function time.Now() is the base for both wall clock and monotonic clock use. This may sound strange, but internally the time.Now() function not only represents an absolute time related to the system clock, but also contains a hidden monotonic clock which can be used for calculations. This monotonic clock was not available from the beginning. It was only implemented in Go version 1.9 in February 2017 after a lengthy discussion and multiple requests in the community.

time.Now() returns a value of type Time. This type is a structure with hidden elements. A a C programmer this kind of data hiding is a little bit awkward, because in a procedural language like C you know everything about any system data structure, but in an object oriented approach like in Go, it makes perfectly sense. It hides the specifics of the implementation of time. For conversions to use either the wall clock or monotonic component, additional methods in the time package are available. The time representation in the Time type has a nanosecond resolution.

Check if times are equal

The Go comparison operator == can be used to check if two time representations of the type Time are equal. This comes however with a catch. The == operator only identifies two time representations to be equal, of both the wall clock and monotonic clock component of the times are equal. IT must also be assured that the timezone component of both times is equivalent. Therefore, although possible, it is better to avoid the == operator completely when comparing times. It is better to use the time.Before(), time.After() and time.Equal() methods defined in the time package. These methods are much better in handling times with different local representations or where the monotonic representations of the times differ. In an example:

package main

// Copyright (C) 2021 - Lammert Bies
// Distributed under terms of the MIT license.

import (
        "fmt"
        "time"
)

func main() {
        var oper_str string
        var equal_str string

        t1 := time.Now()
        t2 := t1.UTC()

        fmt.Printf( "Local time: %s\nUTC time  : %s\n\n", t1, t2 )

        if t1 == t2 {
                oper_str = ""
        } else {
                oper_str = "un"
        }

        fmt.Printf( "The times are %sequal according to the == operator\n", oper_str )

        if t1.Equal(t2) {
                equal_str = ""
        } else {
                equal_str = "un"
        }

        fmt.Printf( "The times are %sequal according to the Equal method\n", equal_str )

 }  /* main */

When running this program on my development system, it created the following output:

Local time: 2021-03-26 23:36:44.013964118 +0100 CET m=+0.000048471
UTC time  : 2021-03-26 22:36:44.013964118 +0000 UTC

The times are unequal according to the == operator
The times are equal according to the Equal() method

The Go program requests the current local time and stores it in variable t1. This time variable contains both the wall clock and the monotonic time represented as the time elapsed since the program was started. The monotonic time value is visible as the m=+0.000048471 component in the first printed line. The variable t2 is the same time, but now converted to UTC. During this conversion, the monotonic component is reset. When comparing the two times with the == operator, they are considered different, but the time.Equal() method determines that both times are the same, even though they have different timezone representations and contain different monotonic clock values.

The Duration type

The time.Time type is used to define a moment with some absolute value like the time in a given timezone or the elapsed time since a predefined moment like the start of a program. When we want measure time relative to some other arbitrary time, Go uses the time.Duration type instead. The Duration type is the number of nanoseconds between two timestamps represented as an int64. The implementation in a 64 bit integer is sufficient for most practical use cases for relative time calculations. Any Go process needing more time than this to execute, or any timeout or delay functionality in a routine larger than the what can be represented in a 64 bit integer makes no practical sense as it will probably outlive the programmer and end-user.

A large number of methods are available to use and manipulate instances of the time.Duration type. I won’t go through them in detail here, but use some of them in an example program to give an idea how they can be used. The other available methods are quite similar and can be found in the Go language specification.

package main

// Copyright (C) 2021 - Lammert Bies
// Distributed under terms of the MIT license.

import (
        "bufio"
        "fmt"
        "os"
        "strings"
        "time"
)

func main() {

        start_time := time.Now()
        period_rdr := bufio.NewReader( os.Stdin )

        fmt.Printf( "Enter how long we should wait: " )
        period_str, read_err := period_rdr.ReadString( '\n' )
        if ( read_err != nil ) {

                fmt.Printf( "An error occured reading input:\n%s\n", read_err )
                return
        }

        period_str         = strings.Replace( period_str, "\n", "", -1 )
        period, parse_err := time.ParseDuration( period_str )
        if parse_err != nil {

                fmt.Printf( "Something went wrong:\n%s\n", parse_err )
                return
        }

        if period > 10*time.Second {

                fmt.Printf( "Life is too short to wait %s\n", period )
                return
        }

        time.Sleep( period )

        fmt.Printf( "We have waited %s and %s passed since program start\n", period, time.Since( start_time ) )

 }  /* main */

The first thing we do during execution of the program is storing the starting time. We use buffered I/O to read a string from the stdin stream. We define the newline character \n as the end of the input string and return all entered characters including the newline character back to the program. This is where we see one of the differences between C and Go. When using data conversion functions in C like the atoi() function, the data conversion continues until characters are encountered which are not recognized. The intermediate result is then returned. Go is different in that anything which doesn’t match what the conversion expects throws an error. Before feeding the input string to the time.ParseDuration() function, we have to strip the newline character from the input string.

There still may be invalid data entered on the command line which causes the time.ParseDuration() function to reject the input. The function therefore returns two values, the string converted to a time.Duration type, and an error string if the conversion did not succeed. If the conversion succeeds and the period is not excessive, the program pauses for the entered time and exits, displaying the waiting time and the total execution time.

Go is very flexible in accepting times and is able to convert combinations of hours, minutes etc. to a meaningful duration. Below are two examples of the test run of this Go program on my development computer

$ go run duration.go
 Enter how long we should wait: 2us
 We have waited 2µs and 1.996647345s passed since program start

 $ go run duration.go
 Enter how long we should wait: 4s932ms002us12ns
 We have waited 4.932002012s and 24.665550235s passed since program start

You see, wire telegraph is a kind of a very, very long cat.
You pull his tail in New York and his head is meowing in Los Angeles.
Do you understand this? And radio operates exactly the same way:
you send signals here, they receive them there.
The only difference is that there is no cat.
ALBERT EINSTEIN
  Mar. 2021
   Copyright © 1997-2021 Lammert Bies, All rights reserved