Free Handbook · Every example compiled & verified

Go + Tools

Nine mini-labs on Go at work: Gin, PostgreSQL with pgx, Redis, Docker, Kubernetes, Prometheus, Grafana, Kafka and GitHub Actions.

0 / 134 lessons🔥 0 day streak
ShareXLinkedIn

Module 12 · what you'll be able to do

  • Build a JSON REST API with the standard net/http router, then the same API with Gin, and test handlers with httptest
  • Query PostgreSQL safely with database/sql and pgx, using placeholders, contexts and a connection pool
  • Add a Redis cache-aside layer and keep it swappable behind a small interface
  • Ship a Go service as a tiny static binary in a multi-stage Docker image, and run it on Kubernetes with liveness and readiness probes
  • Expose Prometheus metrics, consume Kafka events, and run vet, -race tests and builds in GitHub Actions
01

The Go toolchain at a glance

Go is the language of cloud infrastructure — Docker, Kubernetes, Prometheus, Terraform and etcd are all written in it — so a Go job nearly always means Go plus HTTP, a database, containers and observability. These labs show the smallest real version of each. Code that needs a third-party module (go get …) is shown as a static snippet; code that needs only the standard library is a verified example you can run as-is.

ToolGo package you useYou meet it when
net/http, Ginnet/http, github.com/gin-gonic/ginWriting any HTTP API
PostgreSQLdatabase/sql + github.com/jackc/pgx/v5Any service that stores data
Redisgithub.com/redis/go-redis/v9Caching, rate limits, sessions
DockerCGO_ENABLED=0 go buildShipping the binary
Kubernetes/healthz, /readyz handlers, graceful shutdownRunning it in production
Prometheus + Grafanagithub.com/prometheus/client_golangDashboards and alerts
Kafkagithub.com/segmentio/kafka-goEvent-driven services, data pipelines
Git + GitHub Actionsgo vet, go test -race in CIEvery pull request
bash
mkdir orders && cd orders
go mod init github.com/you/orders       # creates go.mod
go get github.com/gin-gonic/gin@latest  # adds a dependency to go.mod and go.sum
go mod tidy                             # add what is imported, drop what is not
go run .                                # build and run the package in this folder

Every lab below lives in a module like this one. Module 09 explains go.mod and go.sum.

02

Go + net/http and Gin: a REST API

Since Go 1.22 the standard http.ServeMux understands methods and path parameters ("GET /items/{id}", read with r.PathValue("id")), so many teams start with no framework at all. Handlers are plain functions of (http.ResponseWriter, *http.Request), which makes them easy to test: net/http/httptest records the response without opening a port.

gomain.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
)

type Item struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

var items = map[string]Item{"42": {ID: "42", Name: "keyboard"}}

