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)
}
|
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
}
|
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)
},
}
|
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