Go

The Complete Guide to "Respectful Farewell" in Go Development

Author : Raven
Published Time : 2025-11-12

In the Go world, writing a running program is easy, but writing a program that can gracefully exit is an art.

Today, we will exit from the "Hello, World" level, fight monsters and upgrade all the way, until we clear the multi-level assembly line and exit this ultimate boss!

🌱  1、 The most primitive exit: leave at will, regardless of the life or close of the employees

package main

import (
	"fmt"
	"time"
)

func main() {
	// Launch a backend 'worker'
	go func() {
		for {
			fmt.Println("I am silently moving bricks...")
			time.Sleep(1 * time.Second)
		}
	}()

	time.Sleep(3 * time.Second)
	fmt.Println("The boss said he's off work!")
	// The main program exits directly, forcing the workers to 'evaporate'
}

output

I am silently moving bricks...
I am silently moving bricks...
I am silently moving bricks...
The boss said he's off work!
(Program ends, backend goroutine mercilessly killed)

❌  Problem: The backend task may be writing files, making requests, saving databases... As soon as you leave, he will be "injured"!

✅  2、 Beginner elegance: Say hello with a channel

package main

import (
	"fmt"
	"time"
)

func main() {
	done := make(chan bool)

	go func() {
		for {
			select {
			case <-done:
				fmt.Println("Received notice of leaving work, currently tidying up the desktop ..")
				return // Dignified exit
			default:
				fmt.Println("Continue moving bricks ..")
				time.Sleep(1 * time.Second)
			}
		}
	}()

	time.Sleep(3 * time.Second)
	close(done) // Send the 'off work' signal
	time.Sleep(1 * time.Second) // Wait for it to finish tidying up
	fmt.Println("Everyone off work, close the door!")
}

output:

Continue moving bricks ..
Continue moving bricks ..
Continue moving bricks ..
Received notice of leaving work, currently tidying up the desktop ..
Everyone off work, close the door!

✅  Progress has been made! But time. Sleep (1 * time. Second) is too casual - what if it takes 2 seconds to tidy up?

🧱  3、 Intermediate Elegance: Use WaitGroup to wait for everyone to finish work

package main


import (
	"fmt"
	"sync"
	"time"
)


func main() {
	var wg sync.WaitGroup
	done := make(chan bool)


	// Recruiting 3 workers
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for {
				select {
				case <-done:
					fmt.Printf("Employee% d: Received! Turning off the computer ..\n", id)
					return
				default:
					fmt.Printf("Worker% d: Moving bricks ..\n", id)
					time.Sleep(800 * time.Millisecond)
				}
			}
		}(i)
	}


	time.Sleep(3 * time.Second)
	close(done)
	wg.Wait() // Be patient and wait for everyone to turn off their computers
	fmt.Println("The office lights are off, so elegant!")
}

output:

Worker 1: Moving bricks...
Worker 2: Moving bricks...
Worker 3: Moving bricks...
...
Employee 2: Received! Turning off the computer...
Employee 1: Received! Turning off the computer...
Employee 3: Received! Turning off the computer...
The office lights are off, so elegant!

✅  Steady! But in the real world, programs often don't quit on their own - users will press Ctrl+C, K8s will send SIGTERM!

📡  4、 Real world: Monitor system signals (Ctrl+C doesn't panic)

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	// Create a signal receiver
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)

	done := make(chan bool)
	go func() {
		for {
			select {
			case <-done:
				fmt.Println("Background task: Received instruction, saving progress ..")
				return
			default:
				fmt.Println("Background task: Running ..")
				time.Sleep(1 * time.Second)
			}
		}
	}()

	fmt.Println("The program has started. Press Ctrl+C to exit gracefully")

	<-sigCh // Block waiting signal(比如 Ctrl+C)
	fmt.Println("\Detected exit signal! Prepare a dignified farewell ..")

	close(done)
	time.Sleep(1 * time.Second) // Simple waiting (will be optimized later)
	fmt.Println("Goodbye, world! 👋")
}

Operation:

$ go run main.go
The program has started. Press Ctrl+C to exit gracefully
Background task: Running ..
^C
Detected exit signal! Prepare a dignified farewell ..
Background task: Received instruction, saving progress ..
Goodbye, world! 👋

✅  Finally, it's like a production environment! But Time Sleep is still not professional enough

🌟  5、 Go official recommendation: Use context unified management to cancel

Context is the "Swiss Army Knife" of Go concurrent programming, especially suitable for transmitting cancellation signals.

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func worker(ctx context.Context, id int) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("Employee% d: Received cancellation instruction, reason:% v, exiting ..\n", id, ctx.Err())
			return
		default:
			fmt.Printf("Worker% d: Working hard ..\n", id)
			time.Sleep(1 * time.Second)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Activate 3 workers
	for i := 1; i <= 3; i++ {
		go worker(ctx, i)
	}

	// Monitor system signals
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
	<-sigCh

	fmt.Println("\N is ready to gracefully exit ..")
	cancel() //Notify all employees

	//Waiting (should be combined with WaitGroup in actual projects)
	time.Sleep(2 * time.Second)
	fmt.Println("Everyone evacuate safely!")
}

