π°οΈ Exercise 7: Patient Go - Making Go Wait for Goroutines
In this exercise, you’ll modify the Go runtime to wait for all goroutines to complete before the program exits. Currently, when main()
returns, Go immediately terminates even if goroutines are still running. We’ll make Go “patient” by waiting for all goroutines to finish!
π― Learning Objectives
By the end of this exercise, you will:
- β Understand Go’s program termination process
- β Know how to count active goroutines
- β Modify the main runtime function to change program behavior
- β Understand the trade-offs of automatic goroutine waiting
π§ Background: Go’s Current Termination Behavior
Currently, when you write:
package main
import "time"
func main() {
go func() {
time.Sleep(2 * time.Second)
println("Goroutine finished!")
}()
println("Main finished!")
// Program exits immediately, goroutine never completes
}
Output:
Main finished!
The goroutine never gets to print because the program exits when main()
returns.
We’ll change this so Go waits patiently for all goroutines to finish:
New Output:
Main finished!
Goroutine finished!
π Step 1: Understanding the Runtime Main Function
The Go runtime’s main()
function in runtime/proc.go
is responsible for running your program’s main()
function. Let’s examine how this works:
cd go/src/runtime
Open proc.go
and find the main()
function. Near the top (around line 135-136), you’ll see how the runtime links to your program’s main:
//go:linkname main_main main.main
func main_main()
This //go:linkname
directive tells the linker to connect the runtime’s main_main
function to your program’s main.main
function. This is how the runtime can call code from your main package.
Further down in the same main()
function (around line 284), you’ll see where this gets called:
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
... // tear-down process continues
How it works:
- The go runtime boostrap process happens
- The runtime’s
main()
function runs first - A bit more of boostrap process
- The
main_main
(which is your program’smain()
function via linkname) is called - Your
main()
function executes - responsibility is delegated to your code - When your
main()
returns, control returns to the runtime’smain()
function - The runtime continues with the program tear-down process (cleanup and exit)
Currently, the tear-down starts immediately after your main()
returns, without waiting for other goroutines.
π§ Step 2: Add Goroutine Waiting Logic
We’ll add code to wait until only 1 goroutine remains (the main goroutine itself).
Edit runtime/proc.go
:
Find the section around line 284-286 where main_main
is called:
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
Add the waiting logic right after the fn()
call:
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
// Wait until only 1 goroutine is running (the main goroutine)
for gcount() > 1 {
Gosched()
}
π Understanding the Code
gcount()
- Runtime function that returns the number of active goroutinesgcount() > 1
- While more than just the main goroutine is runningGosched()
- Yields the processor, allowing other goroutines to run- Loop terminates - When only the main goroutine remains (count = 1)
π Step 3: Rebuild Go Toolchain
cd go/src
./make.bash
This rebuilds the runtime with your patient goroutine waiting logic.
π§ͺ Step 4: Test Basic Goroutine Waiting
Create a test file to verify the behavior:
Create patient_test.go
:
package main
import "time"
func main() {
println("Main starting...")
go func() {
time.Sleep(1 * time.Second)
println("Goroutine 1 finished!")
}()
go func() {
time.Sleep(2 * time.Second)
println("Goroutine 2 finished!")
}()
println("Main finished, but Go will wait...")
}
Run with your modified Go:
./bin/go run patient_test.go
Expected output:
Main starting...
Main finished, but Go will wait...
Goroutine 1 finished!
Goroutine 2 finished!
π Success! Go now waits for all goroutines to complete!
π What We Learned
- π Program Termination: How Go programs exit and cleanup
- π Goroutine Tracking: The
gcount()
function tracks active goroutines - βΈοΈ Cooperative Scheduling:
Gosched()
yields to allow other goroutines to run - π§ Runtime Modification: How a small change affects all Go programs
- βοΈ Design Trade-offs: Benefits and drawbacks of automatic waiting
π‘ Extension Ideas
Try these additional modifications: π
- β Add a timeout: Wait maximum 10 seconds for goroutines
- β Add logging: Print when waiting starts and which goroutines remain
- β Make it configurable: Use environment variable to enable/disable
- β Add a warning: Detect infinite loops in goroutines
π§Ή Cleanup
To restore standard Go behavior:
cd go/src/runtime
git checkout proc.go
cd ..
./make.bash
π Summary
You’ve successfully modified Go’s runtime to be “patient” and wait for all goroutines!
Before: main() returns β immediate exit β goroutines abandoned
After: main() returns β wait for goroutines β all complete β exit
Changes: runtime/proc.go main() function
Result: No goroutine left behind! π―
This modification demonstrates:
- Deep understanding of the Go runtime
- How program termination works
- The relationship between main() and goroutines
- Real-world trade-offs in language design
Your Go is now patient! π°οΈβ¨
Continue to Exercise 8 or return to the main workshop