Hey Gophers,

It’s been a while. Thanks for sticking around 🙂

This week, Go has a new way to find goroutines that get stuck. We’ll look at how that happens with a small example, then fix it. There are also a few good reads on memory, generics, and replacing C code with Go.

Warm-up Time

1. Small allocations get a faster path in Go 1.27
Michael Matloob · Go blog · 16 September

When Go sets aside memory for a value, that’s called an allocation. Go 1.27 makes this step faster for small values by using code built for particular sizes.

The Go team reports up to 20–30% faster allocations. Across a whole program that allocates a lot of memory, the gain is up to about 1%. You get this change by building with Go 1.27. Michael’s post explains where the improvement comes from and why the team focused on small values.

2. Replacing Debian Code Search’s last cgo dependency
Michael Stapelberg · 6 September

Michael’s Debian Code Search project used a C library to compress lists of numbers. He replaced it with Go, removing the project’s last need for cgo, the bridge that lets Go call C code.

The post follows each change and measures what it saves. One step uses SIMD, which lets a CPU work on several numbers in one instruction. That part uses experimental Go features and needs a suitable CPU. A good longer read if you enjoy seeing how someone finds and removes a performance bottleneck.

3. Generic methods have arrived
Mark Freeman · Go blog · 26 August · release catch-up

Imagine a list of users. Sometimes you want their names as strings. Other times you want their numeric IDs. It would be handy to use the same method for both conversions.

Go 1.27 lets a method declare its own type parameter, a placeholder for a type that you choose when using it. The same method can then produce a list of strings in one call and a list of numbers in another.

This works for methods on types such as structs. Methods declared in an interface still cannot have their own type parameters. Mark builds up the idea with a list example.

Tool Time

Tests can pass while leaving a goroutine running in the background. This established library checks for unexpected goroutines left over when your tests finish.

If your tests use t.Parallel, follow the project’s goleak.VerifyTestMain example to check the whole test package together. It’s a useful companion to this week’s deep dive: catch the problem in a test before it reaches your service.

Solod takes a subset of Go and turns it into C. Anton’s latest release adds a tool that generates the code needed to call C libraries, along with better support for testing.

You manage memory yourself, without Go’s usual runtime and automatic memory cleanup. An interesting project if you’re curious about using Go-style code with C libraries.

Community watch

An easier way to build with C libraries
Proposal under discussion · Michael Matloob · 10 September

Using a Go package that calls a C library can mean installing a C compiler just to build your app. Michael proposes skipping that step when the C library is already compiled. Package authors would provide a file describing how Go should call it. You would still need the C library itself.

A change to add an experimental build option is now in code review. The proposal itself is still unapproved, with no confirmed release version. You can follow the community discussion on r/golang.

Deep Dive

The request finished. The goroutine didn’t.

Imagine a user asks your server to build a report. The request handler starts a goroutine to do the work. Then an error happens, and the handler returns before the report is ready.

A goroutine runs a function independently from the code that started it. The go keyword starts one. Returning from the handler does not stop that goroutine. It can still finish the report and try to send it back.

Here’s a small version of the problem. We’ve left out the report-building work so we can focus on sending the result:

func handleRequest() {
    results := make(chan string)
    go func() {
        results <- "report ready"
    }()
    return // the request ends without reading results
}

Why does the worker get stuck?

A channel lets goroutines pass values to each other. The channel above has no space to hold a value while it waits to be read. This is called an unbuffered channel.

So the line results <- "report ready" waits until another goroutine is ready to receive the value.

But our handler has returned. Nothing else has access to this channel, and nobody will read from it. The worker stays on that send line for as long as the server keeps running.

That’s a goroutine leak. The worker cannot finish, but it still takes up memory. If more requests follow the same path, more stuck workers build up.

A small fix for this example

We know this worker sends exactly one result. Change the channel creation line to give it room to hold that result:

results := make(chan string, 1)

The 1 creates a buffer with space for one value. The worker puts its result there and finishes, even if the handler has already returned. Once nothing can reach the channel, Go can clean up the channel and its unused result.

The size matters. If the worker sends a second result into that full buffer, it gets stuck again. This fix fits our example because there is one worker and one send.

For work that keeps running, you may also need a way to tell the worker to stop. Go’s context package provides a cancellation signal for that. The worker has to check the signal and return; cancellation does not stop it automatically.

Finding the same bug in a real service

In these few lines, the missing receive is easy to spot. In a larger service, the code that starts the work and the code that waits for its result may be far apart.

Go 1.27 adds a goroutine leak profile to pprof, Go’s profiling tools. A profile is a report about a running program. This new one points to goroutines that Go can identify as permanently stuck.

If your service already uses net/http/pprof, the report is available at /debug/pprof/goroutineleak. For a private debug server at 127.0.0.1:6060, use:

curl -fsS -o leaks.pprof \
  http://127.0.0.1:6060/debug/pprof/goroutineleak
go tool pprof leaks.pprof

The first command saves the report. The second opens it. At the pprof prompt, list handleRequest shows the relevant lines of our function and where the worker is stuck.

If you haven’t set up pprof before, Vlad’s Go blog post includes a complete program you can run. Keep that debug server private because it exposes details of your running app.

The report has limits. It doesn’t cover goroutines waiting on files or network connections. It can also miss a stuck channel send if other code still has access to the channel, for example through a global variable. Go has to allow for the possibility that this code might use it.

So an empty report doesn’t prove there are no leaks. Collecting it also adds work to Go’s garbage collector, which cleans up unused memory. Use it when investigating a problem; avoid collecting it on every request.

Watch: how goroutine leaks show up in real services

For more on this problem, here’s Vlad Saioc’s 2024 talk about goroutine leaks in real services. It gives background to the work behind today’s debugging tools. It predates Go 1.27, so expect the research story rather than a tutorial on the new profile. The conference page also links the paper.

Did the channel example make sense? Hit reply if there’s a step you’d like me to explain further. I’d also like to know which helped most: the diagram, the code, or the links.

Thanks for reading! If this was useful, forward it to a fellow Gopher.

Moein

Reply

Avatar

or to participate