---
title: "Go pack"
description: "Cards specific to go: stable ones are vetted, drafts are not. Use them with the level page that matches your input."
canonical: "https://goodcodereview.com/pack-go"
updated: "2026-11-06"
---

# Go pack

Each card says whether it is stable or draft. Stable cards are vetted and also appear in their level file. A draft card is not vetted. Report a `FAIL` of one under Draft findings, at `should-fix` at most, never in the ledger or the verdict. Draft cards never change the verdict.

## go-govulncheck-in-ci

severity: should-fix  
concerns: security  
applies_when: The repository is a Go module with a CI pipeline.  
tool gate: govulncheck  
Cites: Google (unjudged). Backing: unjudged.

**Rule.** Run `govulncheck ./...` in CI and fail the build on a reported vulnerability that your code calls.

**Why.** Dependencies pick up known vulnerabilities after you pin them. A scan that only runs on a laptop finds them when someone remembers. govulncheck reports only vulnerabilities in functions your code reaches, so a failing run is worth acting on.

**FAIL**

```yaml
jobs:
  test:
    steps:
      - uses: actions/checkout@v4
      - run: go test ./...
```

**PASS**

```yaml
jobs:
  test:
    steps:
      - uses: actions/checkout@v4
      - run: go test ./...
      - run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
```

**Counterpoint.** "A new advisory breaks an unrelated pull request." That is the signal; the scan needs a fix or a recorded exception. A scheduled run on the main branch keeps the check alive between pull requests.

## go-http-client-timeout

severity: should-fix  
concerns: reliability, performance  
applies_when: A Go function makes an HTTP or other network call.  
context: server, cli  
tool gate: noctx, bodyclose (golangci-lint)  
Cites: Cloudflare (unjudged), Google (unjudged). Backing: unjudged.

**Rule.** Network calls carry a deadline: a `context.Context` passed to `NewRequestWithContext`, or an `http.Client` with `Timeout`, never `http.DefaultClient` without either.

**Why.** A zero timeout means no timeout. One stalled peer hangs the goroutine, and with it the caller, until the process restarts.

**FAIL**

```go
resp, err := http.Get(url)
```

**PASS**

```go
var client = &http.Client{Timeout: 15 * time.Second}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
	return err
}
resp, err := client.Do(req)
```

**Counterpoint.** "Streaming bodies outlive any fixed timeout." Then set per-phase timeouts on the transport and use the context for the total deadline; keep the deadline.

## go-mod-readonly-verified

severity: should-fix  
concerns: security, reliability  
applies_when: The repository is a Go module built in CI.  
tool gate: go mod verify, go mod tidy -diff  
Cites: Google (unjudged). Backing: unjudged.

**Rule.** Commit `go.sum`, build in CI with `-mod=readonly`, and run `go mod verify`.

**Why.** `go.sum` pins the hash of every dependency, so a re-uploaded version fails the build. A build that rewrites `go.mod` on its own hides drift. `go mod verify` catches a tampered module cache.

**FAIL**

```yaml
# go.sum is in .gitignore
- run: go mod tidy && go build -mod=mod ./...
```

**PASS**

```yaml
- run: go mod verify
- run: go build -mod=readonly ./...
- run: go mod tidy -diff
```

**Counterpoint.** "The default is already readonly since Go 1.16." True for `go build`; the explicit flag and the `go.sum` check keep a stray `GOFLAGS=-mod=mod` from changing that.

## go-no-init-side-effects

severity: discuss  
concerns: maintainability, correctness  
applies_when: A Go file defines `func init()`.  
tool gate: gochecknoinits (golangci-lint)  
Cites: Uber (unjudged). Backing: unjudged.

**Rule.** `init()` does no I/O, reads no environment and does not depend on other `init()` functions; do that work from `main()`.

**Why.** `init()` runs on import, in an order readers do not see, before tests can set anything up. A network call or env read there makes importing the package a side effect.

**FAIL**

```go
var db *sql.DB

func init() {
	db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
```

**PASS**

```go
func main() {
	db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	run(db)
}
```

**Counterpoint.** "Registering a driver needs `init()`." Pure registration into a map is deterministic and has no I/O; that is the allowed case.

## go-test-race-in-ci

severity: should-fix  
concerns: correctness, reliability, testing  
applies_when: A Go module uses goroutines, channels or shared state and has CI.  
tool gate: go test -race  
Cites: Google (unjudged). Backing: unjudged.

**Rule.** CI runs the test suite with `go test -race ./...`.

**Why.** Data races pass ordinary tests and fail in production, rarely and without a trace. The detector finds them only when the racing code runs under test, so the flag has to be on in the run that every change passes.

**FAIL**

```yaml
- run: go test ./...
```

**PASS**

```yaml
- run: go test -race ./...
```

**Counterpoint.** "The race detector slows tests and needs cgo." It costs several times the runtime and memory, so run it on every pull request if the suite is fast; otherwise at least on the main branch.

## go-vet-lint-gate

severity: should-fix  
concerns: correctness, maintainability  
applies_when: The repository is a Go module with a CI pipeline.  
tool gate: go vet, staticcheck, errcheck via golangci-lint  
Cites: Uber (unjudged). Backing: unjudged.

**Rule.** CI runs `go vet` and a linter set that includes staticcheck and errcheck, and a finding fails the build.

**Why.** These tools catch unchecked errors, misused `Printf` verbs and dead code before a reviewer sees them. If review comments repeat, the gate is missing. Lint the same way across the whole codebase; consistency matters more than the exact set.

**FAIL**

```yaml
- run: go build ./... && go test ./...
```

**PASS**

```yaml
- run: go vet ./...
- uses: golangci/golangci-lint-action@v6   # .golangci.yml enables errcheck, staticcheck
- run: go test ./...
```

**Counterpoint.** "A new linter floods an old codebase." Enable it with `new-from-rev` so only new code is held to it, then widen.

## go-wrap-errors-with-w

severity: should-fix  
concerns: correctness, reliability  
applies_when: A Go function returns an error it received from a call.  
tool gate: errcheck, errorlint  
Cites: Google (unjudged), Uber (unjudged). Backing: unjudged.

**Rule.** Check every returned `err`; when you add context, wrap with `fmt.Errorf("...: %w", err)` unless the cause must stay hidden.

**Why.** `%v` flattens the cause into text, so callers cannot use `errors.Is` or `errors.As`. An ignored `err` turns a failure into silent wrong output.

**FAIL**

```go
data, _ := os.ReadFile(path)
if err := json.Unmarshal(data, &cfg); err != nil {
	return fmt.Errorf("parse config: %v", err)
}
```

**PASS**

```go
data, err := os.ReadFile(path)
if err != nil {
	return fmt.Errorf("read config: %w", err)
}
if err := json.Unmarshal(data, &cfg); err != nil {
	return fmt.Errorf("parse config: %w", err)
}
```

**Counterpoint.** "`%w` makes the wrapped error part of my API." Right: callers may depend on it. Use `%v` at a package boundary when you do not want that, and say so.
