0% found this document useful (0 votes)
7 views174 pages

Writing Better Go GoLab 2025

Uploaded by

arce.jose.sn365
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views174 pages

Writing Better Go GoLab 2025

Uploaded by

arce.jose.sn365
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Writing Better Go

Lessons from 10 Code Reviews


Konrad Reiche
GoLab 2025
“Rename this variable.”
“Indent this line.”
“Did you even install
gofmt?”
“Don’t put a blank line
between assigning an
error and checking it.”
Why Bother?
The View From the Review Queue

Small style comments often resurface


as real production issues.

Code review is where we learn,


whether we want to or not.

Reviewing hundreds of pull requests


turned repetition into patterns and
patterns into guidance.
Why Bother? Code Reviewed per Week

The View From the Review Queue vs.


Code Written per Week

Small style comments often resurface


as real production issues.

Code review is where we learn,


whether we want to or not.

Reviewing hundreds of pull requests


turned repetition into patterns and
patterns into guidance.
01
Handle
Errors
Handle Errors
BAD: Silently Discarding

result, _ := pickRandom(input)
Handle Errors
BAD: Silently Discarding

result, _ := pickRandom(input)

func pickRandom(input []string) (string, error)


Handle Errors
BAD: Silently Ignoring

result, err := pickRandom(input)


if err != nil {

}
Handle Errors
BAD: Swallowing the Error

result, err := pickRandom(input)


if err != nil {
return nil
}
Handle Errors
Good: Checking and Handling the Error

result, err := pickRandom(input)


if err != nil {
return err
}
Handle Errors
Good: Checking and Handling the Error

result, err := pickRandom(input)


if err != nil {
[Link]()
return nil
}
Handle Errors
Good: Checking and Handling the Error

result, err := pickRandom(input)


if err != nil {
[Link]("pickRandom failed", "error", err)
return nil
}
Handle Errors
BAD: Double Reporting

result, err := pickRandom(input)


if err != nil {
[Link]("pickRandom failed", "error", err)
return err
}
Handle Errors
BAD: Double Reporting

result, err := pickRandom(input)


if err != nil {
[Link]("pickRandom failed", "error", err)
return err
}

Log it, or return it — but not both.


Handle Errors
Good: Checking and Handling the Error + Contexualizing

result, err := pickRandom(input)


if err != nil {
return [Link]("pickRandom failed: %w", err)
}
Handle Errors

resp, err := [Link](req)


if err != nil {
return resp, err
}
func fetch(req *[Link]) (*[Link], error) {
resp, err := [Link](req)
if err != nil {
return resp, err
}
return resp, nil
}

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

resp, err := [Link](req)


if err != nil {
return resp, err
}
Handle Errors
Optimize for the Caller

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, err ❌ Bad Unclear which value to trust.


Handle Errors
Optimize for the Caller

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, err ⚠ Bad Unclear which value to trust.


Unless you need to return partial results;
document explicitly.
02
Adding
Interfaces
Too Soon
Adding Interfaces Too Soon
Two Common Misuses

● 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

type EligibilityService struct {


catalog *[Link]
}

func (e *EligibilityService) IsEligible(


ctx [Link],
userID string,
productID string,
) (bool, error) {
// ...
}
package cache

import (
"context"
)

type Cache[T any] interface {


Get(ctx [Link], key string) (*T, error)
Set(ctx [Link], key string, value T) error
}
Adding Interfaces Too Soon
Premature Abstraction

type EligibilityService struct {


cache [Link][[Link]]
catalog *[Link]
}

func (e *EligibilityService) IsEligible(


ctx [Link],
userID string,
productID string,
) (bool, error) {
// ...
}
Adding Interfaces Too Soon
Premature Abstraction

type EligibilityService struct {


cache [Link][[Link]]
catalog *[Link] 👆
}
package cache

import (
"container/heap"
"container/list"
"context"
"sync"
)

type Cache[T any] interface {


Get(ctx [Link], key string) (*T, error)
Set(ctx [Link], key string, value T) error
}

type LFU[T any] struct {


size int
mu [Link]
data map[string]*lfuItem[T]
heap *MinHeap[T]
}
package cache

import (
"container/heap"
"container/list"
"context"
"sync"
)

type Cache[T any] interface {


Get(ctx [Link], key string) (*T, error)
Set(ctx [Link], key string, value T) error
}

type LRU[T any] struct {


size int

mu [Link]
data map[string]*lruItem[T]
queue *[Link]
}
package cache

import (
"container/heap"
"container/list"
"context"
"sync"
)

