<aside> 💡

Go 1.25

Go 1.25 interactive tour

JSON evolution in Go: from v1 to v2

Green Tea GC: How Go Stopped Wasting 35% of Your CPU Cycles

</aside>

Go 1.25 is out, so it's a good time to explore what's new. The official release notes are pretty dry, so I prepared an interactive version with lots of examples showing what has changed and what the new behavior is.

Read on and see!

synctestjson/v2GOMAXPROCSNew GCAnti-CSRFWaitGroup.GoFlightRecorderos.Rootreflect.TypeAssertT.Attrslog.GroupAttrshash.Cloner

This article is based on the official release notes from The Go Authors, licensed under the BSD-3-Clause license. This is not an exhaustive list; see the official release notes for that.

I provide links to the proposals (𝗣) and commits (𝗖𝗟) for the features described. Check them out for motivation and implementation details.

Error handling is often skipped to keep things simple. Don't do this in production ツ

Synthetic time for testing

Suppose we have a function that waits for a value from a channel for one minute, then times out:

// Read reads a value from the input channel and returns it.
// Timeouts after 60 seconds.
func Read(in chan int) (int, error) {
    select {
    case v := <-in:
        return v, nil
    case <-time.After(60 * time.Second):
        return 0, errors.New("timeout")
    }
}

We use it like this:

func main() {
    ch := make(chan int)
    go func() {
        ch <- 42
    }()
    val, err := Read(ch)
    fmt.Printf("val=%v, err=%v\\n", val, err)
}

How do we test the timeout situation? Surely we don't want the test to actually wait 60 seconds. We could make the timeout a parameter (we probably should), but let's say we prefer not to.

The new synctest package to the rescue! The synctest.Test function executes an isolated "bubble". Within the bubble, time package functions use a fake clock, allowing our test to pass instantly:

func TestReadTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        ch := make(chan int)
        _, err := Read(ch)
        if err == nil {
            t.Fatal("expected timeout error, got nil")
        }
    })
}

The initial time in the bubble is midnight UTC 2000-01-01. Time advances when every goroutine in the bubble is blocked. In our example, when the only goroutine is blocked on select in Read, the bubble's clock advances 60 seconds, triggering the timeout case.

Keep in mind that the t passed to the inner function isn't exactly the usual testing.T. In particular, you should never call T.Run, T.Parallel, or T.Deadline on it:

func TestSubtest(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        t.Run("subtest", func (t *testing.T) {
            t.Log("ok")
        })
    })
}

So, no inner tests inside the bubble.

Another useful function is synctest.Wait. It waits for all goroutines in the bubble to block, then resumes execution: