LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Go for DevOps Engineers

gogolangdevopscliprogrammingtoolsautomation

Go is the language of infrastructure. Docker, Kubernetes, Terraform, Prometheus, Grafana, etcd, Vault, Consul, CockroachDB — the tools that run production systems are overwhelmingly written in Go. That’s not an accident. Go compiles to a single static binary with no runtime dependencies, has first-class concurrency primitives, starts in milliseconds, and produces lean containers. For DevOps engineers who want to move beyond Bash scripts and Python one-offs into tools that perform at scale, Go is the natural choice.

This guide is not a language tour — it’s a practical guide to the patterns DevOps engineers use most: concurrent workers, building CLI tools with Cobra, writing HTTP servers and clients, talking to Kubernetes, and shipping Go binaries correctly.


The Core Mental Shift from Python/Bash

If you’re coming from Python or Bash, a few things feel strange at first:

Static typing catches bugs at compile time. You can’t pass a string where an int is expected, and you can’t ignore errors — the compiler forces you to handle them. This is annoying for small scripts but invaluable for long-running services.

Errors are values, not exceptions. There’s no try/except. Functions return (result, error) and the caller decides what to do. This makes error handling explicit and predictable.

Concurrency is a first-class primitive. Goroutines (lightweight threads) and channels (typed message pipes) are built into the language. Starting 10,000 concurrent HTTP requests is trivial.

One binary, no dependencies. go build produces a self-contained binary that runs anywhere. No Python version conflicts, no requirements.txt, no apt install.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// The two patterns you'll see constantly:

// Error handling
result, err := doSomething()
if err != nil {
    return fmt.Errorf("doing something: %w", err)  // wrap with context
}

// Defer for cleanup (runs when function returns, even on error)
f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close()  // always closes, even if we return early

Concurrency: Goroutines and Channels

Go’s concurrency model is built around goroutines (cheap threads — you can have millions) and channels (typed, synchronised queues).

Fan-Out: Parallel Work with a Worker Pool

The most common pattern in DevOps tooling: you have 100 servers to check, and you want to check them all concurrently, not sequentially.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package main

import (
    "context"
    "fmt"
    "net/http"
    "sync"
    "time"
)

type HealthResult struct {
    Host    string
    Status  int
    Latency time.Duration
    Err     error
}

func checkHealth(ctx context.Context, host string) HealthResult {
    start := time.Now()
    req, _ := http.NewRequestWithContext(ctx, "GET", "https://"+host+"/healthz", nil)
    client := &http.Client{Timeout: 5 * time.Second}

    resp, err := client.Do(req)
    if err != nil {
        return HealthResult{Host: host, Err: err, Latency: time.Since(start)}
    }
    defer resp.Body.Close()

    return HealthResult{
        Host:    host,
        Status:  resp.StatusCode,
        Latency: time.Since(start),
    }
}

func checkAllHosts(ctx context.Context, hosts []string, concurrency int) []HealthResult {
    jobs := make(chan string, len(hosts))
    results := make(chan HealthResult, len(hosts))

    // Start worker pool
    var wg sync.WaitGroup
    for range concurrency {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for host := range jobs {
                results <- checkHealth(ctx, host)
            }
        }()
    }

    // Send jobs
    for _, host := range hosts {
        jobs <- host
    }
    close(jobs) // signal workers there's no more work

    // Wait for all workers, then close results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collect results
    var all []HealthResult
    for r := range results {
        all = append(all, r)
    }
    return all
}

func main() {
    hosts := []string{
        "api.example.com",
        "db.example.com",
        "cache.example.com",
        // ... 100 more
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    results := checkAllHosts(ctx, hosts, 10) // 10 concurrent workers

    for _, r := range results {
        if r.Err != nil {
            fmt.Printf("❌ %s: %v\n", r.Host, r.Err)
        } else {
            fmt.Printf("✓ %s: %d (%s)\n", r.Host, r.Status, r.Latency)
        }
    }
}

Errgroup: Concurrency with Error Propagation

errgroup (from golang.org/x/sync) simplifies fan-out when you want to cancel all goroutines if any one fails:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import "golang.org/x/sync/errgroup"

func deployToAllRegions(ctx context.Context, regions []string, image string) error {
    g, ctx := errgroup.WithContext(ctx)

    // Limit concurrency with a semaphore
    sem := make(chan struct{}, 3) // at most 3 concurrent deploys

    for _, region := range regions {
        region := region // capture loop variable
        g.Go(func() error {
            sem <- struct{}{}        // acquire
            defer func() { <-sem }() // release

            if err := deployToRegion(ctx, region, image); err != nil {
                return fmt.Errorf("deploy to %s: %w", region, err)
            }
            return nil
        })
    }

    // Wait for all goroutines — returns first non-nil error
    // ctx is cancelled when any goroutine returns an error
    return g.Wait()
}

Context: Cancellation and Timeouts

Always propagate context.Context as the first argument. It carries cancellation signals and deadlines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func runWithTimeout(ctx context.Context) error {
    // Derive a child context with a 10-second deadline
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel() // always call cancel to release resources

    // Pass ctx to everything — HTTP requests, DB queries, sub-functions
    result, err := queryDatabase(ctx, "SELECT ...")
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return fmt.Errorf("database query timed out after 10s")
        }
        return fmt.Errorf("database query: %w", err)
    }
    return processResult(ctx, result)
}

Building CLI Tools with Cobra

Cobra is the standard Go library for CLI applications. kubectl, Hugo, GitHub CLI, and most other major Go CLIs use it.

1
2
go get github.com/spf13/cobra@latest
go get github.com/spf13/viper@latest   # configuration management

A Complete CLI Structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// cmd/root.go
package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string
var verbose bool

var rootCmd = &cobra.Command{
    Use:   "platform",
    Short: "LunarOps platform CLI",
    Long: `Platform CLI for managing LunarOps infrastructure.

Provides commands for deploying services, managing secrets,
and interacting with the LunarOps platform.`,
    // PersistentPreRunE runs before every subcommand
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        return initConfig()
    },
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func init() {
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
        "config file (default: $HOME/.platform.yaml)")
    rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false,
        "verbose output")
    // Bind flag to viper key
    viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
}

func initConfig() error {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        if err != nil {
            return err
        }
        viper.AddConfigPath(home)
        viper.SetConfigName(".platform")
        viper.SetConfigType("yaml")
    }

    viper.AutomaticEnv()           // PLATFORM_TOKEN → viper.Get("token")
    viper.SetEnvPrefix("PLATFORM") // prefix for env vars

    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return fmt.Errorf("reading config: %w", err)
        }
    }
    return nil
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// cmd/deploy.go
package cmd