type Cache[T any] interface {


Get(ctx [Link], key string) (*T, error)
Set(ctx [Link], key string, value T) error
}
Adding Interfaces Too Soon
Premature Abstraction

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

type EligibilityService struct {


cache [Link][[Link]]
catalog *[Link]
}

func (e *EligibilityService) IsEligible(


ctx [Link],
userID string,
productID string,
) (bool, error) {
// ...
}
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

type EligibilityService struct {


cache [Link][[Link]]
catalog *[Link]
}
Adding Interfaces Too Soon
Premature Abstraction

type EligibilityService struct {


cache *[Link][[Link]]
catalog *[Link]
}
Adding Interfaces Too Soon
Premature Abstraction

type EligibilityService struct {


cache *[Link][[Link]]
catalog *[Link]
}

Ask yourself if you really need


multiple implementations before
adding the extra layer of indirection.
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
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

type EligibilityService struct {


cache *[Link][[Link]]
catalog *[Link]
}
Adding Interfaces Too Soon
Support Testing

type EligibilityService struct {


cache *[Link][[Link]]
catalog *[Link]
userService *[Link]
}
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 := ???
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

type EligibilityService struct {


cache *[Link][[Link]]
catalog *[Link]
userService *[Link]
}
Adding Interfaces Too Soon
Support Testing

type EligibilityService struct {


cache *[Link][[Link]]
catalog *[Link]
userService userService
}

type userService interface {


GetUser([Link], string) (*[Link], error)
}
type mockUserService struct {
getUser func(ctx [Link], userID string) (*[Link], error)
}

func (m *mockUserService) GetUser(


ctx [Link],
userID string,
) (*[Link], error) {
if [Link] != nil {
return [Link](ctx, userID)
}
return nil, [Link]("[Link]: not implemented")
}
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) {

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]
}

func New(tb [Link], opts ...func(*options)) *[Link] {


options := options{
subscriptionByUser: make(map[string]int),
}
for _, opt := range opts {
opt(&options)
}
svc := &Service{
subscriptionByUser: [Link],
}
return [Link](tb, svc, [Link])
}
func (s *Service) GetUser(
ctx [Link],
req *[Link],
) (*[Link], error) {
subscription, ok := [Link][[Link]]
if !ok {
return nil, [Link]("user not found")
}
return &[Link]{
Id: [Link],
Subscription: int64(subscription),
}, nil
}

func (s *Service) RegisterOn(srv *[Link]) {


[Link](srv, s)
}
type options struct {
subscriptionByUser map[string]int
}

func WithUserSubscriptions(subscriptionByUser map[string]int) func(*options) {


return func(options *options) {
[Link] = subscriptionByUser
}
}

func WithUserSubscription(userID string, subscription int) func(*options) {


return func(options *options) {
[Link][userID] = subscription
}
}
func NewFake[T any](
tb [Link],
svc registerer,
newServiceClient func(addr string, opts ...[Link]) (T, error),
opts ...[Link],
) T {
[Link]()

lis := [Link](1024 * 1024)


srv := [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...)

client, err := newServiceClient(


"passthrough:bufnet",
opts...,
)
if err != nil {
[Link](err)
}
return client
}
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 := [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)
}
}
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

type EligibilityService struct {


cache *[Link][[Link]]
catalog *[Link]
userService userService
}

type userService interface {


GetUser([Link], string) (*[Link], error)
}
Adding Interfaces Too Soon
Support Testing

type EligibilityService struct {


cache *[Link][[Link]]
catalog *[Link]
userService *[Link]
} We have eliminated the need for
extra types. The code under test is
fully covered and we have made
writing tests easier with dedicated
test helper packages.
Adding Interfaces Too Soon
The Right Time

Don’t Start With Interfaces


● Follow the convention: accept interfaces, return concrete types.
● Begin with a concrete type. Only when you truly need multiple interchangeable types,
start to consider introducing interfaces..
● Litmus Test: If you can write it without, you probably don’t need an interface.
Adding Interfaces Too Soon
The Right Time

Don’t Start With Interfaces


● Follow the convention: accept interfaces, return concrete types.
● Begin with a concrete type. Only when you truly need multiple interchangeable types,
start to consider introducing interfaces..
● Litmus Test: If you can write it without, you probably don’t need an interface.

Don’t Create Interfaces Solely for Testing


● Don’t introduce escape-hatches to make production code more testable.
● Prefer testing with real implementations (e.g., grpctest, thriftest, miniredis, etc).
● Some dependencies, like Postgres, Kafka or BigQuery, don’t have a great alternative.
Better an interface than no tests at all.
03
Mutexes
Before
Channels
Mutexes Before Channels
Closing a Closed Channel

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 }
}) }
}