func getItem(w http.ResponseWriter, r *http.Request) {
	it, ok := items[r.PathValue("id")]
	if !ok {
		http.Error(w, "not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(it)
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /items/{id}", getItem)

	for _, path := range []string{"/items/42", "/items/7"} {
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
		fmt.Print(rec.Code, " ", rec.Body.String())
	}
	// In a real program: http.ListenAndServe(":8080", mux)
}
Outputcompiled & run with real Go
200 {"id":"42","name":"keyboard"}
404 not found

The same httptest pattern goes straight into a _test.go file — no server, no port, no flakiness.

Your turn

Register "POST /items" with a handler that decodes a JSON body into an Item, stores it, and replies 201. Send it a request with httptest.NewRequest("POST", "/items", strings.NewReader(`{"id":"1","name":"mouse"}`)).

Gin is the most used Go web framework. It adds route groups, middleware, request binding and validation, and a faster router. The shape is the same — a handler per route — but the handler gets a *gin.Context that wraps the request and response.

Go + Gin

The same API with Gin, plus binding and validation

gin.Default() includes the logger and panic-recovery middleware. ShouldBindJSON decodes the body and checks the binding tags, so a missing name becomes a 400 before your code runs. Route groups (/api/v1) keep versions apart. Run with go run . and try curl localhost:8080/api/v1/items/42.

go
package main

import (
	"net/http"
	"sync"

	"github.com/gin-gonic/gin"
)

type Item struct {
	ID   string `json:"id" binding:"required"`
	Name string `json:"name" binding:"required,min=2"`
}

type store struct {
	mu    sync.RWMutex
	items map[string]Item
}

func main() {
	s := &store{items: map[string]Item{"42": {ID: "42", Name: "keyboard"}}}
	r := gin.Default()

	v1 := r.Group("/api/v1")
	v1.GET("/items/:id", func(c *gin.Context) {
		s.mu.RLock()
		it, ok := s.items[c.Param("id")]
		s.mu.RUnlock()
		if !ok {
			c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
			return
		}
		c.JSON(http.StatusOK, it)
	})
	v1.POST("/items", func(c *gin.Context) {
		var in Item
		if err := c.ShouldBindJSON(&in); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		s.mu.Lock()
		s.items[in.ID] = in
		s.mu.Unlock()
		c.JSON(http.StatusCreated, in)
	})

	r.Run(":8080")
}
net/http or a framework?
Both are normal in real codebases. The standard library is enough for small services and is what libraries build on; Gin (or Echo, Chi, Fiber) saves boilerplate once you have many routes, middleware and validation. Whatever you pick, the handler stays a small function you can test with httptest. Note the mutex: every request runs in its own goroutine, so shared maps need locking (Module 08).
03

Go + PostgreSQL

database/sql is the standard interface to SQL databases; a driver plugs a specific database into it. For PostgreSQL the standard choice is pgx, which can be used through database/sql (portable) or through its own pgxpool API (faster, more Postgres features). Either way, sql.DB / pgxpool.Pool is a connection pool, safe for concurrent use: create it once at startup and share it.

Go + PostgreSQL

Query and insert with pgxpool, contexts and placeholders

Values go in as $1, $2 placeholders, never with fmt.Sprintf — the driver sends them separately from the SQL, which makes SQL injection impossible. Every call takes a context, so a slow query is cancelled when the HTTP request that caused it goes away. pgx.ErrNoRows is the sentinel for "not found" (compare with errors.Is, Module 07). Always defer rows.Close() and check rows.Err() after the loop.

go
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

type Order struct {
	ID    int64
	Email string
	Total float64
}

func orderByID(ctx context.Context, db *pgxpool.Pool, id int64) (Order, error) {
	var o Order
	err := db.QueryRow(ctx,
		`SELECT id, email, total FROM orders WHERE id = $1`, id,
	).Scan(&o.ID, &o.Email, &o.Total)
	if errors.Is(err, pgx.ErrNoRows) {
		return Order{}, fmt.Errorf("order %d: not found", id)
	}
	return o, err
}

func bigOrders(ctx context.Context, db *pgxpool.Pool, min float64) ([]Order, error) {
	rows, err := db.Query(ctx,
		`SELECT id, email, total FROM orders WHERE total >= $1 ORDER BY total DESC`, min)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Order
	for rows.Next() {
		var o Order
		if err := rows.Scan(&o.ID, &o.Email, &o.Total); err != nil {
			return nil, err
		}
		out = append(out, o)
	}
	return out, rows.Err()
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// postgres://user:pass@localhost:5432/shop?sslmode=disable
	db, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	var id int64
	err = db.QueryRow(ctx,
		`INSERT INTO orders (email, total) VALUES ($1, $2) RETURNING id`,
		"[email protected]", 120.50).Scan(&id)
	if err != nil {
		log.Fatal(err)
	}
	o, err := orderByID(ctx, db, id)
	fmt.Println(o, err)

	big, err := bigOrders(ctx, db, 100)
	fmt.Println(len(big), err)
}
Brush up the SQL itself →
bash
# a throwaway Postgres for local development
docker run --name pg -e POSTGRES_PASSWORD=dev -p 5432:5432 -d postgres:17
psql postgres://postgres:dev@localhost:5432/postgres \
  -c 'CREATE TABLE orders (id bigserial PRIMARY KEY, email text NOT NULL, total numeric(10,2) NOT NULL)'
export DATABASE_URL=postgres://postgres:dev@localhost:5432/postgres?sslmode=disable
go run .
In real jobs
Teams usually add one layer on top: sqlc generates type-safe Go functions from plain .sql files, and a migration tool (golang-migrate, goose, Atlas) versions the schema. Full ORMs such as GORM exist but are less common in Go than in Java or Python.
04

Go + Redis: a cache-aside layer

Cache-aside is the most common caching pattern: look in the cache first; on a miss, load from the database and store the result with a time-to-live. Put the cache behind a small interface and your business code does not care whether it is Redis, an in-memory map, or nothing at all — and your tests need no Redis server.

gomain.go
package main

import (
	"fmt"
	"time"
)

// Cache is all the service needs; Redis and a map both satisfy it.
type Cache interface {
	Get(key string) (string, bool)
	Set(key, val string, ttl time.Duration)
}

type memCache map[string]string

func (m memCache) Get(k string) (string, bool)      { v, ok := m[k]; return v, ok }
func (m memCache) Set(k, v string, _ time.Duration) { m[k] = v }

type Prices struct {
	cache   Cache
	dbCalls int
}

func (p *Prices) Get(sku string) string {
	if v, ok := p.cache.Get("price:" + sku); ok {
		return v + " (cache)"
	}
	p.dbCalls++
	v := "19.99" // pretend this came from PostgreSQL
	p.cache.Set("price:"+sku, v, 10*time.Minute)
	return v + " (db)"
}

func main() {
	p := &Prices{cache: memCache{}}
	fmt.Println(p.Get("A1"))
	fmt.Println(p.Get("A1"))
	fmt.Println(p.Get("B2"))
	fmt.Println("db calls:", p.dbCalls)
}
Outputcompiled & run with real Go
19.99 (db)
19.99 (cache)
19.99 (db)
db calls: 2
Your turn

Write a noCache type whose Get always misses and whose Set does nothing. Plug it in and check that dbCalls becomes 3.

Go + Redis

The same Cache interface backed by Redis (go-redis v9)

redis.NewClient is a pooled, goroutine-safe client — create one and share it. Get returns the sentinel redis.Nil on a miss, which is not a real error. A cache is an optimisation, so on any other Redis error the code logs and falls through to the database instead of failing the request. Keys are namespaced (price:A1) and every Set has a TTL so stale data expires on its own.

go
package cache

import (
	"context"
	"errors"
	"log/slog"
	"time"

	"github.com/redis/go-redis/v9"
)

type Redis struct {
	rdb *redis.Client
}

func NewRedis(addr string) *Redis {
	return &Redis{rdb: redis.NewClient(&redis.Options{Addr: addr})} // "localhost:6379"
}

func (r *Redis) Get(key string) (string, bool) {
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	v, err := r.rdb.Get(ctx, key).Result()
	if errors.Is(err, redis.Nil) {
		return "", false // a normal miss
	}
	if err != nil {
		slog.Warn("redis get failed", "key", key, "err", err)
		return "", false // treat as a miss; the database still answers
	}
	return v, true
}

func (r *Redis) Set(key, val string, ttl time.Duration) {
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	if err := r.rdb.Set(ctx, key, val, ttl).Err(); err != nil {
		slog.Warn("redis set failed", "key", key, "err", err)
	}
}

// docker run -d -p 6379:6379 redis:7
// p := &Prices{cache: cache.NewRedis("localhost:6379")}
05

Go + Docker: a static binary in a tiny image

Go compiles to a single binary. With CGO_ENABLED=0 it is statically linked — it needs no libc, no runtime, nothing but the kernel — so the final image can be scratch or distroless and weigh 10–20 MB instead of hundreds. That is a big reason Go dominates container tooling.

Go + Docker

A multi-stage Dockerfile for a Go service

Copying go.mod/go.sum and running go mod download before copying the source lets Docker cache the dependency layer; editing a .go file only rebuilds from COPY . .. -ldflags="-s -w" strips debug symbols to shrink the binary, and -trimpath removes your machine's paths from it. The distroless static image has CA certificates and time-zone data (which scratch lacks) and runs as a non-root user.

dockerfile
# ---- build stage ----
FROM golang:1.27 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download                       # cached until go.mod/go.sum change
COPY . .
RUN go vet ./... && go test ./...
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server

# ---- run stage ----
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]

# docker build -t orders:dev .
# docker run --rm -p 8080:8080 -e DATABASE_URL=... orders:dev
Cross-compiling is free
Go cross-compiles by setting two variables: GOOS=linux GOARCH=arm64 go build on a Mac produces a Linux ARM binary with no extra toolchain. Add a .dockerignore with .git and local build output so COPY . . stays small.
06

Go + Kubernetes: health probes and graceful shutdown

Kubernetes asks your service two questions. Liveness (/healthz): "are you alive?" — if not, it restarts the container. Readiness (/readyz): "should I send you traffic?" — during startup, or while shutting down, the answer is no. Keep liveness trivial (never check the database there, or a database blip restarts every pod); put dependency checks in readiness.

gomain.go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
)

