Writing Better Go GoLab 2025
Writing Better Go GoLab 2025
result, _ := pickRandom(input)
Handle Errors
BAD: Silently Discarding
result, _ := pickRandom(input)
}
Handle Errors
BAD: Swallowing the Error
func main() {
invalid := &[Link]{}
resp, err := fetch(invalid)
if err != nil {
[Link]("fetch failed", "error", err)
}
[Link]([Link])
}
panic: runtime error: invalid memory
address or nil pointer dereference
[signal SIGSEGV: segmentation
violation code=0x1 addr=0x0
pc=0x6461ca]
Handle Errors
BAD: Ambiguous Contract
return result, nil ✅ Good The result is valid and safe to use.
return nil, err ✅ Good The result is invalid; handle the error.
return nil, nil ❌ Bad Ambiguous case forces extra nil checks.
return result, nil ✅ Good The result is valid and safe to use.
return nil, err ✅ Good The result is invalid; handle the error.
return nil, nil ❌ Bad Ambiguous case forces extra nil checks.
● Premature Abstraction
Adding Interfaces Too Soon
Two Common Misuses
● Premature Abstraction
● Support Testing
Adding Interfaces Too Soon
Two Common Misuses
● Premature Abstraction
Introduced prematurely by following object-oriented patterns
from languages like Java. While well-intentioned, this approach
adds unnecessary complexity early in the development process.
● Support Testing
Adding Interfaces Too Soon
Premature Abstraction
import (
"context"
)
import (
"container/heap"
"container/list"
"context"
"sync"
)
import (
"container/heap"
"container/list"
"context"
"sync"
)
mu [Link]
data map[string]*lruItem[T]
queue *[Link]
}
package cache
import (
"container/heap"
"container/list"
"context"
"sync"
)
cache/
├── [Link]
├── [Link]
├── ...
└── [Link]
Adding Interfaces Too Soon
Premature Abstraction
cache/
├── [Link]
├── [Link]
├── ...
├── [Link]
└── [Link]
Adding Interfaces Too Soon
Premature Abstraction
cache/
└── [Link]
Adding Interfaces Too Soon
Premature Abstraction
func NewEligibilityService(
catalog *[Link],
cache [Link][*[Link]],
) *EligibilityService {
return &EligibilityService{
catalog: catalog,
catalogCache: catalogCache,
}
}
Adding Interfaces Too Soon
Premature Abstraction
● Premature Abstraction
Introduced prematurely by following object-oriented patterns
from languages like Java. While well-intentioned, this approach
adds unnecessary complexity early in the development process.
● Support Testing
Adding Interfaces Too Soon
Two Common Misuses
● Premature Abstraction
Introduced prematurely by following object-oriented patterns
from languages like Java. While well-intentioned, this approach
adds unnecessary complexity early in the development process.
● Support Testing
A practice that relies heavily on mocking dependencies that
unblocks your productivity in the short term, but weakens the
expressiveness of your types and reduces code readability in the
long run.
Adding Interfaces Too Soon
Support Testing
catalog := [Link]()
svc := New(userService, catalog)
result, err := [Link]([Link](), "test_user_1", "ad-free")
if err != nil {
[Link](err)
}
if got, want := result, true; got != want {
[Link]("got %t, want: %t", got, want)
}
}
func TestService_IsEligible(t *testing.T) {
userService := ???
catalog := [Link]()
svc := New(userService, catalog)
result, err := [Link]([Link](), "test_user_1", "ad-free")
if err != nil {
[Link](err)
}
if got, want := result, true; got != want {
[Link]("got %t, want: %t", got, want)
}
}
Adding Interfaces Too Soon
Support Testing
catalog := [Link]()
svc := New(userService, catalog)
result, err := [Link]([Link](), "test_user_1", "ad-free")
if err != nil {
[Link](err)
}
if got, want := result, true; got != want {
[Link]("got %t, want: %t", got, want)
}
}
func TestService_IsEligible(t *testing.T) {
catalog := [Link]()
svc := New(userService, catalog)
result, err := [Link]([Link](), "test_user_1", "ad-free")
if err != nil {
[Link](err)
}
if got, want := result, true; got != want {
[Link]("got %t, want: %t", got, want)
}
}
func TestService_IsEligible(t *testing.T) {
userService := &mockUserService{
getUser: func([Link], string) (*[Link], error) {
return &[Link]{
Id: "test_user_1",
Subscription: 1,
}, nil
},
}
catalog := [Link]()
svc := New(userService, catalog)
result, err := [Link]([Link](), "test_user_1", "ad-free")
if err != nil {
[Link](err)
}
if got, want := result, true; got != want {
[Link]("got %t, want: %t", got, want)
}
}
func TestService_IsEligible(t *testing.T) {
userService := &mockUserService{
getUser: func([Link], string) (*[Link], error) {
return &[Link]{
Id: "test_user_1",
Subscription: 1, Service client wrapper
}, nil package has 0% test
}, coverage.
}
catalog := [Link]()
svc := New(userService, catalog)
result, err := [Link]([Link](), "test_user_1", "ad-free")
if err != nil {
[Link](err)
}
if got, want := result, true; got != want {
[Link]("got %t, want: %t", got, want)
}
}
dep
└── user_service.go
dep
├── deptest
└── user_service.go
dep
├── deptest
│ ├── [Link]
│ └── fakeuserservice
│ └── [Link]
└── user_service.go
type Service struct {
subscriptionByUser map[string]int
[Link]
}
[Link](srv)
go func() {
if err := [Link](lis); err != nil {
[Link](err)
}
}()
[Link]([Link])
bufDialer := func(ctx [Link], addr string) ([Link], error) {
return [Link](ctx)
}
opts = append([][Link]{
[Link](bufDialer),
[Link]([Link]()),
}, opts...)
catalog := [Link]()
svc := New(userService, catalog)
result, err := [Link]([Link](), "test_user_1", "ad-free")
if err != nil {
[Link](err)
}
if got, want := result, true; got != want {
[Link]("got %t, want: %t", got, want)
}
}
func TestService_IsEligible(t *testing.T) {
userService := [Link](
t,
[Link]("test_user_1", 1),
)
catalog := [Link]()
svc := New(userService, catalog)
result, err := [Link]([Link](), "test_user_1", "ad-free")
if err != nil {
[Link](err)
}
if got, want := result, true; got != want {
[Link]("got %t, want: %t", got, want)
}
}
Adding Interfaces Too Soon
Support Testing
ch := make(chan int)
close(ch)
close(ch)
panic: close of closed channel
Mutexes Before Channels
Sending on a Closed Channel
ch := make(chan int)
close(ch)
ch <- 3
panic: send on closed channel
Mutexes Before Channels
Sending without Receiver
ch := make(chan int)
ch <- 3
close(ch)
fatal error: all goroutines are
asleep - deadlock!
Mutexes Before Channels
Ranging over a Channel That’s Never Closed
ch := make(chan int)
go func() {
ch <- 1
}()
for v := range ch {
[Link](v)
}
fatal error: all goroutines are
asleep - deadlock!
ch := make(chan int) var resps []int
errors := make(chan error) for {
done := make(chan struct{}) select {
case resp := <-ch:
var wg [Link] resps = append(resps, resp)
for _, v := range input { case err := <-errors:
[Link](func() { return 0, err
resp, err := process(ctx, v) case <-done:
if err != nil { return merge(resps...), nil
errors <- err case <-[Link]():
} return 0, [Link]()
ch <- resp }
}) }
}
go func() {
[Link]()
close(done)
}()
ch := make(chan int) var resps []int
errors := make(chan error) for {
done := make(chan struct{}) select {
case resp := <-ch:
var wg [Link] resps = append(resps, resp)
for _, v := range input { case err := <-errors:
[Link](func() { return 0, err
resp, err := process(ctx, v) case <-done:
if err != nil { return merge(resps...), nil
errors <- err case <-[Link]():
} return 0, [Link]()
ch <- resp }
}) }
}
go func() {
[Link]()
close(done)
}()
ch := make(chan int) var resps []int
errors := make(chan error) for {
done := make(chan struct{}) select {
case resp := <-ch:
var wg [Link] resps = append(resps, resp)
for _, v := range input { case err := <-errors:
[Link](func() { return 0, err
resp, err := process(ctx, v) case <-done:
if err != nil { return merge(resps...), nil
errors <- err case <-[Link]():
} return 0, [Link]()
ch <- resp }
}) }
}
go func() {
[Link]()
close(done)
}()
ch := make(chan int) var resps []int
errors := make(chan error) for {
done := make(chan struct{}) select {
case resp := <-ch:
g, ctx := [Link](ctx) resps = append(resps, resp)
for _, v := range input { case err := <-errors:
[Link](func() error { return 0, err
resp, err := process(ctx, v) case <-done:
if err != nil { return merge(resps...), nil
errors <- err case <-[Link]():
} return 0, [Link]()
ch <- resp }
}) }
}
g, ctx := [Link](ctx)
for _, v := range input {
[Link](func() error {
resp, err := process(ctx, v)
if err != nil {
return err
}
[Link]()
resps = append(resps, resp)
[Link]()
})
}
resps[i] = resp
return nil
})
}
if err := [Link](); err != nil {
return 0, err
}
return merge(resps...), nil
resps := make([]int, len(input))
g, ctx := [Link](ctx)
for i, v := range input {
[Link](func() error {
resp, err := process(ctx, v)
if err != nil {
return err
}
resps[i] = resp
return nil
})
}
if err := [Link](); err != nil {
return 0, err
}
return merge(resps...), nil
resps := make([]int, len(input))
g, ctx := [Link](ctx)
for i, v := range input {
[Link](func() error {
resp, err := process(ctx, v)
if err != nil {
return err
}
resps[i] = resp
return nil
})
}
if err := [Link](); err != nil {
return 0, err
}
return merge(resps...), nil
Mutexes Before Channels
Start Simple, Advance One Step At a Time
err := [Link]()
if err != nil {
return nil, err
}
Declare Close to Usage
Limit Assignment Scope
err = [Link]()
if err != nil {
return nil, err
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
var authErr error
if auth != nil {
authErr = auth(func() error {
results, err = [Link](queries)
return err
})
if authErr != nil {
return nil, err
}
} else {
results, err = [Link](queries)
if err != nil {
return nil, err
}
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
var authErr error
if auth != nil {
authErr = auth(func() error {
results, err = [Link](queries)
return err
})
if authErr != nil {
return nil, err
}
} else { Variable declaration,
results, err = [Link](queries) assignment, and use
if err != nil { are spread out. Is that
return nil, err necessary?
}
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
if auth != nil {
var authErr error
authErr = auth(func() error {
results, err = [Link](queries)
return err
})
if authErr != nil {
return nil, err
}
} else {
results, err = [Link](queries)
if err != nil {
return nil, err
}
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
if auth != nil {
authErr := auth(func() error {
results, err = [Link](queries)
return err
})
if authErr != nil {
return nil, err
}
} else {
results, err = [Link](queries)
if err != nil {
return nil, err
}
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
if auth != nil {
authErr := auth(func() error {
results, err = [Link](queries)
return err
})
We check one error
if authErr != nil {
but return the other.
return nil, err
}
} else {
results, err = [Link](queries)
if err != nil {
return nil, err
}
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
if auth != nil {
err := auth(func() error {
results, err = [Link](queries)
return err
})
if err != nil {
return nil, err
}
} else {
results, err = [Link](queries)
if err != nil {
return nil, err
}
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
if auth != nil {
err := auth(func() error {
results, err = [Link](queries)
return err
})
if err != nil {
return nil, err
}
return results, nil
} else {
results, err = [Link](queries)
if err != nil {
return nil, err
}
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
if auth != nil {
err := auth(func() error {
results, err = [Link](queries)
return err
})
if err != nil {
return nil, err
}
return results, nil
}
results, err := [Link](queries)
if err != nil {
return nil, err
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
if auth != nil {
var results []string
var err error
err := auth(func() error {
results, err = [Link](queries)
return err
})
if err != nil {
return nil, err
}
return results, nil
}
results, err := [Link](queries)
if err != nil {
return nil, err
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
if auth != nil {
var results []string
var err error
err := auth(func() error {
results, err = [Link](queries)
return err
})
if err != nil {
return nil, err
}
return results, nil
}
return [Link](queries)
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
if auth != nil {
var results []string
err := auth(func() (err error) {
results, err = [Link](queries)
return err
})
if err != nil {
return nil, err
}
return results, nil
}
return [Link](queries)
}
Declare Close to Usage
Don’t Let Your Ingredients Dry Out
When to Check
● If data comes from outside (requests, external stores), validate it first.
● Protect yourself from runtime panics on inputs you don’t control.
func main() {
db, err := [Link]()
if err != nil {
[Link](err)
}
job := [Link]("indexer", *db)
if err := [Link](); err != nil {
[Link](err)
}
}
Runtime Panics
Design for Pointer Safety
func main() {
db, err := [Link]()
if err != nil {
[Link](err)
}
job := [Link]("indexer", db)
if err := [Link](); err != nil {
[Link](err)
}
}
06
Minimize
Indentatio
n
func fetch(auth auth, client Client, queries []string) ([]string, error) {
var results []string
var err error
var authErr error
if auth != nil {
authErr = auth(func() error {
results, err = [Link](queries)
return err
})
if authErr != nil {
return nil, err
}
} else {
results, err = [Link](queries)
if err != nil {
return nil, err
}
}
return results, nil
}
func fetch(auth auth, client Client, queries []string) ([]string, error) {
if auth != nil {
var results []string
err := auth(func() (err error) {
results, err = [Link](queries)
return err
})
if err != nil {
return nil, err
}
return results, nil
}
return [Link](queries)
}
Minimize Indentation
BAD: Wraps All Logic Inside
Avoid Catch-All
Packages and
Files
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
interfaces/[Link]
“
Sweatpants are a
sign of defeat. You
lost control of your
life, so you bought
some sweatpants.
Karl Lagerfeld
“
Util packages are a
sign of defeat. You
lost control of your
code base, so you
created some util
packages.
Gnarl Largerfur
NO
TA
“
CTU
AL
CO
DE
REV
Util packages are a I EW
CO
M
sign of defeat. You ME
NT
Order
Declarations by
Importance
Order Declarations by Importance
Most Important Code to the Top
In Go, functions don’t need to be declared before use (no forward declarations).
Order functions by importance, not by dependency. This way, readers see the
most important entry points up front.
Order Declarations by Importance
Most Important Code to the Top
Name Well
Name Well
Avoid Type Suffix
userMap map[string]*User
idStr string
injectFn func()
Name Well
Avoid Type Suffix
Variable names should describe contents, not type. Adding type info
makes code less clear and no more type safe:
userMap map[string]*User
idStr string
injectFn func()
Name Well
Avoid Type Suffix
Variable names should describe contents, not type. Adding type info
makes code less clear and no more type safe:
userMap map[string]*User
idStr string
injectFn func()
❌ BAD
Name Well
Avoid Type Suffix
Variable names should describe contents, not type. Adding type info
makes code less clear and no more type safe:
✅ Good ❌ BAD
Name Well
Variable Length
Let the following metric guide you: the bigger the scope of
declaring a variable and using it, the less likely it should have a
very short or cryptic name.
Name Well
Packages and Exported Identifiers
db := [Link](...)
_, err := [Link](0, [Link])
b := [Link](curve, x, y)
c := [Link](...)
Name Well
Packages and Exported Identifiers
db := [Link](...)
_, err := [Link](0, [Link])
b := [Link](curve, x, y)
c := [Link](...)
10
Document the
Why,
Not the What
Document the Why, Not the What
Justify the Code’s Existence
Write for the future reader (that’s you too). Explain the why!
When writing comments, your goal is to communicate purpose, not just restate the code. A
meaningful description should answer why the change is needed and how you are solving it.
Readers can usually see what the code does, but often struggle to understand why it was
written in the first place.
Summary Summar Su
y y
ar Summar Summar
y y
Summar Summar S
y y y
r Summar Summar
y y
Writing Better Go
The Bigger Picture
Most “style” comments aren’t The real goal isn’t perfection, Code review isn’t just about
about aesthetics — they’re it’s reducing friction: for shipping features safely. It’s
about avoiding real readers, maintainers, and where we teach, learn, and
production pain. your future self. build shared intuition
together.
Patterns repeat: what looks Every rule of thumb (error
like nitpicking in one pull handling, interfaces, mutexes,
request often shows up later naming) is really about the
as a bug, an outage, or same thing: make the code
unreadable code. obvious, safe, and easy to
move forward.
Thank you
Questions?