if err := [Link](); err != nil {


return 0, err
}
ch := make(chan int) var resps []int
errors := make(chan error) for {
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) }
if err != nil { }
errors <- err
}
ch <- resp
})
}

if err := [Link](); err != nil {


return 0, err
}
ch := make(chan int) var resps []int
for {
select {
g, ctx := [Link](ctx) case resp := <-ch:
for _, v := range input { resps = append(resps, resp)
[Link](func() error { }
resp, err := process(ctx, v) }
if err != nil {
return err
}
ch <- resp
})
}

if err := [Link](); err != nil {


return 0, err
}
ch := make(chan int) var resps []int
for resp := <-ch:
resps = append(resps, resp)
g, ctx := [Link](ctx) }
for _, v := range input {
[Link](func() error {
resp, err := process(ctx, v)
if err != nil {
return err
}
ch <- resp
})
}

if err := [Link](); err != nil {


return 0, err
}
for resp := <-ch:
var resps []int resps = append(resps, resp)
}
g, ctx := [Link](ctx)
for _, v := range input {
[Link](func() error {
resp, err := process(ctx, v)
if err != nil {
return err
}
ch <- resp
})
}

if err := [Link](); err != nil {


return 0, err
}
var mu [Link]
var resps []int

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]()
})
}

if err := [Link](); err != nil {


return 0, err
}
var mu [Link]
resps := make([]int, 0)
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]()
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
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

Channels are clever. In production, simpler is safer.

● Begin with synchronous code.


● Only add goroutines when profiling shows a bottleneck.
● Use [Link] and [Link] for shared state.
● Use go test -race to find data races.
● Cannels shine for complex orchestration, not basic synchronization.
04
Declare
Close to
Usage
Declare Close to Usage
A Universal Pattern

● Declare identifier (constants, variables, functions, types, etc.) in


the file that needs them.
● Export identifier only when they are needed outside of the
package.
● Within a function, declare variables as close as possible to
where they end up being consumed.
Declare Close to Usage
Limit Assignment Scope

err := [Link](b, &v)


if err != nil {
return nil, err
}
Declare Close to Usage
Limit Assignment Scope

if err := [Link](b, &v); err != nil {


return nil, err
}
Declare Close to Usage
Limit Assignment Scope

if err := [Link](b, &v); err != nil {


return nil, err
}
Declare Close to Usage
Limit Assignment Scope

if err := [Link](b, &v); err != nil {


return nil, err
}

if err := [Link](); err != nil {


return nil, err
}
Declare Close to Usage
Limit Assignment Scope

err := [Link](b, &v)


if err != nil {
return nil, err
}

err := [Link]()
if err != nil {
return nil, err
}
Declare Close to Usage
Limit Assignment Scope

err := [Link](b, &v)


if err != nil {
return nil, err
}

err := [Link]() no new variables on left side of :=


if err != nil {
return nil, err
}
Declare Close to Usage
Limit Assignment Scope

err := [Link](b, &v)


if err != nil {
return nil, err
}

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

Keep Related Code Close Together


● Don’t scatter identifiers. Declare constants, variables, and types where they’re used.
● Two files need the same identifier? Keep it with the first one — don’t orphan it in a
generic third file.
● Export only once it’s needed.

From Packages to Functions


● Works at every level: packages, functions, and blocks.
● Declare variables near their use to keep scope small.
● Smaller scope reduces subtle bugs like shadowing.
● Compact code groups make refactoring easier — easier to lift into helpers when
everything it needs is already nearby.
05
Avoid
Runtime
Panics
05
Avoid
Runtime
Panics
Runtime Panics
Check Your Inputs

func selectNotifications(req *[Link]) {


max := [Link]
[Link] = [Link][:max]
}
panic: runtime error: invalid memory
address or nil pointer dereference
[signal SIGSEGV: segmentation
violation code=0x1 addr=0x0
pc=0x4a4a4a]
Runtime Panics
Check Your Inputs

func selectNotifications(req *[Link]) {


max := [Link]
[Link] = [Link][:max]
}
Runtime Panics
Check Your Inputs

func selectNotifications(req *[Link]) {


max := [Link]
if len([Link]) > max {
[Link] = [Link][:max]
}
}
Runtime Panics
Check Your Inputs

func selectNotifications(req *[Link]) {


if req == nil {
return
}
max := [Link]
if len([Link]) > max {
[Link] = [Link][:max]
}
}
Runtime Panics
Check Your Inputs

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.

How Not to Overdo It


● Don’t litter the code with endless if x == nil checks.
● Rule of thumb: if you control the flow, trust Go’s error handling.
● Remember: error handling is your contract — don’t duplicate it with nil checks.
● Balance safety with readability: handle real risks, keep the happy path clean.
Runtime Panics
Check Nil Before Dereferencing

