Skip to Content
Course content

245: Effective Go Style Guide Highlights

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you know how to write Go code that compiles. But writing idiomatic Go—the kind of code that makes a seasoned Gopher nod in approval during a PR review—is a different beast. Go has a very strong "one way to do it" philosophy. If you try to bring patterns from Java or Python into Go, your code will feel clunky.

Why are the variable names in Go examples so ridiculously short?

If you've looked at the standard library, you'll see variables like r, w, or ctx. Coming from other languages, this feels like a crime. But in Go, the rule of thumb is: the smaller the scope, the shorter the name.

If you have a variable that only exists for three lines inside a loop, calling it userIndex is overkill. i is enough. The context is right there. I always tell my juniors: don't let your variable names scream when a whisper will do.

// Avoid this in a small scope
for userIndex := 0; userIndex < len(users); userIndex++ {
    fmt.Println(users[userIndex])
}

// Do this instead
for i := 0; i < len(users); i++ {
    fmt.Println(users[i])
}

However, once that variable leaves the local function or becomes a global configuration, give it a full name. The name should grow as the scope grows.

How do I name interfaces without making them sound like Java?

In Java, you might see UserPersistenceInterface or MessageService. In Go, we avoid the word "Interface" in the name entirely. Instead, we use the "-er" suffix to describe what the type does.

If an interface has a Read method, it's a Reader. If it has a Write method, it's a Writer. I've seen people create a UserStorageInterface, but it's much more Go-like to call it a Storer or a Repository.

// Not idiomatic
type UserSaverInterface interface {
    SaveUser(u *User) error
}

// Idiomatic
type Saver interface {
    Save(u *User) error
}

Keep them small. The most powerful interfaces in Go are often just one method. This makes your code incredibly flexible because it's easy for other types to satisfy a one-method interface without even knowing it.

Is it really idiomatic to have "if err != nil" every three lines?

Yes, it is. I know it feels repetitive, but this is a core part of Go's philosophy: errors are values, not exceptions. We don't want "hidden" control flow jumping to a catch block halfway across the file.

The trick to making this readable is the "Happy Path." You want your successful logic to be aligned to the left margin of your screen. Whenever you hit an error, handle it and return immediately. This prevents your code from becoming a giant pyramid of nested if statements.

// The "Pyramid of Doom" - Avoid this
func ProcessFile(path string) error {
    f, err := os.Open(path)
    if err == nil {
        defer f.Close()
        data, err := io.ReadAll(f)
        if err == nil {
            // Logic continues to drift right...
            return nil
        }
        return err
    }
    return err
}

// The Happy Path - Idiomatic Go
func ProcessFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err // Return early
    }
    defer f.Close()

    data, err := io.ReadAll(f)
    if err != nil {
        return err // Return early
    }

    // The main logic stays left-aligned
    return processData(data)
}

What should I use for method receivers?

You'll notice Go doesn't use this or self. Instead, you choose a name for the receiver. The convention is to use one or two letters that represent the type. If you're writing a method for a UserSession struct, don't use s if session is more clear, but definitely don't use this.

I usually go with the first letter of the struct. If the struct is Client, the receiver is c. It keeps the method bodies concise and avoids clashing with local variables.

type Client struct {
    Timeout time.Duration
}

// Use 'c' for Client, not 'this'
func (c *Client) SetTimeout(t time.Duration) {
    c.Timeout = t
}



📋 Practical Task

Refactoring the Legacy Order Processor

You've been handed a piece of code written by a developer who transitioned from Java to Go and brought all their old habits with them. Your task is to refactor the OrderProcessor to follow the "Effective Go" style guide highlights we just covered.

The Goal: Refactor the following code to:

  1. Implement the "Happy Path" by removing nested if blocks for error handling.
  2. Rename the interface to follow the "-er" convention.
  3. Shorten overly verbose variable names in small scopes.
  4. Fix the method receivers to remove this.
package main

import (
    "errors"
    "fmt"
)

type Order struct {
    ID int
}

// REFACTOR THIS INTERFACE
type OrderRepositoryInterface interface {
    SaveOrder(o *Order) error
}

type SQLOrderRepository struct {
    Connection string
}

// REFACTOR THIS RECEIVER
func (this *SQLOrderRepository) SaveOrder(o *Order) error {
    if this.Connection == "" {
        return errors.New("no connection")
    }
    fmt.Printf("Saving order %d to DB\n", o.ID)
    return nil
}

type OrderProcessor struct {
    Repo OrderRepositoryInterface
}

// REFACTOR THIS METHOD
func (this *OrderProcessor) Process(orderID int) error {
    if orderID > 0 {
        var currentOrder = &Order{ID: orderID}
        err := this.Repo.SaveOrder(currentOrder)
        if err == nil {
            fmt.Println("Order processed successfully")
            return nil
        } else {
            return err
        }
    } else {
        return errors.New("invalid order id")
    }
}

func main() {
    repo := &SQLOrderRepository{Connection: "postgres://..."}
    processor := &OrderProcessor{Repo: repo}
    
    err := processor.Process(123)
    if err != nil {
        fmt.Println("Error:", err)
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.