import (
    "context"
    "fmt"
    "time"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var deployCmd = &cobra.Command{
    Use:   "deploy [service]",
    Short: "Deploy a service to the platform",
    Long:  `Deploy a service to the specified environment.`,
    Args:  cobra.ExactArgs(1), // enforce exactly one positional argument
    RunE:  runDeploy,          // RunE returns an error; cobra prints it and exits non-zero
}

var (
    deployEnv     string
    deployVersion string
    deployDryRun  bool
    deployTimeout time.Duration
)

func init() {
    rootCmd.AddCommand(deployCmd)

    deployCmd.Flags().StringVarP(&deployEnv, "env", "e", "staging",
        "Target environment (staging|production)")
    deployCmd.Flags().StringVar(&deployVersion, "version", "",
        "Image version to deploy (default: latest)")
    deployCmd.Flags().BoolVar(&deployDryRun, "dry-run", false,
        "Print what would be done without making changes")
    deployCmd.Flags().DurationVar(&deployTimeout, "timeout", 5*time.Minute,
        "Deployment timeout")

    // Mark required flags
    deployCmd.MarkFlagRequired("version")

    // Validate flag values
    deployCmd.RegisterFlagCompletionFunc("env", func(cmd *cobra.Command,
        args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
        return []string{"staging", "production"}, cobra.ShellCompDirectiveNoFileComp
    })
}

func runDeploy(cmd *cobra.Command, args []string) error {
    service := args[0]
    token := viper.GetString("token")
    if token == "" {
        return fmt.Errorf("platform token not set: use --config or PLATFORM_TOKEN env var")
    }

    if deployDryRun {
        fmt.Printf("Would deploy %s:%s to %s\n", service, deployVersion, deployEnv)
        return nil
    }

    ctx, cancel := context.WithTimeout(context.Background(), deployTimeout)
    defer cancel()

    client := NewPlatformClient(viper.GetString("api_url"), token)

    fmt.Printf("Deploying %s:%s to %s...\n", service, deployVersion, deployEnv)

    if err := client.Deploy(ctx, service, deployVersion, deployEnv); err != nil {
        return fmt.Errorf("deployment failed: %w", err)
    }

    fmt.Printf("✓ %s deployed successfully\n", service)
    return nil
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// cmd/secret.go — a command group with subcommands
package cmd

import "github.com/spf13/cobra"

var secretCmd = &cobra.Command{
    Use:   "secret",
    Short: "Manage secrets",
}

var secretGetCmd = &cobra.Command{
    Use:   "get [path]",
    Short: "Get a secret value",
    Args:  cobra.ExactArgs(1),
    RunE:  runSecretGet,
}

var secretSetCmd = &cobra.Command{
    Use:   "set [path] [key=value...]",
    Short: "Set secret values",
    Args:  cobra.MinimumNArgs(2),
    RunE:  runSecretSet,
}

func init() {
    rootCmd.AddCommand(secretCmd)
    secretCmd.AddCommand(secretGetCmd)
    secretCmd.AddCommand(secretSetCmd)
}
1
2
3
4
5
6
7
8
// cmd/root.go — main.go entrypoint
package main

import "github.com/lunarops/platform/cmd"

func main() {
    cmd.Execute()
}

This gives you:

1
2
3
4
5
platform deploy my-api --version v1.2.3 --env production
platform secret get secret/my-api/database
platform secret set secret/my-api/config key=value other=value2
platform --help
platform deploy --help

Shell Completion

1
2
3
4
# Generate completion scripts (Cobra does this for free)
platform completion bash > /etc/bash_completion.d/platform
platform completion zsh > "${fpath[1]}/_platform"
platform completion fish > ~/.config/fish/completions/platform.fish

HTTP Servers

Go’s net/http package is production-ready without any framework. For a DevOps internal service — a webhook receiver, a metrics exporter, an API proxy — the standard library is often enough.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package main

import (
    "context"
    "encoding/json"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

type Server struct {
    logger *slog.Logger
}

func (s *Server) routes() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /healthz", s.handleHealth)
    mux.HandleFunc("POST /webhook/github", s.handleGithubWebhook)
    mux.HandleFunc("GET /metrics", s.handleMetrics)
    return s.withLogging(s.withRecovery(mux))
}

func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

func (s *Server) handleGithubWebhook(w http.ResponseWriter, r *http.Request) {
    // Validate signature
    sig := r.Header.Get("X-Hub-Signature-256")
    if !validateGithubSignature(sig, r.Body, os.Getenv("WEBHOOK_SECRET")) {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }

    var payload GithubPushEvent
    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
        http.Error(w, "invalid payload", http.StatusBadRequest)
        return
    }

    // Process asynchronously — don't block GitHub's webhook timeout
    go s.processPush(context.Background(), payload)

    w.WriteHeader(http.StatusAccepted)
}

// Middleware: structured request logging
func (s *Server) withLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{ResponseWriter: w, status: 200}
        next.ServeHTTP(rw, r)
        s.logger.Info("request",
            "method", r.Method,
            "path", r.URL.Path,
            "status", rw.status,
            "duration", time.Since(start),
            "remote_addr", r.RemoteAddr,
        )
    })
}

// Middleware: panic recovery
func (s *Server) withRecovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                s.logger.Error("panic recovered", "error", rec)
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(status int) {
    rw.status = status
    rw.ResponseWriter.WriteHeader(status)
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    srv := &Server{logger: logger}

    httpServer := &http.Server{
        Addr:         ":8080",
        Handler:      srv.routes(),
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    // Graceful shutdown
    go func() {
        sig := make(chan os.Signal, 1)
        signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
        <-sig

        logger.Info("shutting down gracefully")
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
        httpServer.Shutdown(ctx)
    }()

    logger.Info("starting server", "addr", httpServer.Addr)
    if err := httpServer.ListenAndServe(); err != http.ErrServerClosed {
        logger.Error("server error", "err", err)
        os.Exit(1)
    }
}

HTTP Clients: Robust API Calls

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package platform

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type Client struct {
    baseURL    string
    token      string
    httpClient *http.Client
}

func NewClient(baseURL, token string) *Client {
    return &Client{
        baseURL: baseURL,
        token:   token,
        httpClient: &http.Client{
            Timeout: 30 * time.Second,
            Transport: &http.Transport{
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 10,
                IdleConnTimeout:     90 * time.Second,
            },
        },
    }
}

func (c *Client) do(ctx context.Context, method, path string, body any) (*http.Response, error) {
    var bodyReader *bytes.Reader
    if body != nil {
        data, err := json.Marshal(body)
        if err != nil {
            return nil, fmt.Errorf("marshalling request body: %w", err)
        }
        bodyReader = bytes.NewReader(data)
    } else {
        bodyReader = bytes.NewReader(nil)
    }

    req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }

    req.Header.Set("Authorization", "Bearer "+c.token)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Accept", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executing request %s %s: %w", method, path, err)
    }

    if resp.StatusCode >= 400 {
        defer resp.Body.Close()
        var apiErr struct {
            Message string `json:"message"`
            Code    string `json:"code"`
        }
        json.NewDecoder(resp.Body).Decode(&apiErr)
        return nil, fmt.Errorf("API error %d: %s (code: %s)",
            resp.StatusCode, apiErr.Message, apiErr.Code)
    }

    return resp, nil
}