type FeedItem struct {


Score *float64 `json:"score"`
}

func sumFeedScores(feed *Feed) error {


var scores float64
for _, item := range [Link] {
scores += *[Link]
}
return nil
}
Runtime Panics
Check Nil Before Dereferencing

type FeedItem struct {


Score *float64 `json:"score"`
}

func sumFeedScores(feed *Feed) error {


var scores float64
for _, item := range [Link] {
if [Link] == nil { continue }
scores += *[Link]
}
return nil
}
Runtime Panics
Design for Pointer Safety

type FeedItem struct {


Score float64 `json:"score"`
}

func sumFeedScores(feed *Feed) error {


var scores float64
for _, item := range [Link] { Best pointer safety:
scores += [Link] Eliminate the need to
explicitly dereference.
}
return nil
}
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)
}
}
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

if err := doSomething(); err == nil {


if ok := check(); ok {
process()
} else {
return [Link]("check failed")
}
} else {
return err
}
Minimize Indentation
Good: Return Early, Flatter Structure

if err := doSomething(); err != nil {


return err
}
if !check() {
return [Link]("check failed")
}
process()
func getQueriesByID(items []*Item) map[string][]string {
queriesByID := make(map[string][]string)
for _, item := range items {
var queryCount int
if [Link] != nil {
queries := make([]string, 0)
for _, query := range [Link] {
if query != "" {
queries = append(queries, query)
queryCount++
}
}
queriesByID[[Link]] = queries
observeQueryCount([Link], queryCount)
}
}
return queriesByID
}
func getQueriesByID(items []*Item) map[string][]string {
queriesByID := make(map[string][]string)
for _, item := range items {
var queryCount int
if [Link] == nil {
continue
}
queries := make([]string, 0)
for _, query := range [Link] {
if query != "" {
queries = append(queries, query)
queryCount++
}
}
queriesByID[[Link]] = queries
observeQueryCount([Link], queryCount)
}
return queriesByID
}
func getQueriesByID(items []*Item) map[string][]string {
queriesByID := make(map[string][]string)
for _, item := range items {
if [Link] == nil {
continue
}
var queryCount int
queries := make([]string, 0)
for _, query := range [Link] {
if query != "" {
queries = append(queries, query)
queryCount++
}
}
queriesByID[[Link]] = queries
observeQueryCount([Link], queryCount)
}
return queriesByID
}
func getQueriesByID(items []*Item) map[string][]string {
queriesByID := make(map[string][]string)
for _, item := range items {
if [Link] == nil {
continue
}
var queryCount int
queries := make([]string, 0)
for _, query := range [Link] {
if query == "" {
continue
}
queries = append(queries, query)
queryCount++
}
queriesByID[[Link]] = queries
observeQueryCount([Link], queryCount)
}
return queriesByID
}
func getQueriesByID(items []*Item) map[string][]string {
queriesByID := make(map[string][]string)
for _, item := range items {
var queryCount int
if [Link] != nil {
queries := make([]string, 0)
for _, query := range [Link] {
if query != "" {
queries = append(queries, query)
queryCount++
}
}
queriesByID[[Link]] = queries
observeQueryCount([Link], queryCount)
}
}
return queriesByID
}
func getQueriesByID(items []*Item) map[string][]string {
queriesByID := make(map[string][]string)
for _, item := range items {
if [Link] == nil {
continue
}
var queryCount int
queries := make([]string, 0)
for _, query := range [Link] {
if query == "" {
continue
}
queries = append(queries, query)
queryCount++
}
queriesByID[[Link]] = queries
observeQueryCount([Link], queryCount)
}
return queriesByID
}
func getQueriesByID(items []*Item) map[string][]string {
queriesByID := make(map[string][]string)
for _, item := range items {
if [Link] == nil {
continue
}
queries := getQueries(item)
queriesByID[[Link]] = queries
observeQueryCount([Link], len(queries))
}
return queriesByID
}

func getQueries(item *Item) []string {


queries := make([]string, 0)
for _, query := range skipEmpty([Link]) {
queries = append(queries, query)
}
return queries
}
func getQueriesByID(items []*Item) map[string][]string {
queriesByID := make(map[string][]string)
for _, item := range items {
if [Link] == nil {
continue
}
queries := [Link](skipEmpty([Link]))
queriesByID[[Link]] = queries
observeQueryCount([Link], len(queries))
}
return queriesByID
}
07

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

lost control of your


