DEV Community

Cover image for Use `chan os.Signal` to Manage OS Signals in Go
Leapcell
Leapcell

Posted on

Use `chan os.Signal` to Manage OS Signals in Go

Cover

chan os.Signal is a channel used to receive signals sent by the operating system. This is very useful when you need to gracefully handle system signals (such as the interrupt signal SIGINT or the termination signal SIGTERM), for example, safely shutting down the program, releasing resources, or performing cleanup operations when receiving these signals.

Main Concepts

What is os.Signal?

os.Signal is an interface defined in Go’s standard library to represent operating system signals. It inherits from syscall.Signal and provides a cross-platform way to handle signals across different operating systems.

type Signal interface {
    String() string
    Signal() // inherits from syscall.Signal
}
Enter fullscreen mode Exit fullscreen mode

What is chan os.Signal?

chan os.Signal is a channel specifically designed to receive os.Signal type data. By listening to this channel, a program can respond to signals from the operating system and perform the corresponding handling logic.

Common Signals

Here are some common operating system signals and their uses:

  • SIGINT: Interrupt signal, usually triggered when the user presses Ctrl+C.
  • SIGTERM: Termination signal, used to request the program to exit normally.
  • SIGKILL: Forced termination signal, cannot be caught or ignored (and also cannot be handled in Go).
  • SIGHUP: Hangup signal, usually used to notify the program to reload configuration files.

kill is the most commonly used command-line tool for sending signals. Its basic syntax is as follows:

kill [options]<signal>
Enter fullscreen mode Exit fullscreen mode

Here, <signal> can be either the signal name (such as SIGTERM) or the signal number (such as 15), and <PID> is the process ID of the target process.

Example

Send a SIGTERM signal (request the process to exit normally):

kill -15 <PID>
# or
kill -s SIGTERM <PID>
# or
kill <PID>  # sends SIGTERM by default
Enter fullscreen mode Exit fullscreen mode

Send a SIGKILL signal (force terminate a process):

kill -9 <PID>
# or
kill -s SIGKILL <PID>
Enter fullscreen mode Exit fullscreen mode

Send a SIGINT signal (interrupt signal):

kill -2 <PID>
kill -s SIGINT <PID>
Enter fullscreen mode Exit fullscreen mode

Using chan os.Signal to Listen for Signals

Go provides the signal.Notify function to register the signals to listen for and send those signals to a specified channel. The following is a typical usage example:

package main

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

func main() {
    // Create a channel to receive signals
    sigs := make(chan os.Signal, 1)

    // Register signals to be listened for
    // Here we listen for SIGINT and SIGTERM
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    fmt.Println("The program is running. Press Ctrl+C or send a SIGTERM signal to exit.")

    // Block the main goroutine until a signal is received
    sig := <-sigs
    fmt.Printf("Received signal: %v", sig)

    // Perform cleanup operations or other processing before exiting
    fmt.Println("Exiting program gracefully...")
}
Enter fullscreen mode Exit fullscreen mode

Example Output

The program is running. Press Ctrl+C or send a SIGTERM signal to exit.
^C
Received signal: interrupt
Exiting program gracefully...
Enter fullscreen mode Exit fullscreen mode

In the above example:

  1. A chan os.Signal channel sigs with a buffer size of 1 was created.
  2. The signals SIGINT and SIGTERM were registered using signal.Notify, and these signals are sent to the sigs channel.
  3. The main goroutine blocks on the <-sigs operation, waiting to receive a signal.
  4. When the user presses Ctrl+C or the program receives a SIGTERM signal, the sigs channel receives the signal, the program prints the received signal, and performs exit processing.

Gracefully Handling Multiple Signals

You can listen for multiple types of signals simultaneously and perform different handling logic based on the specific signal.

Example Code

package main

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

