Dev Skills

Harden it, and review your own code

Read what you wrote as if a stranger wrote it, hunting for eight things that commonly break.

Topic 86 of 98 · 5 min to read · 50 min to build
After this topic you can
  • Run vet, staticcheck, the race detector and govulncheck before review.
  • Find common anti-patterns in your own code with the self-review checklist.
  • Attack your own API with bad input, missing auth and other users' IDs.

What hardening and self-review mean

Hardening is going back over working code looking for ways it could be misused. You are not hunting bugs that show up in normal use, but the request nobody sends by accident.

Self-review is reading your own diff as if a stranger wrote it and you have to maintain it. It is uncomfortable, and it pays well: every problem you find now is one you do not find in production.

The order matters: run the automated tools first, so you spend your own attention on what they cannot see.

Run the tools first

terminal
gofmt -l . # must print nothing
go vet ./... # must be silent
go test -race ./... # must be green
govulncheck ./... # must be clean
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck is the Go team's tool. It is low-noise because it reports vulnerabilities in functions your code can reach, not every CVE in your dependency tree.

The self-review checklist

#CheckWhat you are hunting for
1Every route’s autha handler registered without protect — grep your Routes functions
2Every query’s scopeWHERE id = $1 with no AND user_id = $2
3Every query’s SQLany fmt.Sprintf or + near a query string
4Every error returna bare return err with no context
5Every goroutineone with no ctx.Done() case and no way to exit
6Every log linea password, token, or whole request body
7Every list endpointa missing limit, or a limit with no ceiling
8Every file pathone built from something the client sent

The anti-patterns to grep for

Anti-patternFix
if err != nil { return err } six layers deepwrap with %w and what you were doing
an ignored error with _handle it, or write a comment saying why it is safe to skip
interface{} / any in your own codea concrete type, or generics
an interface with one implementationdelete it; use the struct
a utils or common packagename it for what it does
a global var db *sql.DBa struct field, injected in run()
time.Sleep to wait for a goroutineWaitGroup or a channel
defer inside a loopmove the body into a function
SELECT *list the columns
user.UserServiceuser.Service — do not stutter
a context stored in a structfirst parameter
business logic inside a handlermove it into the service

Attack your own API

terminal
# no token
curl -i localhost:8080/v1/notes
# somebody else's note
curl -i -H "Authorization: Bearer $B_TOKEN" localhost:8080/v1/notes/$A_NOTE_ID
# a tampered token (change one character of the signature)
curl -i -H "Authorization: Bearer $TAMPERED" localhost:8080/v1/me
# oversized body
head -c 50M /dev/urandom > big.bin
curl -i -F "file=@big.bin" -H "Authorization: Bearer $T" \
localhost:8080/v1/notes/1/attachments
# a script pretending to be an image
cp evil.sh evil.jpg
curl -i -F "file=@evil.jpg" -H "Authorization: Bearer $T" \
localhost:8080/v1/notes/1/attachments
# SQL injection attempt
curl -i "localhost:8080/v1/notes?limit=1;DROP%20TABLE%20users"
Expected: 401, 404, 401, 413, 415, and for the last one a 400 (or your default limit, if you fall back on bad input). Never a 500. If any of those surprises you, you found today’s work.

The eight questions a Go reviewer asks

  • Are all errors handled, and wrapped with context?
  • Can any goroutine leak? Is every one bounded and cancellable?
  • Is shared state guarded? Would -race be clean?
  • Is ctx threaded through to every I/O call?
  • Is every query parameterised and owner-scoped?
  • Do the tests cover the error paths, not just the happy one?
  • Is there an abstraction here that nobody asked for?
  • Would a new hire understand this in six months?
Note: Then delete something. Most codebases this age have a helper used once, a commented-out block, an interface with one implementation, or a config value that never changes. Each one you delete is code nobody has to read again.
The short version
  • gofmt, vet, race, govulncheck: all four clean before you review anything by hand.
  • The eight-point sweep: auth, scope, SQL, errors, goroutines, logs, limits, paths.
  • Attack your own API with curl. Six requests cover the most common holes.
  • Delete something. Unused abstraction is the debt you are least likely to notice.
Remember: Reviewing your own code means reading it as if someone else wrote it and you have to maintain it. Be unkind to it now so production is kind to you later.
Build this before moving on
Run all four tools, work the eight-point checklist line by line, run every attack above, and fix what falls out. Then delete at least one thing.
Then try these in your own editor
  1. 1Grep your own routes for a handler missing protect, and your queries for one missing AND user_id.
  2. 2Run all four tools and fix everything they report before reading a line of your own code.
  3. 3Run the six attack curls from this topic and record what each returns.
  4. 4Delete one thing: a helper used once, a commented-out block, or an interface with one implementation.
Check yourselfTake the Production test