✅  Standard practice! However, please note that the semantics of context are 'cancel as soon as possible', and there is no guarantee that the remaining tasks will be processed!

🚨  6、 High energy warning: the trap of "data jamming" in the assembly line!

Assuming you have such a three-stage assembly line:

Producer → [10 intermediate workers] → [3 final consumers]

If all goroutines listen to ctx. Done() and exit immediately, any unprocessed data in the channel will be lost!

💥  This is the 'pseudo elegant exit' - superficially dignified, but actually losing data!

🏆  7、 Ultimate solution: two-stage exit+pipeline emptying

We need to achieve:

  • Stop production (source cut off)
  • Let the assembly line run naturally (clear the channel)
  • Not losing a single data
package main

import (
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
)

func main() {
	//Two level buffer channel
	ch1 := make(chan int, 100) // Save original tasks
	ch2 := make(chan int, 100) // Save processing results

	var wg sync.WaitGroup

	// ========== Phase 1: Producer (unique response exit signal)==========
	stopProducing := make(chan struct{})
	wg.Add(1)
	go func() {
		defer wg.Done()
		defer close(ch1) // Production is completed, the channel is closed, and downstream is notified that there are no new activities

		for taskID := 1; taskID <= 50; taskID++ {
			select {
			case <-stopProducing:
				fmt.Println("Producer: Received a shutdown order and will no longer accept new orders!")
				return
			case ch1 <- taskID:
				fmt.Printf("Producer: Release task% d \ n", taskID)
				time.Sleep(50 * time.Millisecond)
			}
		}
		fmt.Println("Producer: All tasks for today have been released")
	}()

	// ========== Phase 2: 10 intermediate workers (do not respond to cancellation! Exit only by closing the channel)==========
	stage1Wg := &sync.WaitGroup{}
	stage1Wg.Add(10)
	for i := 1; i <= 10; i++ {
		go func(id int) {
			defer stage1Wg.Done()
			//Key: Do not listen for any cancellation signals here!
            //As long as ch1 is not turned off, keep working
			for task := range ch1 {
				result := task * 2
				fmt.Printf("Intermediate worker% d: Processing task% d → Output% d \ n", id, task, result)
				ch2 <- result
			}
			fmt.Printf("Middle worker% d: ch1 has been turned off, off work! \n", id)
		}(i)
	}

	// After all the intermediate workers are finished, close ch2
	go func() {
		stage1Wg.Wait()
		close(ch2)
		fmt.Println("All intermediate workers are off work, ch2 is closed!")
	}()

	// ========== Phase Three: Three Final Consumers ==========
	wg.Add(3)
	for i := 1; i <= 3; i++ {
		go func(id int) {
			defer wg.Done()
			// Similarly, relying solely on range to automatically exit
			for result := range ch2 {
				fmt.Printf("Final consumer% d: Received finished product% d", id, result)
				time.Sleep(30 * time.Millisecond)
			}
			fmt.Printf("End consumer% d: No new products, work is done! \n", id)
		}(i)
	}

	// ========== Monitor system exit signal ==========
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
	<-sigCh
	fmt.Println("\n⚠️  Received system exit signal!")

	//Only notify producers to stop work, * * do not forcibly interrupt workers**
	close(stopProducing)

	// ========== Waiting for the entire assembly line to be emptied==========
	fmt.Println("Waiting for the assembly line to clear all tasks...")
	wg.Wait()

	fmt.Println("✅ All tasks have been processed, and the program exits with dignity!")
}

Simulate the output of pressing Ctrl+C halfway:

Producer: Release Task 1
Intermediate worker 3: Processing task 1 → Output 2
Final Consumer 1: Received Finished Product 2
...
...
Producer: Release Task 18
^C
⚠️  Received system exit signal!
Producer: Received a shutdown order and will no longer accept new orders!
Intermediate worker 5: Processing task 18 → Output 36
Final Consumer 2: Received Finished Product 36
...
Middle worker 1: ch1 has been turned off, off work!
All intermediate workers are off work, ch2 is closed!
End consumer 3: No new products, work is done!
✅  All tasks have been processed, and the program exits with dignity!

✅  Perfect! Even if stopped midway, it ensures:

  • No more accepting new tasks
  • All accepted tasks have been completed
  • No panic, no leakage of goroutines

🛡️  8、 Anti freeze fallback: add a 'timeout insurance'

Although we hope to empty it, what if a task gets stuck? Add a 30 second timeout:

// Add timeout protection before wg. Wait()
done := make(chan struct{})
go func() {
	wg.Wait()
	close(done)
}()

select {
case <-done:
	fmt.Println("Elegant exit successful!")
case <-time.After(30 * time.Second):
	fmt.Println("❌ Time out! Forced exit (there may be unprocessed data)")
	os.Exit(1)
}

🎉  Conclusion: Elegance is a form of cultivation

In the world of Go, graceful exit is not about whether it is possible or not, but about willingness or not. Spending an extra 10 lines of code can prevent online accidents, data loss, and midnight alerts - this wave is not a loss!