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 nothinggo vet ./... # must be silentgo test -race ./... # must be greengovulncheck ./... # must be cleango install golang.org/x/vuln/cmd/govulncheck@latestThe self-review checklist
| # | Check | What you are hunting for |
|---|---|---|
| 1 | Every route’s auth | a handler registered without protect — grep your Routes functions |
| 2 | Every query’s scope | WHERE id = $1 with no AND user_id = $2 |
| 3 | Every query’s SQL | any fmt.Sprintf or + near a query string |
| 4 | Every error return | a bare return err with no context |
| 5 | Every goroutine | one with no ctx.Done() case and no way to exit |
| 6 | Every log line | a password, token, or whole request body |
| 7 | Every list endpoint | a missing limit, or a limit with no ceiling |
| 8 | Every file path | one built from something the client sent |
The anti-patterns to grep for
| Anti-pattern | Fix |
|---|---|
if err != nil { return err } six layers deep | wrap 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 code | a concrete type, or generics |
| an interface with one implementation | delete it; use the struct |
a utils or common package | name it for what it does |
a global var db *sql.DB | a struct field, injected in run() |
time.Sleep to wait for a goroutine | WaitGroup or a channel |
defer inside a loop | move the body into a function |
SELECT * | list the columns |
user.UserService | user.Service — do not stutter |
| a context stored in a struct | first parameter |
| business logic inside a handler | move it into the service |
Attack your own API
terminal
# no tokencurl -i localhost:8080/v1/notes# somebody else's notecurl -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 bodyhead -c 50M /dev/urandom > big.bincurl -i -F "file=@big.bin" -H "Authorization: Bearer $T" \ localhost:8080/v1/notes/1/attachments# a script pretending to be an imagecp evil.sh evil.jpgcurl -i -F "file=@evil.jpg" -H "Authorization: Bearer $T" \ localhost:8080/v1/notes/1/attachments# SQL injection attemptcurl -i "localhost:8080/v1/notes?limit=1;DROP%20TABLE%20users"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
-racebe clean? - Is
ctxthreaded 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
- 1Grep your own routes for a handler missing
protect, and your queries for one missingAND user_id. - 2Run all four tools and fix everything they report before reading a line of your own code.
- 3Run the six attack curls from this topic and record what each returns.
- 4Delete one thing: a helper used once, a commented-out block, or an interface with one implementation.
Official documentation