Writing Go code that works is easy. Writing Go code that lasts? That takes practice.
After working on production systems in Go for several years β across SaaS platforms, cloud-native backends, and developer tooling β Iβve collected a set of battle-tested best practices that have helped me write maintainable, clean, and scalable Go code.
π§ 0. Agree on Code Style Before You Write a Line
Before starting any development, align on a shared code style with your team.
This prevents unnecessary friction during code reviews, ensures consistency, and reduces the mental overhead of switching between files written by different developers.
A great starting point is the Google Go Style Guide β it’s clear, opinionated, and battle-tested at scale. You can automate style enforcement with:
gofmt
/goimports
for formattinggolangci-lint
to enforce idiomatic Go practices
Establishing your code style early also makes onboarding faster and simplifies collaboration β especially in cross-functional teams or open source projects.
β 1. Keep it Simple
Go is intentionally minimal β embrace it.
- Avoid over-engineering.
- Prefer composition over inheritance.
- Use plain interfaces and simple data structures.
- Donβt abstract too early β write the concrete code first.
𧱠2. Project Structure Matters
Use a predictable layout:
|
|
Stick to convention. Tools like golang-standards/project-layout
are a great starting point β but adapt it to your teamβs needs.
π§ͺ 3. Tests Are Not Optional
- Use table-driven tests
- Use
testing
, and only bring in libraries liketestify
if you really need them - Keep unit tests fast and independent
- Use
go test -cover
to check coverage
β¨ 4. Errors Are First-Class Citizens
- Always check errors β no exceptions.
- Wrap errors with context using
fmt.Errorf("failed to read config: %w", err)
- For complex systems, consider using
errors.Join
orerrors.Is/As
for proper error handling.
π¦ 5. Use Interfaces at the Boundaries
Keep interfaces small, and only expose them where needed:
|
|
Donβt write interfaces for everything β only where mocking or substitution matters (e.g. storage, HTTP clients, etc.).
π§° 6. Tooling Makes You Better
- Use go vet, staticcheck, and golangci-lint
- Automate formatting: gofmt, goimports
- Use go mod tidy to keep your dependencies clean
- Pin tool versions with a
tools.go
file - π Use SonarQube for static code analysis at scale
SonarQube helps enforce code quality and security standards across large codebases. It can detect bugs, vulnerabilities, code smells, and even provide actionable remediation guidance. Integrate it into your CI pipeline to ensure every PR gets automatically analyzed.
You can use sonar-scanner
or a Docker-based runner like:
|
|
SonarQube works great alongside golangci-lint, giving you both quick feedback locally and deep insights via the web dashboard.
π 7. Secure By Default
- Always set timeouts on HTTP clients and servers
- Avoid leaking secrets in logs
- Validate all inputs β especially on the API boundary
- Use context.Context consistently and propagate it properly
π 8. Embrace the Go Ecosystem
- Use standard library wherever possible β it’s well-tested and fast
- Prefer established, well-maintained packages
- Read source code β Go makes it easy to learn from the best
π 9. Performance Matters (but correctness first)
- Profile with
pprof
- Avoid allocations in tight loops
- Use channels, but donβt abuse goroutines
- Benchmark with go test -bench
π§ 10. Readability > Cleverness
Your code will be read 10x more than itβs written.
"Write code for humans, not machines."
Stick to idiomatic Go β use golangci-lint to enforce consistency, and always code with your teammates in mind.
π Conclusion
Go is an incredible tool for building fast, reliable software β but like any tool, it shines brightest in the hands of developers who respect its philosophy: clarity, simplicity, and composability.
What are your favorite Go best practices? Let me know on Twitter or GitHub @norbix!
π Follow me on norbix.dev for more insights on Go, system design, and engineering wisdom.