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.go

Expected 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.

💻 Run locally

Copy the code above and run it on your machine

© 2026 ByteLearn.dev. Free courses for developers. · Privacy