Upgrade to Pro — share decks privately, control downloads, hide ads and more …

When Go Styles Stops Being Style: Code Review a...

When Go Styles Stops Being Style: Code Review at Scale

Why do Go developers obsess over variable names, error handling, and interfaces, even when your approach does the job, too? Whether gently or not so gently, they'll remind you there's a right way to write Go.

In this talk, I walk through frequent code review comments to show what looks like nitpicking is a window into Go's design philosophy. These aren't arbitrary style debates; they're about writing clear, deliberate, and unapologetically pragmatic Go. You leave with a better understanding of the "why" behind the rules, how they translate to AI-assisted development, and how we think about code review at Reddit.

GopherCon Latam 2026, Florianópolis
https://konradreiche.com/

Avatar for Konrad Reiche

Konrad Reiche

September 02, 2026

More Decks by Konrad Reiche

Other Decks in Programming

Transcript

  1. What’s the Point? The View From the Review Queue When

    the code convention only exists in your head, you have to repeat yourself.
  2. What’s the Point? The View From the Review Queue When

    the code convention only exists in your head, you have to repeat yourself. You may not even realize there is a rule, until you start to review the code. func get_user_by_id(userId string)(*User,error) { data, _ := db.GetUser(userId) if data!=nil { return data,nil } else { return nil,nil } }
  3. About Me Konrad Reiche I live in San Francisco, but

    I was born and raised in Berlin, Germany, where I studied Computer Science. I began my career as a full-stack developer in the UK and I’ve been writing Go since 2016.
  4. About Me Ranking Platform • Internal Platform Team • Building

    Reusable Infrastructure Konrad Reiche I live in San Francisco, but I was born and raised in Berlin, Germany, where I studied Computer Science. I began my career as a full-stack developer in the UK and I’ve been writing Go since 2016. • Powers Home Feed, Search, Push Notifications • Evolves Together in a Go Monorepo
  5. What’s the Point? The View From the Review Queue Reviewing

    hundreds of pull requests turned repetition into patterns and patterns into guidance. Small style comments often resurface as real production issues. Code review is where we learn, whether we want to or not.
  6. What’s the Point? The View From the Review Queue Reviewing

    hundreds of pull requests turned repetition into patterns and patterns into guidance. Small style comments often resurface as real production issues. Code review is where we learn, whether we want to or not. Commits
  7. What’s the Point? The View From the Review Queue Reviewing

    hundreds of pull requests turned repetition into patterns and patterns into guidance. Small style comments often resurface as real production issues. Code review is where we learn, whether we want to or not. Unique Contributors Commits
  8. Handle Errors Good: Checking and Handling the Error result, err

    := pickRandom(input) if err != nil { return err }
  9. Handle Errors Good: Checking and Handling the Error result, err

    := pickRandom(input) if err != nil { pickRandomFailures.Inc() return nil }
  10. Handle Errors Good: Checking and Handling the Error result, err

    := pickRandom(input) if err != nil { slog.Error("pickRandom failed", "error", err) return nil }
  11. Handle Errors BAD: Double Reporting result, err := pickRandom(input) if

    err != nil { slog.Error("pickRandom failed", "error", err) return err }
  12. Handle Errors BAD: Double Reporting result, err := pickRandom(input) if

    err != nil { slog.Error("pickRandom failed", "error", err) return err } Log it, or return it — but not both.
  13. Handle Errors Good: Checking and Handling the Error + Contexualizing

    result, err := pickRandom(input) if err != nil { return fmt.Errorf("pickRandom failed: %w", err) }
  14. Handle Errors Good: Checking and Handling the Error + Contexualizing

    result, err := pickRandom(input) if err != nil { return fmt.Errorf("fallback selection failed: %w", err) }
  15. Handle Errors BAD: Ambiguous Contract resp, err := http.DefaultClient.Do(req) if

    err != nil { slog.Error("fetch failed", "error", err) } slog.Info(resp.Status)
  16. panic: runtime error: invalid memory address or nil pointer dereference

    [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x6461ca]
  17. 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.
  18. 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. Sometimes you need to return partial results; document explicitly.
  19. 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
  20. Adding Interfaces Too Soon Premature Abstraction type EligibilityService struct {

    catalog *product.Catalog } func (e *EligibilityService) IsEligible( ctx context.Context, userID string, productID string, ) (bool, error) { // ... }
  21. Adding Interfaces Too Soon Premature Abstraction type Cache[T any] interface

    { Get(ctx context.Context, key string) (*T, error) Set(ctx context.Context, key string, value T) error }
  22. Adding Interfaces Too Soon Premature Abstraction type EligibilityService struct {

    cache cache.Cache[model.Product] catalog *product.Catalog } func (e *EligibilityService) IsEligible( ctx context.Context, userID string, productID string, ) (bool, error) { // ... }
  23. Adding Interfaces Too Soon Premature Abstraction type EligibilityService struct {

    cache cache.Cache[model.Product] catalog *product.Catalog } 👆
  24. package cache import ( "container/heap" "container/list" "context" "sync" ) type

    Cache[T any] interface { Get(ctx context.Context, key string) (*T, error) Set(ctx context.Context, key string, value T) error }
  25. package cache import ( "container/heap" "container/list" "context" "sync" ) type

    Cache[T any] interface { Get(ctx context.Context, key string) (*T, error) Set(ctx context.Context, key string, value T) error } type LFU[T any] struct { size int mu sync.RWMutex data map[string]*lfuItem[T] heap *MinHeap[T] }
  26. package cache import ( "container/heap" "container/list" "context" "sync" ) type

    Cache[T any] interface { Get(ctx context.Context, key string) (*T, error) Set(ctx context.Context, key string, value T) error }
  27. package cache import ( "container/heap" "container/list" "context" "sync" ) type

    Cache[T any] interface { Get(ctx context.Context, key string) (*T, error) Set(ctx context.Context, key string, value T) error } type LRU[T any] struct { size int mu sync.RWMutex data map[string]*lruItem[T] queue *list.List }
  28. Adding Interfaces Too Soon Premature Abstraction cache/ ├── cache.go ├──

    heap.go ├── ... ├── lru.go └── lfu.go
  29. Adding Interfaces Too Soon Premature Abstraction func NewEligibilityService( catalog *product.Catalog,

    cache cache.Cache[*model.Product], ) *EligibilityService { return &EligibilityService{ catalog: catalog, catalogCache: catalogCache, } }
  30. Adding Interfaces Too Soon Premature Abstraction type EligibilityService struct {

    cache cache.Cache[model.Product] catalog *product.Catalog }
  31. Adding Interfaces Too Soon Premature Abstraction type EligibilityService struct {

    cache *lfu.Cache[model.Product] catalog *product.Catalog }
  32. Adding Interfaces Too Soon Premature Abstraction type EligibilityService struct {

    cache *cache.LFU[model.Product] catalog *product.Catalog } Ask yourself if you really need multiple implementations before adding the extra layer of indirection.
  33. 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
  34. 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.
  35. Adding Interfaces Too Soon Support Testing type EligibilityService struct {

    cache *cache.LFU[model.Product] catalog *product.Catalog }
  36. Adding Interfaces Too Soon Support Testing type EligibilityService struct {

    cache *cache.LFU[model.Product] catalog *product.Catalog userService *dep.UserService }
  37. Adding Interfaces Too Soon Support Testing type EligibilityService struct {

    cache *cache.LFU[model.Product] catalog *product.Catalog userService userService } type userService interface { GetUser(context.Context, string) (*dep.UserResponse, error) }
  38. func TestService_IsEligible(t *testing.T) { userService := &mockUserService{ getUser: func(context.Context, string)

    (*dep.UserResponse, error) { return &userpb.GetUserResponse{ Id: "test_user_1", Subscription: 1, }, nil }, } catalog := product.NewCatalog() svc := New(userService, catalog) result, err := svc.IsEligible(t.Context(), "test_user_1", "ad-free") if err != nil { t.Fatal(err) } if got, want := result, true; got != want { t.Errorf("got %t, want: %t", got, want) } }
  39. func TestService_IsEligible(t *testing.T) { userService := &mockUserService{ getUser: func(context.Context, string)

    (*dep.UserResponse, error) { return &userpb.GetUserResponse{ Id: "test_user_1", Service client package Subscription: 1, has 0% test coverage. }, nil }, } catalog := product.NewCatalog() svc := New(userService, catalog) result, err := svc.IsEligible(t.Context(), "test_user_1", "ad-free") if err != nil { t.Fatal(err) } if got, want := result, true; got != want { t.Errorf("got %t, want: %t", got, want) } }
  40. Adding Interfaces Too Soon Support Testing type EligibilityService struct {

    cache *cache.LFU[model.Product] catalog *product.Catalog userService userService 👆 } type userService interface { GetUser(context.Context, string) (*dep.UserResponse, error) }
  41. Adding Interfaces Too Soon Test the Implementation and the Integration

    Fakes Instead of Mocks • A fake simulates your dependency with similar logic, while a mock returns static data. • Start a real gRPC service without network interfaces by utilizing bufconn. • bufconn provides an in-memory connection that mimics a real network connection. Test Helper Packages • A package strictly scoped to make testing a specific dependency easy. • Its constructors return the concrete type under test. dep ├── deptest │ ├── deptest.go │ └── fakeuserservice └── user_service.go
  42. func TestService_IsEligible(t *testing.T) { userService := &mockUserService{ getUser: func(context.Context, string)

    (*dep.UserResponse, error) { return &userpb.GetUserResponse{ Id: "test_user_1", Subscription: 1, }, nil }, } catalog := product.NewCatalog() svc := New(userService, catalog) result, err := svc.IsEligible(t.Context(), "test_user_1", "ad-free") if err != nil { t.Fatal(err) } if got, want := result, true; got != want { t.Errorf("got %t, want: %t", got, want) } }
  43. func TestService_IsEligible(t *testing.T) { userService := fakeuserservice.New( t, fakeuserservice.WithUserSubscription("test_user_1", 1),

    ) } catalog := product.NewCatalog() svc := New(userService, catalog) result, err := svc.IsEligible(t.Context(), "test_user_1", "ad-free") if err != nil { t.Fatal(err) } if got, want := result, true; got != want { t.Errorf("got %t, want: %t", got, want) }
  44. func TestService_IsEligible(t *testing.T) { userService := fakeuserservice.New( t, fakeuserservice.WithUserSubscription("test_user_1", 1),

    ) catalog := product.NewCatalog() svc := New(userService, catalog) result, err := svc.IsEligible(t.Context(), "test_user_1", "ad-free") if err != nil { t.Fatal(err) } if got, want := result, true; got != want { t.Errorf("got %t, want: %t", got, want) } }
  45. Adding Interfaces Too Soon Support Testing type EligibilityService struct {

    cache *cache.LFU[model.Product] catalog *product.Catalog userService userService } type userService interface { GetUser(context.Context, string) (*dep.UserResponse, error) }
  46. Adding Interfaces Too Soon Support Testing type EligibilityService struct {

    cache *cache.LFU[model.Product] catalog *product.Catalog userService *dep.UserService } We have eliminated the need for additional types. The code under test is covered and we have made writing tests easier with dedicated test helper packages.
  47. Adding Interfaces Too Soon The Right Time Don’t Start With

    Interfaces • Follow the convention of Accept interfaces, Return Concrete Types. • Begin with the concrete type, and introduce the interface once different types need to flow through the same code. • If you can easily write the code without an interface, you probably don’t need it.
  48. Adding Interfaces Too Soon The Right Time Don’t Start With

    Interfaces • Follow the convention of Accept interfaces, Return Concrete Types. • Begin with the concrete type, and introduce the interface once different types need to flow through the same code. • If you can easily write the code without an interface, you probably don’t need it. 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.
  49. Mutexes Before Channels Ranging over a Channel That’s Never Closed

    ch := make(chan int) go func() { ch <- 1 }() for v := range ch { fmt.Println(v) }
  50. ch := make(chan int) errors := make(chan error) done :=

    make(chan struct{}) var wg sync.WaitGroup for _, v := range input { wg.Go(func() { resp, err := process(ctx, v) if err != nil { errors <- err } ch <- resp }) } go func() { wg.Wait() close(done) }() var resps []int for { select { case resp := <-ch: resps = append(resps, resp) case err := <-errors: return 0, err case <-done: return merge(resps...), nil case <-ctx.Done(): return 0, ctx.Err() } }
  51. ch := make(chan int) errors := make(chan error) done :=

    make(chan struct{}) var wg sync.WaitGroup for _, v := range input { wg.Go(func() { resp, err := process(ctx, v) if err != nil { errors <- err } ch <- resp }) } go func() { wg.Wait() close(done) }() var resps []int for { select { case resp := <-ch: resps = append(resps, resp) case err := <-errors: return 0, err case <-done: return merge(resps...), nil case <-ctx.Done(): return 0, ctx.Err() } }
  52. ch := make(chan int) errors := make(chan error) done :=

    make(chan struct{}) var wg sync.WaitGroup for _, v := range input { wg.Go(func() { resp, err := process(ctx, v) if err != nil { errors <- err } ch <- resp }) } go func() { wg.Wait() close(done) }() var resps []int for { select { case resp := <-ch: resps = append(resps, resp) case err := <-errors: return 0, err case <-done: return merge(resps...), nil case <-ctx.Done(): return 0, ctx.Err() } }
  53. ch := make(chan int) errors := make(chan error) done :=

    make(chan struct{}) g, ctx := errgroup.WithContext(ctx) for _, v := range input { g.Go(func() error { resp, err := process(ctx, v) if err != nil { errors <- err } ch <- resp }) } if err := g.Wait(); err != nil { return 0, err } var resps []int for { select { case resp := <-ch: resps = append(resps, resp) case err := <-errors: return 0, err case <-done: return merge(resps...), nil case <-ctx.Done(): return 0, ctx.Err() } }
  54. ch := make(chan int) errors := make(chan error) g, ctx

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

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

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

    := range input { g.Go(func() error { resp, err := process(ctx, v) if err != nil { return err } ch <- resp }) } if err := g.Wait(); err != nil { return 0, err } for resp := <-ch: resps = append(resps, resp) }
  58. var mu sync.Mutex var resps []int g, ctx := errgroup.WithContext(ctx)

    for _, v := range input { g.Go(func() error { resp, err := process(ctx, v) if err != nil { return err } mu.Lock() resps = append(resps, resp) mu.Unlock() }) } if err := g.Wait(); err != nil { return 0, err }
  59. var mu sync.Mutex resps := make([]int, 0) g, ctx :=

    errgroup.WithContext(ctx) for _, v := range input { g.Go(func() error { resp, err := process(ctx, v) if err != nil { return err } mu.Lock() resps = append(resps, resp) mu.Unlock() return nil }) } if err := g.Wait(); err != nil { return 0, err } return merge(resps...), nil
  60. resps := make([]int, len(input)) g, ctx := errgroup.WithContext(ctx) for i,

    v := range input { g.Go(func() error { resp, err := process(ctx, v) if err != nil { return err } resps[i] = resp }) return nil } if err := g.Wait(); err != nil { return 0, err } return merge(resps...), nil
  61. resps := make([]int, len(input)) g, ctx := errgroup.WithContext(ctx) for i,

    v := range input { g.Go(func() error { resp, err := process(ctx, v) if err != nil { return err } resps[i] = resp return nil }) } if err := g.Wait(); err != nil { return 0, err } return merge(resps...), nil
  62. resps := make([]int, len(input)) g, ctx := errgroup.WithContext(ctx) for i,

    v := range input { g.Go(func() error { resp, err := process(ctx, v) if err != nil { return err } resps[i] = resp return nil }) } if err := g.Wait(); err != nil { return 0, err } return merge(resps...), nil
  63. 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 sync.Mutex and sync.WaitGroup for shared state. Use go test -race to find data races. Channels shine for fine-grained control, handling blocking, and back-pressure, not basic synchronization.f
  64. Declare Close to Usage Limit Assignment Scope if err :=

    json.Unmarshal(b, &v); err != nil { return nil, err }
  65. Declare Close to Usage Limit Assignment Scope if err :=

    json.Unmarshal(b, &v); err != nil { return nil, err }
  66. Declare Close to Usage Limit Assignment Scope if err :=

    json.Unmarshal(b, &v); err != nil { return nil, err } if err := v.Validate(); err != nil { return nil, err }
  67. Declare Close to Usage Limit Assignment Scope err := json.Unmarshal(b,

    &v) if err != nil { return nil, err } err := v.Validate() if err != nil { return nil, err }
  68. Declare Close to Usage Limit Assignment Scope err := json.Unmarshal(b,

    &v) if err != nil { return nil, err } err := v.Validate() if err != nil { return nil, err } no new variables on left side of :=
  69. Declare Close to Usage Limit Assignment Scope err := json.Unmarshal(b,

    &v) if err != nil { return nil, err } err = v.Validate() if err != nil { return nil, err }
  70. Declare Close to Usage Limit Assignment Scope if err :=

    json.Unmarshal(b, &v); err != nil { return nil, err } if err := v.Validate(); err != nil { return nil, err }
  71. 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 = client.PostSearch(queries) return err }) if authErr != nil { return nil, err } } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil
  72. 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 = client.PostSearch(queries) return err }) if authErr != nil { return nil, err } } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil Variable declaration, assignment, and use are spread out. Is that necessary?
  73. 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 = client.PostSearch(queries) return err }) if authErr != nil { return nil, err } } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil
  74. 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 = client.PostSearch(queries) return err }) if authErr != nil { return nil, err } } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil
  75. 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 = client.PostSearch(queries) return err }) We check one error if authErr != nil { but return the other. return nil, err } } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil
  76. 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 = client.PostSearch(queries) return err }) if err != nil { return nil, err } } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil
  77. 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 = client.PostSearch(queries) return err }) if err != nil { return nil, err } return results, nil } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil
  78. 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 = client.PostSearch(queries) return err }) if err != nil { return nil, err } return results, nil } results, err := client.PostSearch(queries) if err != nil { return nil, err } return results, nil
  79. 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 = client.PostSearch(queries) return err }) if err != nil { return nil, err } return results, nil } results, err := client.PostSearch(queries) if err != nil { return nil, err } return results, nil }
  80. 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 = client.PostSearch(queries) return err }) if err != nil { return nil, err } return results, nil } return client.PostSearch(queries) }
  81. func fetch(auth auth, client Client, queries []string) ([]string, error) {

    if auth != nil { var results []string err := auth(func() (err error) { results, err = client.PostSearch(queries) return err }) if err != nil { return nil, err } return results, nil } return client.PostSearch(queries) }
  82. 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.
  83. Runtime Panics Check Your Inputs func selectNotifications(req *pb.Request) { max

    := req.Options.MaxNotifications req.Notifications = req.Notifications[:max] }
  84. panic: runtime error: invalid memory address or nil pointer dereference

    [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4a4a4a]
  85. Runtime Panics Check Your Inputs func selectNotifications(req *pb.Request) { max

    := req.Options.MaxNotifications req.Notifications = req.Notifications[:max] }
  86. Runtime Panics Check Your Inputs func selectNotifications(req *pb.Request) { max

    := req.Options.MaxNotifications if len(req.Notifications) > max { req.Notifications = req.Notifications[:max] } }
  87. Runtime Panics Check Your Inputs func selectNotifications(req *pb.Request) { if

    req == nil { return Excessive Check } max := req.Options.MaxNotifications if len(req.Notifications) > max { req.Notifications = req.Notifications[:max] } }
  88. 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. Avoiding Excessive Checks • Don’t litter your code with if x == nil • If you control the flow, trust Go’s error handling and treat error handling as a contract. • Balance safety with readability by handling real risks and keeping the happy path clean.
  89. Runtime Panics Check Nil Before Dereferencing type FeedItem struct {

    Score *float64 `json:"score"` } func sumFeedScores(feed *Feed) error { var scores float64 for _, item := range feed.Items { scores += *item.Score } return nil }
  90. Runtime Panics Check Nil Before Dereferencing type FeedItem struct {

    Score *float64 `json:"score"` } func sumFeedScores(feed *Feed) error { var scores float64 for _, item := range feed.Items { if item.Score == nil { continue } scores += *item.Score } return nil }
  91. Runtime Panics Design for Pointer Safety type FeedItem struct {

    Score float64 `json:"score"` } func sumFeedScores(feed *Feed) error { var scores float64 for _, item := range feed.Items { scores += item.Score } return nil } Best Pointer Safety Eliminate the need to explicitly dereference.
  92. Runtime Panics Design for Pointer Safety func main() { db,

    err := dep.NewVectorDB() if err != nil { log.Fatal(err) } job := cron.NewJob("indexer", *db) if err := job.Run(); err != nil { log.Fatal(err) } }
  93. Runtime Panics Design for Pointer Safety func main() { db,

    err := dep.NewVectorDB() if err != nil { log.Fatal(err) } job := cron.NewJob("indexer", db) if err := job.Run(); err != nil { log.Fatal(err) } }
  94. 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 = client.PostSearch(queries) return err }) if authErr != nil { return nil, err } } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil
  95. func fetch(auth auth, client Client, queries []string) ([]string, error) {

    if auth != nil { var results []string err := auth(func() (err error) { results, err = client.PostSearch(queries) return err }) if err != nil { return nil, err } return results, nil } return client.PostSearch(queries) }
  96. Minimize Indentation BAD: Wraps All Logic Inside if err :=

    doSomething(); err == nil { if ok := check(); ok { process() } else { return errors.New("check failed") } } else { return err }
  97. Minimize Indentation Good: Return Early, Flatter Structure if err :=

    doSomething(); err != nil { return err } if !check() { return errors.New("check failed") } process()
  98. func getQueriesByID(items []*Item) map[string][]string { queriesByID := make(map[string][]string) for _,

    item := range items { var queryCount int if item.Queries != nil { queries := make([]string, 0) for _, query := range item.Queries.Values { if query != "" { queries = append(queries, query) queryCount++ } } queriesByID[item.ID] = queries observeQueryCount(item.ID, queryCount) } } return queriesByID }
  99. func getQueriesByID(items []*Item) map[string][]string { queriesByID := make(map[string][]string) for _,

    item := range items { var queryCount int if item.Queries == nil { continue } queries := make([]string, 0) for _, query := range item.Queries.Values { if query != "" { queries = append(queries, query) queryCount++ } } queriesByID[item.ID] = queries observeQueryCount(item.ID, queryCount) } return queriesByID }
  100. func getQueriesByID(items []*Item) map[string][]string { queriesByID := make(map[string][]string) for _,

    item := range items { if item.Queries == nil { continue } var queryCount int queries := make([]string, 0) for _, query := range item.Queries.Values { if query != "" { queries = append(queries, query) queryCount++ } } queriesByID[item.ID] = queries observeQueryCount(item.ID, queryCount) } return queriesByID }
  101. func getQueriesByID(items []*Item) map[string][]string { queriesByID := make(map[string][]string) for _,

    item := range items { if item.Queries == nil { continue } var queryCount int queries := make([]string, 0) for _, query := range item.Queries.Values { if query == "" { continue } queries = append(queries, query) queryCount++ } queriesByID[item.ID] = queries observeQueryCount(item.ID, queryCount) } return queriesByID }
  102. func getQueriesByID(items []*Item) map[string][]string { queriesByID := make(map[string][]string) for _,

    item := range items { var queryCount int if item.Queries != nil { queries := make([]string, 0) for _, query := range item.Queries.Values { if query != "" { queries = append(queries, query) queryCount++ } } queriesByID[item.ID] = queries observeQueryCount(item.ID, queryCount) } } return queriesByID }
  103. func getQueriesByID(items []*Item) map[string][]string { queriesByID := make(map[string][]string) for _,

    item := range items { if item.Queries == nil { continue } var queryCount int queries := make([]string, 0) for _, query := range item.Queries.Values { if query == "" { continue } queries = append(queries, query) queryCount++ } queriesByID[item.ID] = queries observeQueryCount(item.ID, queryCount) } return queriesByID }
  104. func getQueriesByID(items []*Item) map[string][]string { queriesByID := make(map[string][]string) for _,

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

    item := range items { if item.Queries == nil { continue } queries := slices.Collect(skipEmpty(item.Queries.Values)) queriesByID[item.ID] = queries observeQueryCount(item.ID, len(queries)) } return queriesByID }
  106. “ Sweatpants are a sign of defeat. You lost control

    of your life, so you bought some sweatpants. Karl Lagerfeld
  107. “ util packages are a sign of defeat. You lost

    control of your code base, so you created some util packages. Gnarl Largerfur
  108. “ NO TA CTU AL CO DE REV I util

    packages are a sign of defeat. You lost control of your code base, so you created some util packages. EW CO M ME NT Gnarl Largerfur
  109. Avoid Catch-All Packages and Files Prefer Locality over Hierarchy •

    Code is easier to understand when it’s close 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.
  110. 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 { ... }
  111. 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) }
  112. 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 { ... }
  113. 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.
  114. Order Declarations by Importance Most Important Code to the Top

    type mockBigQuery struct{ orders: []model.Order } func (m *mockBigQuery) ListOrders() ([]model.Order, error) { return m.orders, nil } func TestCreateOrder(t *testing.T) { bq := &mockBigQuery{} // ... }
  115. Order Declarations by Importance Most Important Code to the Top

    func TestCreateOrder(t *testing.T) { bq := &mockBigQuery{} // ... } type mockBigQuery struct{ orders: []model.Order } func (m *mockBigQuery) ListOrders() ([]model.Order, error) { return m.orders, nil }
  116. 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()
  117. 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
  118. 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 id string inject func() userMap map[string]*User idStr string injectFn func() ✅ Good ❌ BAD
  119. 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.
  120. Name Well Packages and Exported Identifiers Think about how code

    reads at the call site: db := test.NewDatabaseFromFile(...) _, err := f.Seek(0, common.SeekStart) b := helper.Marshal(curve, x, y) c := consumer.NewConsumerHandler(...)
  121. Name Well Packages and Exported Identifiers Think about how code

    reads at the call site: db := spannertest.NewDatabaseFromFile(...) _, err := f.Seek(0, io.SeekStart) b := elliptic.Marshal(curve, x, y) c := consumer.NewHandler(...)
  122. If AI writes and reviews the code, what are conventions

    for? Who uses AI to help write Go code?
  123. If AI writes and reviews the code, what are conventions

    for? As AI gets better, do we still need conventions?
  124. Document the Why, Not the What Justify the Code’s Existence

    func EscapeDoubleQuotes(s string) string { if strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) { core := strings.TrimPrefix(strings.TrimSuffix(s, `"`), `"`) escaped := strings.ReplaceAll(core, `"`, `\"`) escaped = strings.ReplaceAll(escaped, `\\"`, `\"`) return fmt.Sprintf(`"%s"`, escaped) } return s }
  125. Document the Why, Not the What Justify the Code’s Existence

    // Escapes internal double quotes by replacing `"` with `\"`. func EscapeDoubleQuotes(s string) string { if strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) { core := strings.TrimPrefix(strings.TrimSuffix(s, `"`), `"`) escaped := strings.ReplaceAll(core, `"`, `\"`) escaped = strings.ReplaceAll(escaped, `\\"`, `\"`) return fmt.Sprintf(`"%s"`, escaped) } return s }
  126. 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 strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) { core := strings.TrimPrefix(strings.TrimSuffix(s, `"`), `"`) escaped := strings.ReplaceAll(core, `"`, `\"`) escaped = strings.ReplaceAll(escaped, `\\"`, `\"`) return fmt.Sprintf(`"%s"`, escaped) } return s }
  127. 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.
  128. If AI writes and reviews the code, what are conventions

    for? As AI gets better, do we still need conventions?
  129. Maintainability matters, no matter who writes the code. Simplicity and

    consistency reduce ambiguity for humans and AI. Better code and clearer guidance leave more space to focus on solving the actual problem.
  130. Scaling Code Review at Reddit The Classic Code Review Process

    Write Code Open Pull Request Improve Code Request Review Review Code Merge Code
  131. Scaling Code Review at Reddit The Classic Code Review Process

    Write Code Open Pull Request Improve Code Request Review Review Code Merge Code
  132. Scaling Code Review at Reddit The Classic Code Review Process

    Write Code Open Pull Request Improve Code Request Review Review Code This used to be driven by GitHub’s CODEOWNERS Merge Code
  133. Scaling Code Review at Reddit Reducing the Time to First

    Feedback Write Code Open Pull Request Improve Code Request Review Review Code Driven by REDDITOWNERS Merge Code
  134. Scaling Code Review at Reddit Reducing the Time to First

    Feedback Write Code Open Pull Request Improve Code Request Review Review Code This can get tiresome Merge Code
  135. Scaling Code Review at Reddit Reducing the Time to First

    Feedback Write Code Improve Code Open Draft Request Review AI Review Improve Code Review Code Merge Code
  136. Scaling Code Review at Reddit Reducing the Time to First

    Feedback Write Code Improve Code Open Draft Request Human Review Review Code AI Review Improve Code Mark Ready Merge Code
  137. Scaling Code Review at Reddit Feedback as Infrastructure Teams can

    turn project-specific conventions into AI review personas, bringing that guidance into the pull request workflow. • • • A style guide should not simply become one enormous prompt. Rules must be concise and prioritized with different levels of severity. False positives, missed issues, repeated comments signal that the system has to change. When a recurring class of problem appears, ask how the system allowed it. Then fix that. A rule that can be checked deterministically should move out of code review and into the tooling as close to the developer as possible.
  138. When Go Style Stops Being Style The Story Behind It

    Most “style” comments aren’t about aesthetics — they’re about avoiding production pain. The goal is not perfection, but reducing friction: for readers, writers, which includes your future self. Patterns repeat: what looks like nitpicking in one pull request often shows up later as a bug, an outage, or unreadable code. Every rule of thumb is about the same principle. Make the code obvious, safe, and easy to work with. Code review isn’t just about shipping safely. It’s where we teach, learn, and build intuition together.