var ready atomic.Bool // false until startup work is done

func healthz(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok") // alive as long as we can answer
}

func readyz(w http.ResponseWriter, r *http.Request) {
	if !ready.Load() {
		http.Error(w, "not ready", http.StatusServiceUnavailable)
		return
	}
	fmt.Fprintln(w, "ready")
}

func probe(mux *http.ServeMux, path string) {
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
	fmt.Print(path, " ", rec.Code, " ", rec.Body.String())
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", healthz)
	mux.HandleFunc("GET /readyz", readyz)

	probe(mux, "/healthz")
	probe(mux, "/readyz")
	ready.Store(true) // e.g. after the DB pool connected and caches warmed
	probe(mux, "/readyz")
}
Outputcompiled & run with real Go
/healthz 200 ok
/readyz 503 not ready
/readyz 200 ready

atomic.Bool because the probe handlers run on request goroutines while startup code flips the flag.

Your turn

On shutdown, a service should fail readiness first so Kubernetes stops routing to it. Add ready.Store(false) and a fourth probe.

Go + Kubernetes

A Deployment with probes, limits and graceful shutdown

Kubernetes sends SIGTERM before killing a pod. The Go side (bottom) catches it with signal.NotifyContext and calls srv.Shutdown, which stops accepting new connections and waits for in-flight requests. GOMEMLIMIT tells the garbage collector about the container's memory limit so it collects harder before the kernel kills the pod. Set it a little below limits.memory.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
spec:
  replicas: 3
  selector:
    matchLabels: { app: orders }
  template:
    metadata:
      labels: { app: orders }
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: orders
          image: ghcr.io/you/orders:1.4.0
          ports:
            - containerPort: 8080
          env:
            - name: GOMEMLIMIT
              value: "230MiB"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: { name: orders-db, key: url }
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits: { memory: 256Mi }
          livenessProbe:
            httpGet: { path: /healthz, port: 8080 }
            periodSeconds: 10
          readinessProbe:
            httpGet: { path: /readyz, port: 8080 }
            periodSeconds: 5

