Waiting for goroutines

August 6, 2025

Yesterday we looked at some sample code with goroutines:

for i := range 10 {
	go func() {
		fmt.Println(i)
	}()
}

But if you ran it, you probably saw no output. This is because the main goroutine exits before the 10 new goroutines have a chance to run. How do we solve this?

We need some sort of synchronization mechanism in place, to ensure we wait for all goroutines to finish before exiting the program. We’re going to look at severaly ways to do this over the coming days. For now, we’ll look at how to handle this ourselves, without any special packages.

Using a channel

A simple way to do this is to use a channel. We can create a channel that we will use to signal when a goroutine has finished. Here’s how we can modify the code:

done := make(chan struct{})

for i := range 10 {
	go func(i int) {
		fmt.Println(i)
		done <- struct{}{} // Signal that this goroutine is done
	}(i)
}

// Wait for all goroutines to finish
for range 10 {
	<-done
}

Now if you run the code, you should see the numbers 0 through 9 printed in random order, as the main goroutine waits for all the spawned goroutines to finish before exiting.

Let’s look at how this works.

We’ve created a channel called done. The type of the channel doesn’t matter, since we’re using it merely to signal completion. I chose an empty struct (struct{}), as this does a good job of conveying the semantic meaning of a signal-only channel without carrying any data.

We’ve updated the anonymous function called by each new goroutine to send a value to the done channel when it finishes its work. Then the main goroutine runs a separate loop that waits until it has received 10 values (“done” signals) on the done channel before it exits.

Note that technically it’s still possible for one or more goroutines to still be running in when the main goroutine exits. That’s because there may be a small lag between the time the ‘done’ value is sent on the channel, and the time the goroutine is cleaned up and exits. However, in practice, we don’t care—all we care about is that the work of the goroutine is completed before the main goroutine exits.


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Go statements, continued

Now that we’ve talked about what goroutines are and aren’t, lets look at the details of a Go statement: Go statements … The expression must be a function or method call; it cannot be parenthesized. This is important! The expression must be a functino or method call. Not simply the name of a function or method. That is to say, this: go foo() not that: go foo This may feel pretty natural, but it’s easy (at leat for me) to forget sometimes.


Concurrent use of contexts

Overview … The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines. Contexts are built using layers. Every time you call one of the context.With* functions, the orginal context is wrapped in a new layer. This is the “magic” that makes contexts concurrency safe by design–they’re never mutated once they’re created. ctx := context.Background() // Create an original context ctx, cancel = context.


Zero values

I’m sorry for missing a couple days. We took an long weekend with some extended family to visit the beach here in Guatemala. But I’m back, and ready to talk about … zero values! Program initialization and execution The zero value When storage is allocated for a variable, either through a declaration or a call of new, or when a new value is created, either through a composite literal or a call of make, and no explicit initialization is provided, the variable or value is given a default value.

Get daily content like this in your inbox!

Subscribe