flag, os/exec, and os/signal
flag, os/exec, and os/signal
Overview
Most Go programs start as tools: parse flags, run work, maybe spawn subprocesses, and shut down cleanly on SIGINT/SIGTERM.
| Package | Role |
|---|---|
flag |
CLI flags and usage |
os/exec |
Subprocesses |
os/signal |
Graceful shutdown signals |
flag
var (
addr = flag.String("addr", ":8080", "listen address")
n = flag.Int("n", 1, "repeat count")
v = flag.Bool("v", false, "verbose")
)
flag.Parse()
args := flag.Args() // non-flag argsCustom FlagSet (subcommands)
fs := flag.NewFlagSet("greet", flag.ExitOnError)
name := fs.String("name", "world", "who to greet")
_ = fs.Parse(os.Args[2:])Good CLI hygiene
- Provide defaults and help text.
- Exit non-zero on misuse (
flag.ExitOnErroror manualos.Exit(2)). - Prefer explicit flags over positional soup for optional config.
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: %s [flags] <path>\n", filepath.Base(os.Args[0]))
flag.PrintDefaults()
}os/exec
cmd := exec.CommandContext(ctx, "git", "status", "--short")
cmd.Dir = repo
cmd.Env = append(os.Environ(), "LC_ALL=C")
out, err := cmd.Output() // stdout; err may be *exec.ExitErrorStream pipes
cmd := exec.Command("grep", "TODO")
cmd.Stdin = strings.NewReader(src)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()Rules
- Prefer
CommandContextso cancel kills the process tree (platform nuances apply). - Never build shell command strings with untrusted input — pass argv slices.
- Check exit codes via
errors.As(err, &exitErr).
if err := cmd.Run(); err != nil {
var ee *exec.ExitError
if errors.As(err, &ee) {
return fmt.Errorf("exit %d: %w", ee.ExitCode(), err)
}
return err
}os/signal and graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
}()signal.NotifyContext (Go 1.16+) is the clean default for servers and long CLIs.
Runnable example
go mod init example
go run . -n 2 hellopackage main
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"os/exec"
"time"
)
func main() {
n := flag.Int("n", 1, "repeat")
flag.Parse()
msg := "world"
if flag.NArg() > 0 {
msg = flag.Arg(0)
}
for i := 0; i < *n; i++ {
fmt.Println("hello", msg)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "echo", "from-subprocess")
var buf bytes.Buffer
cmd.Stdout = &buf
if err := cmd.Run(); err != nil {
fmt.Fprintln(os.Stderr, "exec:", err)
os.Exit(1)
}
fmt.Print("exec out:", buf.String())
}Expected output:
hello hello
hello hello
exec out:from-subprocess
(With -n 2 hello you get two greeting lines.)
What to notice: - flag.Arg / Args are only valid after Parse. - CommandContext ties subprocess lifetime to a deadline. - Real services combine NotifyContext with http.Server.Shutdown.
Try next: Add a timeout duration flag and pass it into context.WithTimeout around a long exec.Command.