# --- the Go side: graceful shutdown on SIGTERM ---
# ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
# defer stop()
# srv := &http.Server{Addr: ":8080", Handler: mux}
# go srv.ListenAndServe()
# <-ctx.Done()                                   // SIGTERM arrived
# ready.Store(false)                             // fail readiness first
# shutCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
# defer cancel()
# srv.Shutdown(shutCtx)                          // drain in-flight requests
07

Go + Prometheus and Grafana: metrics

Prometheus (itself written in Go) scrapes a /metrics endpoint on each pod every few seconds and stores the numbers as time series; Grafana draws them. The Go client registers Go runtime metrics (goroutines, GC pauses, heap) for free; you add the business ones. The three types you need: a counter only goes up (requests, errors), a gauge goes up and down (queue length, in-flight requests), a histogram buckets durations so you can graph the 95th percentile.

Go + Prometheus

Request count and latency middleware with client_golang

promauto registers each metric when it is created. Labels split one metric into series — keep their values bounded: use the route pattern (r.Pattern, e.g. GET /items/{id}) and the status code, never the raw URL or a user id, or Prometheus will drown in millions of series. promhttp.Handler() serves the text format Prometheus scrapes.

go
package main

import (
	"net/http"
	"strconv"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	requests = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "http_requests_total",
		Help: "HTTP requests by route and status.",
	}, []string{"route", "code"})

	latency = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "http_request_duration_seconds",
		Help:    "HTTP request latency.",
		Buckets: prometheus.DefBuckets,
	}, []string{"route"})
)