func (c *Client) Deploy(ctx context.Context, service, version, env string) error {
    payload := map[string]string{
        "service": service,
        "version": version,
        "env":     env,
    }
    resp, err := c.do(ctx, "POST", "/api/v1/deployments", payload)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    var result struct {
        DeploymentID string `json:"deployment_id"`
    }
    return json.NewDecoder(resp.Body).Decode(&result)
}

Working with the Kubernetes API

The client-go library lets you interact with any Kubernetes cluster programmatically — the same library kubectl uses internally.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package main

import (
    "context"
    "fmt"
    "path/filepath"

    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
    "k8s.io/client-go/util/homedir"
)

func buildClient() (*kubernetes.Clientset, error) {
    // Use in-cluster config when running inside a pod
    // Fall back to kubeconfig for local development
    kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")

    config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
    if err != nil {
        return nil, fmt.Errorf("building kubeconfig: %w", err)
    }

    return kubernetes.NewForConfig(config)
}

func listPodsWithLabel(ctx context.Context, client *kubernetes.Clientset,
    namespace, label string) ([]corev1.Pod, error) {

    pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
        LabelSelector: label, // e.g., "app=my-api,version=v2"
    })
    if err != nil {
        return nil, fmt.Errorf("listing pods: %w", err)
    }
    return pods.Items, nil
}

func restartDeployment(ctx context.Context, client *kubernetes.Clientset,
    namespace, name string) error {

    // Trigger a rollout by patching the pod template annotation
    patch := []byte(`{
        "spec": {
            "template": {
                "metadata": {
                    "annotations": {
                        "kubectl.kubernetes.io/restartedAt": "` + metav1.Now().UTC().Format("2006-01-02T15:04:05Z") + `"
                    }
                }
            }
        }
    }`)

    _, err := client.AppsV1().Deployments(namespace).
        Patch(ctx, name, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
    return err
}

func watchPodLogs(ctx context.Context, client *kubernetes.Clientset,
    namespace, podName, container string) error {

    req := client.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{
        Container: container,
        Follow:    true,
        TailLines: ptr(int64(100)),
    })

    stream, err := req.Stream(ctx)
    if err != nil {
        return fmt.Errorf("opening log stream: %w", err)
    }
    defer stream.Close()

    _, err = io.Copy(os.Stdout, stream)
    return err
}

Using controller-runtime’s Client (for operators or advanced tools)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import (
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/client/config"
)

// controller-runtime's client is more ergonomic than client-go for custom resources
func buildRuntimeClient() (client.Client, error) {
    cfg, err := config.GetConfig()
    if err != nil {
        return nil, err
    }
    return client.New(cfg, client.Options{Scheme: scheme})
}

// List with field selectors and label selectors
var deploys appsv1.DeploymentList
err = k8sClient.List(ctx, &deploys,
    client.InNamespace("production"),
    client.MatchingLabels{"app.kubernetes.io/managed-by": "my-operator"},
)

Working with Files, Processes, and the OS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Running external commands
func runCommand(ctx context.Context, name string, args ...string) (string, error) {
    cmd := exec.CommandContext(ctx, name, args...)
    out, err := cmd.CombinedOutput()
    if err != nil {
        return "", fmt.Errorf("command %q failed: %w\noutput: %s", name, err, out)
    }
    return string(out), nil
}

// Streaming command output
func streamCommand(ctx context.Context, name string, args ...string) error {
    cmd := exec.CommandContext(ctx, name, args...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    return cmd.Run()
}

// Walking a directory tree
func findYAMLFiles(root string) ([]string, error) {
    var files []string
    err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if !d.IsDir() && (filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml") {
            files = append(files, path)
        }
        return nil
    })
    return files, err
}

