# Go (Golang) Cheatsheet

- URL: https://karandeepsingh.ca/cheatsheets/languages/golang/
- Author: Karandeep Singh
- Published: 2025-07-08
- Tags: golang
- Summary: A practical Go reference for DevOps work covering go mod and build commands, core syntax, concurrency patterns, testing, and the stdlib snippets I reuse most.

Docker, Kubernetes, Terraform, and half the CLIs I touch daily are written in Go, so it became my default for tooling too. These are the notes I wish I'd had when I started: toolchain commands, syntax I keep forgetting, and the concurrency patterns that actually come up in DevOps work.

## Toolchain

> **Docs:** [go command on pkg.go.dev](https://pkg.go.dev/cmd/go)

```bash
go mod init github.com/karan/myapp        # Start a module
go mod tidy                               # Add missing / drop unused deps
go get github.com/spf13/cobra@latest      # Add or upgrade a dependency
go run .                                  # Compile + run current package
go build -o bin/myapp ./cmd/myapp         # Build a binary
go test ./...                             # Test everything, recursively
go test -run TestParse -v ./internal/...  # One test, verbose
go test -race -cover ./...                # Race detector + coverage
go vet ./...                              # Static analysis
gofmt -l -w .                             # Format (or: go fmt ./...)
go install golang.org/x/tools/cmd/goimports@latest   # Install a tool to $GOBIN
GOOS=linux GOARCH=arm64 go build -o myapp-arm64 .    # Cross-compile
go build -ldflags="-s -w" .               # Strip symbols, smaller binary
```

Static cross-compiled binaries are why Go containers are tiny; the multi-stage build in my [Docker Cheatsheet](https://karandeepsingh.ca/cheatsheets/containers/docker/) shows the payoff.

## Syntax Essentials

> **Docs:** [The Go language specification at go.dev](https://go.dev/ref/spec)

### Types, Variables, Slices, Maps

```go
var count int = 10        // explicit
name := "karan"           // inferred, function scope only
const MaxRetries = 3      // constants: untyped until used

nums := []int{1, 2, 3}                 // slice
nums = append(nums, 4)
sub := nums[1:3]                       // [2 3] - shares backing array

ports := map[string]int{"http": 80, "https": 443}
ports["ssh"] = 22
v, ok := ports["ftp"]                  // ok == false: key absent
delete(ports, "ssh")

for i, n := range nums { fmt.Println(i, n) }    // map range order is random
```

### Structs, Methods, Interfaces

```go
type Server struct {
    Host string
    Port int
    tags []string          // lowercase = unexported
}

// Value receiver: gets a copy. Pointer receiver: can mutate.
func (s Server) Addr() string      { return fmt.Sprintf("%s:%d", s.Host, s.Port) }
func (s *Server) AddTag(t string)  { s.tags = append(s.tags, t) }

type Notifier interface {
    Notify(msg string) error
}

// Interfaces are satisfied implicitly - no "implements" keyword.
type SlackNotifier struct{ Webhook string }

func (s SlackNotifier) Notify(msg string) error { return post(s.Webhook, msg) }

func alert(n Notifier, msg string) error { return n.Notify(msg) }
```

### Errors & Defer

```go
f, err := os.Open("config.yaml")
if err != nil {
    return fmt.Errorf("opening config: %w", err)   // %w wraps for errors.Is/As
}
defer f.Close()          // runs when the function returns, LIFO order

if errors.Is(err, os.ErrNotExist) { /* fall back to defaults */ }

var pathErr *os.PathError
if errors.As(err, &pathErr) { fmt.Println(pathErr.Path) }

var ErrNotFound = errors.New("not found")   // sentinel error
```

## Goroutines & Channels

```text
$ go run main.go
checked db-1
checked web-1
checked web-2
# The order is not the order they were started in, and it changes between
# runs. If your code depends on it, you want a slice indexed by position,
# not a channel.
```

> **Docs:** [Effective Go on go.dev](https://go.dev/doc/effective_go)

```go
go doWork()                        // fire-and-forget goroutine

ch := make(chan string)            // unbuffered: send blocks until receive
buf := make(chan int, 10)          // buffered: blocks only when full

ch <- "ping"; msg := <-ch          // send / receive
close(ch)                          // sender closes; receivers range safely

for msg := range ch { fmt.Println(msg) }   // loop until channel closed
```

### select

```go
select {
case msg := <-ch:
    fmt.Println("got", msg)
case <-time.After(2 * time.Second):
    fmt.Println("timeout")
case <-ctx.Done():
    return ctx.Err()
}
```

### WaitGroup

```go
var wg sync.WaitGroup
for _, host := range hosts {
    wg.Add(1)
    go func(h string) {
        defer wg.Done()
        checkHealth(h)
    }(host)
}
wg.Wait()
```

### Context Cancelation

```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := http.DefaultClient.Do(req)   // aborts when ctx expires

// In workers, select on ctx.Done() alongside your jobs channel and
// return ctx.Err() when it fires - same pattern as the select above.
```

**tip:**
Always run `go test -race ./...` in CI. The race detector catches shared-memory bugs that pass every normal test run. The classic one is goroutines appending to a shared slice without a mutex.

## Table-Driven Tests

The payoff is the failure message. Each case is a named subtest, so a break
tells you *which* input broke without a debugger. This run is real — the
expectation in the last case was simply wrong:

```text
$ go test -v -run TestSum .
# exits with status 1
| === RUN   TestSum/negative
    sum_test.go:21: Sum([-1 1]) = 0, want 1
| --- FAIL: TestSum (0.00s)
|     --- PASS: TestSum/empty (0.00s)
|     --- PASS: TestSum/one (0.00s)
|     --- PASS: TestSum/many (0.00s)
|     --- FAIL: TestSum/negative (0.00s)
FAIL	fleet	0.002s
# Three passed, one named case failed, and the line number is the case —
# not the loop. That is the whole reason to write the test this way.
```

```text
$ go test -v -run TestSum .
--- PASS: TestSum (0.00s)
    --- PASS: TestSum/empty (0.00s)
    --- PASS: TestSum/one (0.00s)
    --- PASS: TestSum/many (0.00s)
    --- PASS: TestSum/negative (0.00s)
ok  	fleet	0.003s
```

> **Docs:** [testing package on pkg.go.dev](https://pkg.go.dev/testing)

```go
func TestParsePort(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    int
        wantErr bool
    }{
        {"plain", "8080", 8080, false},
        {"with colon", ":443", 443, false},
        {"garbage", "abc", 0, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParsePort(tt.input)
            if (err != nil) != tt.wantErr {
                t.Fatalf("err = %v, wantErr %v", err, tt.wantErr)
            }
            if got != tt.want {
                t.Errorf("got %d, want %d", got, tt.want)
            }
        })
    }
}
```

## Generics (Quick Example)

> **Docs:** [Generics tutorial on go.dev](https://go.dev/doc/tutorial/generics)

```go
func Map[T, U any](in []T, f func(T) U) []U {
    out := make([]U, len(in))
    for i, v := range in {
        out[i] = f(v)
    }
    return out
}

func Max[T cmp.Ordered](a, b T) T { if a > b { return a }; return b }

names := Map(servers, func(s Server) string { return s.Addr() })
```

## Formatting Verbs

> **Docs:** [fmt package on pkg.go.dev](https://pkg.go.dev/fmt)

| Verb | Meaning | Example output |
|------|---------|----------------|
| `%v` | Default format | `{web 8080}` |
| `%+v` | Struct with field names | `{Host:web Port:8080}` |
| `%#v` | Go syntax representation | `main.Server{Host:"web", ...}` |
| `%T` | Type of the value | `main.Server` |
| `%d` | Integer (base 10) | `8080` |
| `%s` | String | `web` |
| `%q` | Quoted string | `"web"` |
| `%f` / `%.2f` | Float / 2 decimals | `3.14` |
| `%x` | Hex | `1f90` |
| `%w` | Wrap error (`fmt.Errorf` only) | - |

## Common Stdlib Snippets

> **Docs:** [Standard library on pkg.go.dev](https://pkg.go.dev/std)

### net/http Server

```go
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
    })
    srv := &http.Server{Addr: ":8080", Handler: mux, ReadTimeout: 5 * time.Second}
    log.Fatal(srv.ListenAndServe())
}
```

### JSON Marshal / Unmarshal

```go
type Alert struct {
    Name     string    `json:"name"`
    Severity string    `json:"severity,omitempty"`
    FiredAt  time.Time `json:"fired_at"`
}

b, err := json.Marshal(alert)              // struct → []byte
err = json.Unmarshal(b, &alert)            // []byte → struct
err = json.NewDecoder(r.Body).Decode(&alert)   // straight from an io.Reader
```

### os/exec

```go
out, err := exec.Command("kubectl", "get", "pods", "-o", "json").Output()
if err != nil {
    var exitErr *exec.ExitError
    if errors.As(err, &exitErr) {
        log.Printf("exit %d: %s", exitErr.ExitCode(), exitErr.Stderr)
    }
}

cmd := exec.CommandContext(ctx, "sh", "-c", "long-running-task")   // killable via ctx
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
err = cmd.Run()
```

### time

```go
now := time.Now()
now.Format(time.RFC3339)                          // 2025-07-23T09:09:00Z
t, _ := time.Parse("2006-01-02", "2025-07-23")    // reference date layout
elapsed := time.Since(start)

ticker := time.NewTicker(30 * time.Second)   // defer ticker.Stop()
for range ticker.C { poll() }
```

Goroutines are a different world from Python's GIL-constrained threads. If you work in both languages, my [Python Threading & Concurrency Cheatsheet](https://karandeepsingh.ca/cheatsheets/python/python-threading-concurrency/) makes a handy side-by-side.

**Question:** When a Go test of yours fails in CI, does the output name the input that broke it — or do you have to re-run it locally to find out?

## Related Cheatsheets

This sheet is the broad one. Three others go deeper where Go code usually
goes wrong:

- [Go Concurrency](https://karandeepsingh.ca/cheatsheets/languages/go-concurrency/) — goroutines, channels, select, context, worker pools, and the race detector finding the bug that printed the right answer
- [Go Logging](https://karandeepsingh.ca/cheatsheets/languages/go-logging/) — `log/slog`, handlers, typed attributes, and the `!BADKEY` the compiler cannot catch for you
- [Go Security](https://karandeepsingh.ca/cheatsheets/languages/go-security/) — `exec.Command` without a shell, `crypto/rand`, path containment, TLS and timeouts

- [Python Threading & Concurrency](https://karandeepsingh.ca/cheatsheets/python/python-threading-concurrency/) is the contrast case: the GIL, ThreadPoolExecutor, and asyncio next to goroutines and channels
- [Docker](https://karandeepsingh.ca/cheatsheets/containers/docker/) has the multi-stage builds that shrink a Go binary into a scratch image, plus the run flags and compose reference
- [Kubernetes](https://karandeepsingh.ca/cheatsheets/containers/kubernetes/) is where Go tooling lives in production: kubectl reference, manifest skeletons, rollouts, and debugging

There's more in the [Languages group](https://karandeepsingh.ca/cheatsheets/languages/), or start from the whole [cheatsheet library](https://karandeepsingh.ca/cheatsheets/).