type statusWriter struct {
	http.ResponseWriter
	code int
}

func (s *statusWriter) WriteHeader(c int) { s.code = c; s.ResponseWriter.WriteHeader(c) }

func instrument(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		sw := &statusWriter{ResponseWriter: w, code: 200}
		start := time.Now()
		next.ServeHTTP(sw, r)
		requests.WithLabelValues(r.Pattern, strconv.Itoa(sw.code)).Inc()
		latency.WithLabelValues(r.Pattern).Observe(time.Since(start).Seconds())
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})
	mux.Handle("GET /metrics", promhttp.Handler())
	http.ListenAndServe(":8080", instrument(mux))
}
Go + Grafana

PromQL for the Grafana panels every Go service needs

Add Prometheus as a Grafana data source, then build panels from these queries. rate(...[5m]) turns an ever-growing counter into "per second"; histogram_quantile turns histogram buckets into a percentile. The last two use the runtime metrics the Go client exports automatically — a steadily climbing goroutine count is the classic sign of a goroutine leak.

text
# requests per second, per route
sum by (route) (rate(http_requests_total[5m]))

# error ratio (5xx / all)
sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))

# p95 latency per route
histogram_quantile(0.95, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))

# goroutines per pod (leak detector)
go_goroutines{job="orders"}

# heap in use
go_memstats_heap_inuse_bytes{job="orders"}
08

Go + Apache Kafka: a consumer

Kafka is a distributed log: producers append events ("order placed") to a topic, and consumers read them at their own pace. Consumers that share a group id split the topic's partitions between them, so you scale by running more pods. Go is a popular choice for consumers because one small binary handles thousands of messages per second with modest memory.

Go + Apache Kafka

A consumer-group reader with explicit commits (segmentio/kafka-go)

FetchMessage blocks until a message arrives or the context is cancelled (Ctrl+C or SIGTERM here). The offset is committed only after the message is processed, so a crash mid-way means the message is delivered again — "at least once". That makes the handler responsible for being idempotent: processing the same order twice must not charge the customer twice. Other widely used clients are franz-go and confluent-kafka-go.

go
package main

import (
	"context"
	"encoding/json"
	"errors"
	"log/slog"
	"os"
	"os/signal"
	"syscall"

	"github.com/segmentio/kafka-go"
)

type OrderPlaced struct {
	OrderID string  `json:"order_id"`
	Total   float64 `json:"total"`
}

func handle(ctx context.Context, e OrderPlaced) error {
	// idempotent: e.g. INSERT ... ON CONFLICT (order_id) DO NOTHING
	slog.Info("order placed", "id", e.OrderID, "total", e.Total)
	return nil
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	r := kafka.NewReader(kafka.ReaderConfig{
		Brokers: []string{"localhost:9092"},
		GroupID: "billing",
		Topic:   "orders",
	})
	defer r.Close()

	for {
		m, err := r.FetchMessage(ctx)
		if errors.Is(err, context.Canceled) {
			return // shutting down
		}
		if err != nil {
			slog.Error("fetch", "err", err)
			return
		}
		var e OrderPlaced
		if err := json.Unmarshal(m.Value, &e); err != nil {
			slog.Warn("bad message, skipping", "offset", m.Offset, "err", err)
		} else if err := handle(ctx, e); err != nil {
			slog.Error("handle failed; will be redelivered", "err", err)
			continue // do not commit
		}
		if err := r.CommitMessages(ctx, m); err != nil {
			slog.Error("commit", "err", err)
		}
	}
}
Where Kafka fits in a data platform →
Poison messages
A message that can never be parsed would block the partition forever if you refused to commit it. The code above logs and skips it; production systems usually publish it to a dead-letter topic for someone to inspect.
09