func main() {
    sigs := make(chan os.Signal, 1)
    // Register SIGINT, SIGTERM, and SIGHUP
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)

    fmt.Println("The program is running. Press Ctrl+C to send SIGINT, or send other signals to test.")

    for {
        sig := <-sigs
        switch sig {
        case syscall.SIGINT:
            fmt.Println("Received SIGINT signal, preparing to exit.")
            // Perform cleanup operations
            return
        case syscall.SIGTERM:
            fmt.Println("Received SIGTERM signal, preparing to exit gracefully.")
            // Perform cleanup operations
            return
        case syscall.SIGHUP:
            fmt.Println("Received SIGHUP signal, reloading configuration.")
            // Reload configuration logic
        default:
            fmt.Printf("Received unhandled signal: %v", sig)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Using signal.Stop to Stop Listening for Signals

In some cases, you may want to stop listening for specific signals while the program is running. You can achieve this with the signal.Stop function.

Example Code

package main

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

func main() {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    fmt.Println("The program is running. Press Ctrl+C to send SIGINT to exit.")

    // Start a goroutine to handle signals
    go func() {
        sig := <-sigs
        fmt.Printf("Received signal: %v", sig)
    }()

    // Simulate program running for some time before stopping signal listening
    time.Sleep(10 * time.Second)
    signal.Stop(sigs)
    fmt.Println("Stopped listening for signals.")

    // Continue running other parts of the program
    select {}
}
Enter fullscreen mode Exit fullscreen mode

In the above example, the program stops listening for signals after running for 10 seconds and continues to execute other logic.

Notes

  • Buffered channel: It is generally recommended to set a buffer for the signal channel to prevent signals from being lost before they are processed. For example: make(chan os.Signal, 1).
  • Default behavior: If you don’t call signal.Notify, the Go program will handle signals according to the operating system’s default behavior. For example, SIGINT usually causes the program to terminate.
  • Multiple signals: Some signals may be sent multiple times, especially in long-running programs. Ensure that your signal handling logic can correctly deal with receiving the same signal more than once.
  • Resource cleanup: After receiving a termination signal, make sure to perform necessary resource cleanup operations, such as closing files, releasing locks, and disconnecting network connections, to avoid resource leaks.
  • Blocking issue: signal.Notify sends signals to the specified channel but does not block the send operation. Therefore, make sure there is a goroutine listening on that channel; otherwise, signals may be lost.
  • Uncatchable signals: Some signals (such as SIGKILL) cannot be caught or ignored. When a program receives such signals, it will terminate immediately.

Practical Applications

In real-world development, listening for system signals is often used in the following scenarios:

  • Gracefully shutting down servers: After receiving a termination signal, the server can stop accepting new connections and wait for existing connections to finish before exiting.
  • Hot reloading configuration: After receiving a specific signal (such as SIGHUP), the program can reload the configuration file without restarting.
  • Resource release: Ensure that resources are properly released before the program exits, such as closing database connections or releasing locks.

Example: Gracefully Shutting Down an HTTP Server

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // Create an HTTP server
    http.HandleFunc("/", handler)
    server := &http.Server{Addr: ":8080"}

    // Start the server
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("ListenAndServe error: %v", err)
        }
    }()

    // Create a signal channel
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    // Wait for a signal
    sig := <-sigs
    fmt.Printf("Received signal: %v, shutting down server...", sig)

    // Create a context with timeout for shutting down the server
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Gracefully shut down the server
    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Server shutdown error: %v", err)
    }

    fmt.Println("Server has been successfully shut down.")
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Response completed")
    fmt.Println("Printed 'response completed' in the background")
}
Enter fullscreen mode Exit fullscreen mode

In the above example, the HTTP server waits up to 5 seconds to finish processing current requests after receiving SIGINT or SIGTERM, and then shuts down gracefully.

Summary

chan os.Signal is an important mechanism in Go for handling operating system signals. By combining signal.Notify with a signal channel, programs can listen for and respond to various system signals, thereby enabling graceful resource management and program control. Understanding and correctly using chan os.Signal is especially important for writing robust and reliable concurrent programs.


We are Leapcell, your top choice for hosting Go projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ


Read on our blog

Top comments (0)