Go’s combination of fast startup, static binaries, cross-compilation, and strong standard library makes it ideal for system utilities. A Go utility compiles to a single binary with no runtime dependencies — scp it to a server and run it immediately.
This guide covers patterns for building practical system tools: file system analysis, log processing, process monitoring, and file watching. For CLI flag parsing see Go command-line parsing flags and for Cobra-based CLIs see Go building CLI with Cobra.
Disk Usage Analyzer
A practical example: a du-like tool that reports directory sizes sorted by size:
package main
import (
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
)
func main() {
depth := flag.Int("depth", 1, "max depth to display")
human := flag.Bool("h", false, "human-readable sizes")
flag.Parse()
root := "."
if flag.NArg() > 0 { root = flag.Arg(0) }
sizes, err := directorySizes(root, *depth)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
// Sort largest first
sort.Slice(sizes, func(i, j int) bool {
return sizes[i].size > sizes[j].size
})
for _, entry := range sizes {
if *human {
fmt.Printf("%8s %s\n", humanSize(entry.size), entry.path)
} else {
fmt.Printf("%12d %s\n", entry.size, entry.path)
}
}
}
type dirSize struct { path string; size int64 }
func directorySizes(root string, maxDepth int) ([]dirSize, error) {
rootDepth := strings.Count(root, string(os.PathSeparator))
sizes := make(map[string]int64)
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil { return err }
if d.IsDir() { return nil }
info, err := d.Info()
if err != nil { return err }
// Attribute file size to each ancestor directory up to maxDepth
rel, _ := filepath.Rel(root, path)
parts := strings.Split(rel, string(os.PathSeparator))
for i := 1; i <= len(parts) && i <= maxDepth; i++ {
dir := filepath.Join(append([]string{root}, parts[:i]...)...)
sizes[dir] += info.Size()
}
return nil
})
if err != nil { return nil, err }
result := make([]dirSize, 0, len(sizes))
for path, size := range sizes {
result = append(result, dirSize{path, size})
}
return result, nil
}
func humanSize(n int64) string {
units := []string{"B", "KB", "MB", "GB", "TB"}
f := float64(n)
for _, u := range units {
if f < 1024 { return fmt.Sprintf("%.1f %s", f, u) }
f /= 1024
}
return fmt.Sprintf("%.1f PB", f)
}
Log File Processor
Many system utilities parse logs. Key patterns: stream line-by-line (don’t load the whole file), parse fields, aggregate statistics:
package main
import (
"bufio"
"compress/gzip"
"flag"
"fmt"
"os"
"regexp"
"sort"
"strconv"
"strings"
)
// Parse nginx access log format:
// 127.0.0.1 - - [01/Jan/2026:00:00:00 +0000] "GET /api/users HTTP/1.1" 200 1234
var logRE = regexp.MustCompile(`^(\S+) .+ "(\S+) (\S+) \S+" (\d+) (\d+)`)
type Stats struct {
count int
total int64
status map[int]int
}
func analyzeLog(path string) (map[string]*Stats, error) {
f, err := os.Open(path)
if err != nil { return nil, err }
defer f.Close()
var r io.Reader = f
if strings.HasSuffix(path, ".gz") {
gz, err := gzip.NewReader(f)
if err != nil { return nil, err }
defer gz.Close()
r = gz
}
routes := make(map[string]*Stats)
scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 0, 64*1024), 1<<20)
for scanner.Scan() {
m := logRE.FindStringSubmatch(scanner.Text())
if m == nil { continue }
method, path, statusStr, bytesStr := m[2], m[3], m[4], m[5]
status, _ := strconv.Atoi(statusStr)
bytes, _ := strconv.ParseInt(bytesStr, 10, 64)
key := method + " " + path
s := routes[key]
if s == nil {
s = &Stats{status: make(map[int]int)}
routes[key] = s
}
s.count++
s.total += bytes
s.status[status]++
}
return routes, scanner.Err()
}
Process Monitor
Watch running processes and alert on resource usage:
package main
import (
"context"
"fmt"
"os"
"runtime"
"time"
)
type ResourceSnapshot struct {
Timestamp time.Time
Goroutines int
HeapAlloc uint64
HeapSys uint64
NumGC uint32
}
func (r ResourceSnapshot) String() string {
return fmt.Sprintf(
"%s goroutines=%d heap=%.1fMB sys=%.1fMB gc=%d",
r.Timestamp.Format("15:04:05"),
r.Goroutines,
float64(r.HeapAlloc)/1e6,
float64(r.HeapSys)/1e6,
r.NumGC,
)
}
func snapshot() ResourceSnapshot {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return ResourceSnapshot{
Timestamp: time.Now(),
Goroutines: runtime.NumGoroutine(),
HeapAlloc: m.HeapAlloc,
HeapSys: m.HeapSys,
NumGC: m.NumGC,
}
}
func monitor(ctx context.Context, interval time.Duration, alert func(ResourceSnapshot)) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
var prev ResourceSnapshot
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
current := snapshot()
// Alert on goroutine leak
if prev.Goroutines > 0 && current.Goroutines > prev.Goroutines*2 {
alert(current)
}
// Alert on rapid memory growth
if prev.HeapAlloc > 0 && current.HeapAlloc > prev.HeapAlloc*3 {
alert(current)
}
fmt.Println(current)
prev = current
}
}
}
File Watcher with fsnotify
fsnotify watches for file system changes without polling:
go get github.com/fsnotify/fsnotify
import "github.com/fsnotify/fsnotify"
func watchConfig(ctx context.Context, path string, onReload func()) error {
watcher, err := fsnotify.NewWatcher()
if err != nil { return err }
defer watcher.Close()
if err := watcher.Add(filepath.Dir(path)); err != nil {
return err
}
for {
select {
case <-ctx.Done():
return nil
case event, ok := <-watcher.Events:
if !ok { return nil }
// Watch for writes or creates to the specific file
if event.Name == path && (event.Has(fsnotify.Write) || event.Has(fsnotify.Create)) {
// Debounce: wait for write to complete
time.Sleep(50 * time.Millisecond)
onReload()
}
case err, ok := <-watcher.Errors:
if !ok { return nil }
fmt.Fprintf(os.Stderr, "watcher error: %v\n", err)
}
}
}
Tabular Output Formatting
System utilities need clean, aligned output. Use text/tabwriter for tab-aligned tables:
import "text/tabwriter"
func printTable(header []string, rows [][]string) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, strings.Join(header, "\t"))
fmt.Fprintln(w, strings.Repeat("-\t", len(header)))
for _, row := range rows {
fmt.Fprintln(w, strings.Join(row, "\t"))
}
w.Flush()
}
// Output:
// NAME SIZE MODIFIED
// - - -
// main.go 2.1 KB 2026-01-01
// util.go 845 B 2025-12-30
For JSON output, add a --json flag that changes output format without changing the logic:
if *jsonOutput {
json.NewEncoder(os.Stdout).Encode(results)
} else {
printTable(header, toRows(results))
}
Single Binary Distribution
Go utilities compile to static binaries. Cross-compile for any target:
# Linux AMD64 (for servers)
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o myutil-linux-amd64 .
# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o myutil-darwin-arm64 .
# Windows
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o myutil.exe .
-ldflags="-s -w" strips debug symbols, reducing binary size by ~30%.
For automatic multi-platform releases, see Go packaging and distribution.
Summary
filepath.WalkDir+ attribute sizes to ancestor directories for recursive disk usage calculation- Stream log files with
bufio.Scanner— usescanner.Bufferto handle long lines; handle.gztransparently runtime.ReadMemStatsfor in-process resource snapshots; alert on sudden jumps, not absolute valuesfsnotifyfor event-based file watching — watch the directory, filter for the specific filetext/tabwriterfor aligned table output; add--jsonflag for machine-readable alternative- Single-binary distribution:
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w"
Comments