After shipping a transaction bug, I built a linter
Key point
The story of building a custom linter that catches DB calls escaping Go transaction boundaries.
Details
After experiencing a production incident caused by DB calls leaking outside transaction boundaries, I built a custom linter for Go that catches this at compile time.
The core bug is accidentally using an outer repository like s.repo instead of tx inside a Transaction callback. The code compiles fine and tests pass, but in reality some DB operations end up running outside the transaction, which can lead to data corruption or race conditions.
To catch this, I used Go's go/analysis framework. I built an analysis.Analyzer, used inspect.Analyzer to efficiently traverse the AST and find Transaction calls. Then I analyze inside the callback to check for the following:
- Whether a repo method call uses an outer repository instead of the transaction parameter
- Whether an outer repository is passed instead of the transaction repository when passing function arguments
- Whether it follows helper function chains recursively, catching indirect violations that go through multiple steps
The transaction parameter is tracked by comparing types.Object rather than simple name comparison. This allows accurate identification of the same identifier even when variable names are shadowed.
Also, nested Transaction calls are treated as a separate scope and the current analysis stops there, and a visited function set prevents infinite recursion.
Finally, I set up tests using analysistest to organize a flow that verifies both valid code and violation cases together. Overall, this post concretely demonstrates how to statically detect real-world bug patterns using Go AST analysis.
This summary was generated automatically by AI. Check the original for the author's claims and context. Copyright belongs to the original author.
Our guide explains how the AI works. Report summary errors, attribution issues, or removal requests via Contact.