Review & Verify Updated 2026-09 10 min read View as Markdown

The Go mistakes language models actually make

The compiler catches most of it. This is the list of what gets through — almost all of it concurrency.

Go's compiler eliminates most of what makes generated code in other languages risky. What survives clusters almost entirely in one area — concurrency — plus a handful of idiom mistakes that come from training data older than the current standard library.

That is a short list, which is the good news. It is also a list of the hardest bugs to find, which is the bad news.

Concurrency#

1. Goroutine leaks#

go
func fetchAll(urls []string) []Result {
    ch := make(chan Result)
    for _, u := range urls {
        go func(u string) { ch <- fetch(u) }(u)   // blocks forever if nobody reads
    }
    var out []Result
    for i := 0; i < 3; i++ {                       // reads 3, started len(urls)
        out = append(out, <-ch)
    }
    return out
}

Every unread goroutine blocks on the send and never exits. Under load the process accumulates goroutines until it dies. Nothing fails in a test.

Correct: buffer the channel to the number of sends, or use errgroup with a context, and always have a way for a goroutine to be cancelled.

Catch it with: go.uber.org/goleak in TestMain. Three lines, and it turns every leak into a test failure.

go
func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }

2. Unbuffered channel deadlock#

go
ch := make(chan int)
ch <- 1          // deadlock: nothing is receiving yet
fmt.Println(<-ch)

Generated code frequently reaches for an unbuffered channel where a buffered one or a sync.WaitGroup is meant. Catch it with: go test -race and a timeout — go test -timeout 30s turns a deadlock into a failure with a full goroutine dump.

3. WaitGroup misuse#

wg.Add(1) inside the goroutine instead of before it; defer wg.Done() missing; passing a WaitGroup by value so each goroutine gets a copy. All three appear regularly and all three produce a program that either exits early or hangs.

4. Unsynchronised shared state#

go
counter := 0
for i := 0; i < 100; i++ {
    go func() { counter++ }()      // data race
}

Catch it with: go test -race. Always. Put it in make check and never take it out — the race detector is the single highest-value tool in Go and it finds this class of bug reliably.

5. Context not propagated, or ignored#

go
func (s *Service) Get(ctx context.Context, id string) (*User, error) {
    return s.db.Query("SELECT ...")        // ctx dropped; no cancellation, no timeout
}

The generated function accepts a context because the signature convention says so, then never uses it. Cancellation and timeouts silently stop working, and a request that the client abandoned keeps a database connection busy.

Catch it with: go vet's lostcancel, plus contextcheck in golangci-lint, plus grep: ctx context.Context in a signature with no ctx in the body is always worth a look.

Errors#

6. Wrapping with %v instead of %w#

go
return fmt.Errorf("load user: %v", err)    // breaks errors.Is / errors.As

The error message looks identical, so nothing appears wrong until a caller's errors.Is(err, sql.ErrNoRows) stops matching. Catch it with: errorlint in golangci-lint.

7. Errors ignored with _#

Explicit, greppable, and still generated — most often on defer f.Close() for a writable file, where the close error is the one that tells you the write failed.

shell
git diff | grep -nE '_ = |_, _ ='

Catch it with: errcheck (included in golangci-lint).

8. Sentinel errors compared with ==#

if err == ErrNotFound fails as soon as anything in the chain wraps it. Use errors.Is. errorlint catches this too.

Types and nil#

9. The nil interface trap#

go
func find() error {
    var e *MyError = nil
    return e              // non-nil interface holding a nil pointer
}

if find() != nil { /* this runs */ }

The canonical Go gotcha, well represented in training data as an example of a bug — which means it also gets reproduced as code. Correct: return nil literally, never a typed nil pointer.

10. Slice aliasing after append#

go
a := []int{1, 2, 3, 4, 5}
b := a[:2]
b = append(b, 99)         // overwrites a[2] — b shares a's backing array

Generated slicing code assumes copies. Use slices.Clone, or a full slice expression a[:2:2] to force reallocation.

11. Map iteration order#

Deliberately randomised in Go. Generated code that builds output by ranging a map produces results that differ between runs — flaky tests, non-reproducible files. Sort the keys.

Stale idioms#

Models trained on older Go write older Go. None of these are wrong, but they are worth correcting in AGENTS.md if your codebase has moved on.

GeneratedCurrent
ioutil.ReadFileos.ReadFile (ioutil deprecated since 1.16)
interface{}any
rand.Seed(time.Now()...)unnecessary since 1.20; math/rand/v2
hand-rolled min/max/containsbuiltins and slices package
x := x loop shadowingunnecessary since 1.22
logrus, zap by reflexlog/slog is stdlib
github.com/pkg/errorsfmt.Errorf with %w

The config that catches most of this#

.golangci.yml
linters:
  enable:
    - errcheck        # ignored errors
    - errorlint       # %w, errors.Is
    - govet           # lostcancel, copylocks, printf
    - staticcheck     # the big one
    - contextcheck    # dropped contexts
    - bodyclose       # unclosed response bodies
    - rowserrcheck
    - noctx           # http requests without a context
    - gosec
makefile
check:
	gofmt -l -w .
	golangci-lint run
	go test -race -count=1 -timeout 60s ./...

-count=1 disables test caching, which matters when an agent is iterating — a cached pass is not a pass.

The short version

In Go, the compiler and golangci-lint handle nearly everything except concurrency. So put -race and goleak in your default test command and spend your review attention on the goroutines. That is where the bugs are.

Common questions#

Is generated Go safer than generated Python?#

For everything the compiler and vet can see, substantially — unused variables, type confusion, unhandled imports, silently swallowed errors are all structurally harder. Concurrency is the exception, and it is exactly where Go's own difficulty is concentrated, so the residual risk is narrow but sharp.

Do I really need -race on every test run?#

Yes. It roughly doubles test time and finds a class of bug that is otherwise found in production, at 3am, non-reproducibly. If your suite is too slow with it, that is an argument for a faster suite rather than for turning it off.

Why does generated Go use deprecated stdlib functions?#

Because ioutil.ReadFile appears in a decade of training data and os.ReadFile in three years of it. It is harmless as such — the functions still work — but it is a useful signal about how current the rest of the generated code is likely to be. A line in AGENTS.md fixes it.

Get the Go agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.

Disclosure: some links on this page are affiliate links. If you buy something through one, we earn a commission at no extra cost to you. We only list tools we would tell a friend to use, and we say so when we have not used something ourselves. This is how the site stays free and ad-light.