
Go doesn’t provide a built-in “kill” mechanism for goroutines; instead, you must design cooperative termination using channels—typically by sending a signal (e.g., via a done or quit channel) and having the goroutine exit its loop gracefully.
go doesn’t provide a built-in “kill” mechanism for goroutines; instead, you must design cooperative termination using channels—typically by sending a signal (e.g., via a `done` or `quit` channel) and having the goroutine exit its loop gracefully.
To stop a goroutine safely and correctly, avoid busy-waiting (default cases), prevent reading from closed channels, and ensure the goroutine exits the loop upon receiving a shutdown signal.
✅ Correct Pattern: Use a done Channel for Graceful Exit
Here’s a clean, idiomatic approach:
done := make(chan struct{})
go func() {
for {
select {
case <-done:
fmt.Println("Goroutine stopped.")
return // Exit the goroutine cleanly
default:
// Optional: perform work only when appropriate
// e.g., call myFunc(c) conditionally — but not in a tight loop
time.Sleep(100 * time.Millisecond) // Prevent busy-looping
}
}
}()
// To stop it:
close(done) // or send: done <- struct{}{}❌ Why Your Original Code Fails
-
close(run)is dangerous: After closingrun,always succeeds immediately (returning the zero value of <code>bool), causingmyFunc(c)to be called repeatedly—even afterquitis signaled. -
defaultcauses busy-looping: Thedefaultbranch makes the loop spin continuously, consuming CPU and potentially starving other goroutines. -
No synchronization: Sending to
quitorrunhappens after the goroutine starts—but there's no guarantee the goroutine has entered theselectbefore yourif x == trueexecutes. You risk missing the signal entirely.
✅ Better Design: Control Start/Stop via a Single Signal Channel
If you need to toggle execution (start/stop myFunc dynamically), use a sync.WaitGroup + atomic state or a dedicated control channel:
control := make(chan bool) // true = start, false = stop
done := make(chan struct{})
var active int32 // atomic flag
go func() {
defer close(done)
for {
select {
case start := <-control:
if start {
atomic.StoreInt32(&active, 1)
go func() {
myFunc(c)
atomic.StoreInt32(&active, 0)
}()
} else {
atomic.StoreInt32(&active, 0)
}
case <-done:
return
}
}
}()
// Start
control <- true
// Stop
control <- false
// Wait for graceful shutdown (optional)
<-done? Key Takeaways
- ✅ Always
return(orbreakout of the loop) after receiving a shutdown signal — never just close another channel. - ✅ Never use
defaultin long-runningselectloops unless intentional and bounded (e.g., withtime.Sleep). - ✅ Prefer
chan struct{}overchan boolfor signaling — it’s zero-size, semantically clearer, and avoids accidental value reads. - ✅ Use
close(ch)only when all senders are done; for signaling, prefer sending a value or closing a dedicateddonechannel.
By embracing Go’s concurrency model—cooperative, channel-driven, and non-preemptive—you write safer, more maintainable, and debuggable concurrent code.

