code base, so you
created some util
packages.
Gnarl Largerfur
Avoid Catch-All Packages and Files
Prefer Locality over Hierarchy

● Code is easier to understand when it’s near what it affects.


● Abstract organization might feel tidy, but it often hides purpose.
● Be specific: name after domain or functionality.
● Group by meaning, not by type.
08

Order
Declarations by
Importance
Order Declarations by Importance
Most Important Code to the Top

func Trim(s, cutset string) string {


// ...
return trimLeftUnicode(trimRightUnicode(s, cutset), cutset)
}

func trimLeftByte(s string, c byte) string { ... }


func trimRightUnicode(s, cutset string) string { ... }
Order Declarations by Importance
Most Important Code to the Top

func trimLeftByte(s string, c byte) string { ... }


func trimRightUnicode(s, cutset string) string { ... }

func Trim(s, cutset string) string {


// ...
return trimLeftUnicode(trimRightUnicode(s, cutset), cutset)
}
Order Declarations by Importance
Most Important Code to the Top

func Trim(s, cutset string) string {


// ...
return trimLeftUnicode(trimRightUnicode(s, cutset), cutset)
}

func trimLeftByte(s string, c byte) string { ... }


func trimRightUnicode(s, cutset string) string { ... }
Order Declarations by Importance
Most Important Code to the Top

In Go, functions don’t need to be declared before use (no forward declarations).

Declaration order still matters for readability.

● Put exported, API-facing functions first.


● Follow with helper functions, which are implementation details.

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

type mockBigQuery struct{ orders: [][Link] }

func (m *mockBigQuery) ListOrders() ([][Link], error) {


return [Link], nil
}

func TestCreateOrder(t *testing.T) {


bq := &mockBigQuery{}
// ...
}
Order Declarations by Importance
Most Important Code to the Top

func TestCreateOrder(t *testing.T) {


bq := &mockBigQuery{}
// ...
}

type mockBigQuery struct{ orders: [][Link] }

func (m *mockBigQuery) ListOrders() ([][Link], error) {


return [Link], nil
}
09

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:

userByID map[string]*User userMap map[string]*User


id string idStr string
inject func() injectFn func()

✅ Good ❌ BAD
Name Well
Variable Length

It is not uncommon to see the use of one character variables in Go.


While this conflicts with Clear Naming, it can make sense in some
cases.

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

Think about how code reads at the call site:


Name Well
Packages and Exported Identifiers

Think about how code reads at the call site:

db := [Link](...)
_, err := [Link](0, [Link])
b := [Link](curve, x, y)
c := [Link](...)
Name Well
Packages and Exported Identifiers

Think about how code reads at the call site:

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

func EscapeDoubleQuotes(s string) string {


if [Link](s, `"`) && [Link](s, `"`) {
core := [Link]([Link](s, `"`), `"`)
escaped := [Link](core, `"`, `\"`)
escaped = [Link](escaped, `\\"`, `\"`)
return [Link](`"%s"`, escaped)
}
return s
}
Document the Why, Not the What
Justify the Code’s Existence

// Escapes internal double quotes by replacing `"` with `\"`.


func EscapeDoubleQuotes(s string) string {
if [Link](s, `"`) && [Link](s, `"`) {
core := [Link]([Link](s, `"`), `"`)
escaped := [Link](core, `"`, `\"`)
escaped = [Link](escaped, `\\"`, `\"`)
return [Link](`"%s"`, escaped)
}
return s
}
Document the Why, Not the What
Justify the Code’s Existence

// We can sometimes receive a label like: ""How-To"" because the frontend


// wraps user-provided labels in quotes, even when the value itself
// contains literal `"` characters. In this case, attempt to escape all
// internal double quotes, leaving only the outermost ones unescaped.
func EscapeDoubleQuotes(s string) string {
if [Link](s, `"`) && [Link](s, `"`) {
core := [Link]([Link](s, `"`), `"`)
escaped := [Link](core, `"`, `\"`)
escaped = [Link](escaped, `\\"`, `\"`)
return [Link](`"%s"`, escaped)
}
return s
}
Document the Why, Not the What
Justify the Code’s Existence

// Escapes internal double quotes by replacing `"` with `\"`.


func EscapeDoubleQuotes(s string) string {
if [Link](s, `"`) && [Link](s, `"`) {
core := [Link]([Link](s, `"`), `"`)
escaped := [Link](core, `"`, `\"`)
escaped = [Link](escaped, `\\"`, `\"`)
return [Link](`"%s"`, escaped)
}
return s
}
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.

● Pull request description: explain why this change matters.


● Code comments: document intent, not mechanics.
● Future readers should be able to understand the motivation behind your choices.

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?

You might also like