15 Go Subtleties You May Not Already Know

32 points by fs111


bbrks

If using the time.After example - ensure you are running Go 1.23 and have 1.23 in your mod file to opt-in to the new timer behaviour.

https://go.dev/wiki/Go123Timer

This example used to leak a goroutine in the happy-path case where the timeout did not fire - the old fix was something like the following (or deferring the Stop() if you had a function to exit out of), but 1.23 managed to close up this longstanding foot-gun by allowing the unfired timer to be GC'd

timeout := time.NewTimer(time.Second)
select {
case res := <-ch:
	fmt.Println("Received:", res)
	if !timeout.Stop() {
		<-timeout.C
	}
case <-timeout.C:
	fmt.Println("Timeout: did not receive a result in time")
}
coxley

Great tips! If anyone is interested, I've added some commentary of my own below based on a ~decade of Go.

Constraining Generic Function Signatures

You can kind of do structural typing with this, in so much as the structure exactly matches. It's useful for packages that allow callers to provide their own types based off a core one.

Abbreviated example from https://github.com/orsinium-labs/enum:

// Package
type Member[T comparable] struct {
	Value T
}

type Enum[M ~struct{ Value V }, V comparable] struct {
	members []M
}

func New[M ~struct{ Value V }, V comparable](members ...M) Enum[M, V] {
	return Enum[M, V]{members}
}

// Usage
type Color Member[string]

var (
	Blue   = Color{"blue"}
	Red    = Color{"red"}
	Colors = New(Blue, Red)
)

Index-based String Interpolation

Another interpolation trick I like is using %T in type-switch default cases. Makes debugging easier, and it's easy to copy-paste around:

switch v := thing.(type) {
case Foo:
case Bar:
default:
  return fmt.Errorf("unexpected type: %T", v)
}

Comparing Times

There are two things with time in Go that I constantly trip over:

  1. IsZero()
  2. Unittests on Apple Silicon laptops

t.IsZero() returns true if t represents 0001-01-01T00:00:00Z. This means any system that deals with encoding/decoding UNIX timestamps, where 0 is the natural zero value, the naive check will fail.

ts1 := time.Unix(0, 0)
fmt.Println("1:", ts1.IsZero())
fmt.Println("1:", ts1.Unix() == 0)

// Unfortunately that constant is unexported, but it is the
// amount of seconds between 0001-01-01T00:00:00Z and
// 1970-01-01T00:00:00Z
ts2 := time.Unix(-62135596800, 0)
fmt.Println("2:", ts2.IsZero())

var ts3 time.Time
fmt.Println("3:", ts3.IsZero())
fmt.Println("3:", ts3.Unix())

// Output:
// 1: false
// 1: true
// 2: true
// 3: true
// 3: -62135596800

For Apple Silicon laptops, their monotonic clock only has microsecond-precision. This can make tests that rely on time equality to pass locally, but fail on other systems.

now := time.Now()
fmt.Printf("%d.%d\n", now.Unix(), now.Nanosecond())
fmt.Println("ns:", now.Nanosecond()/1e9)

now = time.Now()
fmt.Printf("%d.%d\n", now.Unix(), now.Nanosecond())
fmt.Println("ns:", now.Nanosecond()/1e9)

// Output:
// 1761143594.202720000
// ns: 0
// 1761143594.202744000
// ns: 0