Daemon Skeleton
The bones of a real service: a single-instance PID-file lock created with O_EXCL, structured slog output, a periodic work loop, and a graceful shutdown on SIGINT/SIGTERM. The demo signals itself so it exits cleanly.
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// Single-instance guard: O_EXCL create fails if the PID file already exists.
pidPath := filepath.Join(os.TempDir(), "daemon-skeleton.pid")
f, err := os.OpenFile(pidPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if errors.Is(err, os.ErrExist) {
logger.Error("already running", "pidfile", pidPath)
os.Exit(1)
} else if err != nil {
logger.Error("pidfile", "err", err)
os.Exit(1)
}
fmt.Fprintf(f, "%d\n", os.Getpid())
f.Close()
defer os.Remove(pidPath) // release the lock on the way out
// Graceful shutdown on SIGINT/SIGTERM.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
logger.Info("daemon started", "pid", os.Getpid())
// Demo: stop ourselves after a moment so the example finishes.
go func() {
time.Sleep(300 * time.Millisecond)
syscall.Kill(os.Getpid(), syscall.SIGTERM)
}()
// Work loop: do periodic work until the context is cancelled.
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
logger.Info("tick")
case <-ctx.Done():
logger.Info("shutting down cleanly")
return
}
}
}Save the code as main.go and run it:
go run main.goExpected output:
{"time":"2026-07-18T18:32:15.06Z","level":"INFO","msg":"daemon started","pid":4812}
{"time":"2026-07-18T18:32:15.16Z","level":"INFO","msg":"tick"}
{"time":"2026-07-18T18:32:15.26Z","level":"INFO","msg":"tick"}
{"time":"2026-07-18T18:32:15.36Z","level":"INFO","msg":"tick"}
{"time":"2026-07-18T18:32:15.36Z","level":"INFO","msg":"shutting down cleanly"}Timestamps and pid vary, and you'll see roughly three ticks before it signals itself and exits. Launch a second copy while the first is still running and it prints already running and exits 1, because the PID file is still held.