// Atomic file write (write to temp, rename — prevents partial writes)
func writeFileAtomic(path string, data []byte) error {
    dir := filepath.Dir(path)
    tmp, err := os.CreateTemp(dir, ".tmp-*")
    if err != nil {
        return err
    }
    defer os.Remove(tmp.Name()) // cleanup if rename fails

    if _, err := tmp.Write(data); err != nil {
        tmp.Close()
        return err
    }
    if err := tmp.Sync(); err != nil { // flush to disk
        tmp.Close()
        return err
    }
    tmp.Close()
    return os.Rename(tmp.Name(), path)
}

Packaging and Distributing Go Binaries

Cross-Compilation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Build for Linux AMD64 (from any OS)
GOOS=linux GOARCH=amd64 go build -o platform-linux-amd64 .

# Build for macOS ARM (Apple Silicon)
GOOS=darwin GOARCH=arm64 go build -o platform-darwin-arm64 .

# Build for Windows
GOOS=windows GOARCH=amd64 go build -o platform-windows-amd64.exe .

# Build with version information embedded
VERSION=$(git describe --tags --always --dirty)
go build \
  -ldflags="-X main.version=${VERSION} -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -o platform .
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Embed version at build time
var (
    version   = "dev"     // overridden by ldflags
    buildDate = "unknown"
)

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Print version information",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("platform %s (built %s)\n", version, buildDate)
    },
}

GoReleaser: Multi-Platform Release Automation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# .goreleaser.yaml
version: 2

before:
  hooks:
    - go mod tidy

builds:
  - env:
      - CGO_ENABLED=0   # static binary, no C dependencies
    goos:
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - arm64
    ldflags:
      - -s -w   # strip debug info — smaller binary
      - -X main.version={{.Version}}
      - -X main.buildDate={{.Date}}

archives:
  - format: tar.gz
    name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
    format_overrides:
      - goos: windows
        format: zip

checksum:
  name_template: 'checksums.txt'

signs:
  - artifacts: checksum
    args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"]

release:
  github:
    owner: lunarops
    name: platform

changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'
      - '^ci:'
1
2
3
4
5
6
7
# Test the release locally without publishing
goreleaser release --snapshot --clean

# Publish a release (triggered by pushing a tag)
git tag v1.2.3
git push origin v1.2.3
# GitHub Actions runs: goreleaser release --clean

Minimal Docker Image

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Multi-stage: build in Go image, copy binary to scratch
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w" \
    -o platform .

# Final image: just the binary + CA certificates
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/platform /platform
ENTRYPOINT ["/platform"]

The resulting image is typically 8–15 MB — compared to 800+ MB for a Python base image.


Structured Logging with slog

Go 1.21 added log/slog — structured logging in the standard library:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import "log/slog"

// JSON output for production (machine-readable)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))

// Text output for development (human-readable)
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))

// Log with structured fields
logger.Info("deployment started",
    "service", "my-api",
    "version", "v1.2.3",
    "environment", "production",
)

// Add persistent context to a logger
reqLogger := logger.With(
    "request_id", requestID,
    "user_id", userID,
)
reqLogger.Info("processing request")
reqLogger.Error("request failed", "error", err)

Project Layout

platform-cli/
├── cmd/                  # Cobra commands (one file per command group)
│   ├── root.go
│   ├── deploy.go
│   ├── secret.go
│   └── version.go
├── internal/             # Private packages (not importable externally)
│   ├── client/           # API client
│   ├── config/           # Configuration loading
│   └── k8s/              # Kubernetes helpers
├── pkg/                  # Public packages (importable by other modules)
│   └── api/              # Shared type definitions
├── main.go               # Entrypoint: just calls cmd.Execute()
├── go.mod
├── go.sum
├── .goreleaser.yaml
└── Makefile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Makefile
.PHONY: build test lint release

build:
	go build -ldflags="-X main.version=$(shell git describe --tags --always)" -o bin/platform .

test:
	go test ./... -race -coverprofile=coverage.out

lint:
	golangci-lint run ./...

release:
	goreleaser release --snapshot --clean

Go’s combination of fast compilation, static binaries, excellent concurrency, and a rich standard library makes it the right tool for most DevOps tooling beyond trivial Bash scripts. The learning investment is front-loaded — the type system and explicit error handling take adjustment — but the payoff is tools that are fast, portable, and correct in ways that dynamically typed scripts often aren’t. Start with a small CLI tool to automate something you currently do by hand. The confidence you build there transfers directly to operators, services, and everything in between.

Comments