go: time: Round(0), Truncate(0) strip monotonic clock readings but documentation still says it returns t unchanged

Please answer these questions before submitting your issue. Thanks!

What version of Go are you using (go version)?

go version go1.9rc2 darwin/amd64

this does not happen, for contrast, with go1.8.3 darwin/amd64

This change is documented at the top of the new go1.9rc2 time/time.go file:

// For debugging, the result of t.String does include the monotonic                                             
// clock reading if present. If t != u because of different monotonic clock readings,                           
// that difference will be visible when printing t.String() and u.String().                                     

This is actually a rather drastic proposed change. I refer not the use of the monotone clock, but the display of it in timestamps formated via String(). Many of us use String() for many purposes other than debugging. For debugging, a separate new StringWithMonotone() should be provided.

What operating system and processor architecture are you using (go env)?

go env: GOARCH=“amd64” GOBIN=“” GOEXE=“” GOHOSTARCH=“amd64” GOHOSTOS=“darwin” GOOS=“darwin” GOPATH=“/Users/jaten/go” GORACE=“” GOROOT=“/usr/local/go1.9rc2” GOTOOLDIR=“/usr/local/go1.9rc2/pkg/tool/darwin_amd64” GCCGO=“gccgo” CC=“clang” GOGCCFLAGS=“-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/6s/zdc0hvvx7kqcglg5yqm3kl4r0000gn/T/go-build055854381=/tmp/go-build -gno-record-gcc-switches -fno-common” CXX=“clang++” CGO_ENABLED=“1” CGO_CFLAGS=“-g -O2” CGO_CPPFLAGS=“” CGO_CXXFLAGS=“-g -O2” CGO_FFLAGS=“-g -O2” CGO_LDFLAGS=“-g -O2” PKG_CONFIG=“pkg-config”

What did you do?

Here code to reproduce the issue, serialize_time.go. It is extracted from the time.Time serialization in a popular msgpack serialization package. e.g. getunix() and putunix() are here https://github.com/tinylib/msgp/blob/ad0ff2e232ad2e37faf67087fb24bf8d04a8ce20/msgp/integers.go#L113

package main

import (
	"fmt"
	"time"
)

func main() {

	// time to bytes
	t := time.Now()
	fmt.Printf("original  t='%s'\n", t.String())
	t = t.UTC()
	fmt.Printf("original  t='%s' in UTC\n", t.String())
	var b [12]byte
	putUnix(b[:], t.Unix(), int32(t.Nanosecond()))

	// bytes to time:
	sec, nsec := getUnix(b[:])
	t2 := time.Unix(sec, int64(nsec)).Local()

	fmt.Printf("restored t2='%s'\n", t2.String())
}

/* output of main:

original  t='2017-08-16 19:35:41.958759249 -0400 EDT m=+0.000241395'
original  t='2017-08-16 23:35:41.958759249 +0000 UTC' in UTC
restored t2='2017-08-16 19:35:41.958759249 -0400 EDT'

*/

func getUnix(b []byte) (sec int64, nsec int32) {
	sec = (int64(b[0]) << 56) | (int64(b[1]) << 48) |
		(int64(b[2]) << 40) | (int64(b[3]) << 32) |
		(int64(b[4]) << 24) | (int64(b[5]) << 16) |
		(int64(b[6]) << 8) | (int64(b[7]))

	nsec = (int32(b[8]) << 24) | (int32(b[9]) << 16) | (int32(b[10]) << 8) | (int32(b[11]))
	return
}

func putUnix(b []byte, sec int64, nsec int32) {
	b[0] = byte(sec >> 56)
	b[1] = byte(sec >> 48)
	b[2] = byte(sec >> 40)
	b[3] = byte(sec >> 32)
	b[4] = byte(sec >> 24)
	b[5] = byte(sec >> 16)
	b[6] = byte(sec >> 8)
	b[7] = byte(sec)
	b[8] = byte(nsec >> 24)
	b[9] = byte(nsec >> 16)
	b[10] = byte(nsec >> 8)
	b[11] = byte(nsec)
}

In summary, in go1.9rc2, time.Time.String() may produce:

2017-08-16 18:41:39.184829495 -0400 EDT m=+0.057013769

whereas in go1.83, the String() applied to the same value produces always:

2017-08-16 18:41:39.184829495 -0400 EDT

While not a language consistency issue, this is a backwards-compatibility issue for programs that expected one thing from their time.Time values. For example, my tests in a fork of the tinylib/msgp lib cited above are suddenly broken under go1.9rc2. So these changes may break many programs unexpectedly when moving from go1.8.3 to go1.9.

I’m glad for the new monotonic time feature under the covers, as it solves bugs with walltime going through leap seconds, but does it have to surface itself into the string representation of time in a non-backcompatible way? For the new info, how about adding a separate stringification method if one needs both timestamps? Perhaps: StringWithMonotone() alongside the old fashioned String().

In summary, while useful, features meant for debugging the new time.Time values should not break backwards compatibility, when they can trivially be provided alongside in a separate new method.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 20 (11 by maintainers)

Commits related to this issue

Most upvoted comments

This issue of String’s output changing was brought up in #20876, and the decision there is to be more firm that String is intended for human consumption only, in which case there is no guarantees about stability.

As @shurcooL mentioned, MarshalText, MarshalBinary, or Format should be used for stable representations of time.

@dsnet This bug points out two important documentation fixes that should be made. Since you seem in a hurry to close it, would you mind opening a new bug to note the needed doc fixes to time.Round and time.Truncate? Both claim they are unchanged with a zero argument, but that is no longer true in 1.9.

What will happen is this: user sees call to Round(0) in some code they haven’t written themselves. Question occurs to user: what the heck is this Round(0) call doing? It doesn’t make any sense!?! Let’s look at the documentation for Round! User positions cursor over the Round(0) call and presses F12 (or whatever godef is bound to), and jumps to the documentation for Round. Now does the user get a clue? They could, if we help them out here. Or we could just leave them mystified.

I’m not saying remove anything. I’m just saying we should add to the Round documentation that one sentence to explain Round(0); in addition to the preamble stuff.

Come on. Let’s not be stingy with docs. This is crappy API, as rsc agreed. Let’s help all we can.

I see that the go1.9rc2 time API documentation does recommend using Round(0) to strip the mono part; at https://github.com/golang/go/blob/master/src/time/time.go#L46

// The canonical way to strip a monotonic clock reading is to use t = t.Round(0).

However the documentation for Round itself belies this, also claiming at https://github.com/golang/go/blob/master/src/time/time.go#L1403 that

// If d <= 0, Round returns t unchanged.