Go + GitHub Actions and Git: CI and releases

Go makes CI short because the toolchain already contains the formatter, the linter-lite (go vet), the test runner, the race detector and coverage. What to commit: go.mod and go.sum (always — go.sum pins the exact dependency hashes), your source and tests. What not to commit: built binaries, coverage.out, .env files. Committing a vendor/ folder is a team choice.

Go + GitHub

A GitHub Actions workflow: format, vet, race tests, build

setup-go reads the Go version from go.mod and caches downloaded modules. The gofmt -l step fails the build if any file is not formatted — Go teams do not argue about style, they let the tool decide. -race is on for tests because CI is where races get caught (Module 11).

yaml
# .github/workflows/ci.yml
name: ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - name: gofmt
        run: test -z "$(gofmt -l .)" || (gofmt -l . && exit 1)
      - run: go vet ./...
      - run: go test -race -coverprofile=coverage.out ./...
      - run: go build ./...
      - uses: golangci/golangci-lint-action@v8
        with:
          version: latest

Go modules are versioned by Git tags: pushing v1.4.0 makes go get github.com/you/[email protected] work for everyone, with no package registry to publish to. Tags must be semantic versions with the v prefix; a breaking change needs a new major version and a /v2 suffix on the module path.

bash
# .gitignore for a Go project
/bin/
/dist/
*.exe
*.test
*.out            # coverage.out, cpu.out
.env
.idea/
.vscode/
.DS_Store

# --- the usual loop ---
git switch -c feat/order-cache
go test ./... && git add -A && git commit -m "Add Redis cache for prices"
git push -u origin feat/order-cache       # open a pull request; CI runs

# --- releasing a library version ---
git tag v1.4.0
git push origin v1.4.0
ServeMux
The standard library HTTP router; since Go 1.22 it matches methods and {name} path parameters.
httptest
Standard package that records handler responses and starts throwaway test servers.
Connection pool
A shared set of open database connections (sql.DB, pgxpool.Pool); create once, use from every goroutine.
Placeholder
$1, $2 in SQL: values sent separately from the query text, which prevents SQL injection.
Cache-aside
Read the cache; on a miss, load from the source and write the cache with a TTL.
Static binary
A binary built with CGO_ENABLED=0 that needs no shared libraries, so it runs in a scratch or distroless image.
Liveness / readiness probe
Kubernetes health checks: failing liveness restarts the pod; failing readiness stops traffic to it.
GOMEMLIMIT
A soft memory limit for the Go garbage collector, set just below the container limit.
Consumer group
Kafka consumers sharing a group id, which split a topic's partitions between them.
Quick check

Your Go service runs in Kubernetes and checks the database in its liveness probe. The database has a 30-second outage. What happens?

Quick check

Why does CGO_ENABLED=0 matter when building for a distroless or scratch image?

Frequently asked questions

Should I use Gin or the standard net/http package for a Go REST API?
Since Go 1.22 the standard ServeMux supports methods and path parameters, which is enough for many small services. Gin adds route groups, middleware, JSON binding and validation, and saves boilerplate as the API grows. Both are common in real jobs, and handlers written for either can be tested with httptest.
Why are Go Docker images so small?
A Go program built with CGO_ENABLED=0 is a single statically linked binary with no runtime or shared libraries to install. A multi-stage Dockerfile compiles it in the full golang image and copies only the binary into a scratch or distroless image, which is typically 10 to 20 MB.
What is the best PostgreSQL driver for Go?
pgx (github.com/jackc/pgx/v5) is the standard choice. Use it through database/sql for portability or through pgxpool for better performance and Postgres-specific features, and always pass values as $1-style placeholders.

Finish the Go handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.