Hot Reloading Go Configuration Files Using System Signals
This tutorial explains how to implement hot‑reloading of Go configuration files by manually sending Unix signals, covering the creation of a signal‑listening goroutine, use of the signal.Notify API, selective handling with select, and a complete example that loads JSON config on SIGUSR1.
In many Go projects configuration files (JSON, XML, YAML, or plain text) store essential metadata, and a long‑running process such as a web server often needs to reload these files when they change. Two approaches are introduced: manually sending a system signal and using inotify to watch file changes.
Manual method – using system signals
The idea is to notify the running program that the configuration has been updated by sending a Unix signal (e.g., SIGUSR1 ). A dedicated goroutine receives the signal and triggers the reload logic.
import "os/signal"
signal.Notify(c chan<- os.Signal, sig ...os.Signal)Typical steps:
Create a buffered channel to receive signals.
Start a goroutine that calls signal.Notify with the desired signal (usually SIGUSR1 ).
Inside the goroutine, use an infinite for loop with a select statement to block until a signal arrives.
When a signal is received, invoke a configuration‑loading function.
The select statement works like a switch but only for channel operations, ensuring the goroutine blocks until a signal is placed on the channel.
Example of the signal‑listening goroutine:
func main() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGUSR1)
go func() {
for {
select {
case <-sig:
// load configuration here
_ = loadConfig("/tmp/env.json")
}
}
}()
// keep the program alive
select {}
}Configuration loading logic
The loadConfig function reads the JSON file, checks its modification time against a cached timestamp, unmarshals the content into a struct, and updates a global configuration variable under a read‑write mutex.
type configBean struct {
Test string
}
type config struct {
LastModify time.Time
Data configBean // configuration content
}
var Config *config
func loadConfig(path string) error {
locker := new(sync.RWMutex)
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
fileInfo, err := os.Stat(path)
if err != nil {
return err
}
if Config != nil && fileInfo.ModTime().Before(Config.LastModify) {
return errors.New("no need update")
}
var bean configBean
if err = json.Unmarshal(data, &bean); err != nil {
return err
}
cfg := config{LastModify: fileInfo.ModTime(), Data: bean}
locker.Lock()
Config = &cfg
locker.Unlock()
return nil
}The function deliberately locks only around the assignment to avoid unnecessary contention, assuming only one goroutine writes to the file. If multiple goroutines may write concurrently, the file I/O should also be protected.
Putting it all together
The final main function sets up the signal channel, starts the listening goroutine, and blocks the process so it can react to signals:
func main() {
configPath := "/tmp/env.json"
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGUSR1)
go func(path string) {
for {
select {
case <-sig:
_ = loadConfig(path)
}
}
}(configPath)
// block forever (or until another termination signal)
select {}
}The article also provides a link to the full source code on GitHub and notes that the demo omits error handling and signal‑channel cleanup, which readers should implement themselves.
Future work will cover the second method—using inotify to automatically watch file changes and trigger reloads without manual signals.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.