How to Write Clean Functions in Go (Small, Focused, Named Returns)

Write Go functions that do exactly one thing, keep them under 20 lines, and use named return values only when they improve readability for complex error handling or logging.

Write Go functions that do exactly one thing, keep them under 20 lines, and use named return values only when they improve readability for complex error handling or logging. Small, focused functions make your code easier to test, refactor, and understand at a glance.

Keep functions small by extracting logic that feels like a "step" into its own helper. If a function needs a comment explaining a block of code, that block is likely a new function. Avoid doing too much in a single place; instead, compose small functions together.

Here is an example of refactoring a bloated function into small, focused units:

// Before: Hard to read, does too much
func ProcessOrder(order *Order) error {
    if order == nil {
        return errors.New("nil order")
    }
    // ... 15 lines of validation ...
    // ... 10 lines of database logic ...
    // ... 5 lines of email sending ...
    return nil
}

// After: Clear intent, easy to test individually
func ProcessOrder(order *Order) error {
    if err := ValidateOrder(order); err != nil {
        return err
    }
    if err := SaveOrderToDB(order); err != nil {
        return err
    }
    return SendConfirmationEmail(order)
}

func ValidateOrder(o *Order) error {
    if o == nil {
        return errors.New("nil order")
    }
    if o.Total <= 0 {
        return errors.New("invalid total")
    }
    return nil
}

Use named return values sparingly. They are powerful when you need to modify a return value before exiting (e.g., wrapping errors or adding context) or when you have multiple return values that are logically grouped. However, avoid them for simple functions as they can obscure the flow and make the function signature harder to scan.

// Good: Named returns help with error wrapping and logging
func FetchData(ctx context.Context) (data []byte, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()

    data, err = io.ReadAll(ctx)
    if err != nil {
        err = fmt.Errorf("read failed: %w", err)
    }
    return // Named returns allow clean early exits with modified values
}

// Avoid: Unnecessary named returns for simple logic
func Add(a, b int) (result int) {
    result = a + b
    return
}
// Prefer:
func Add(a, b int) int {
    return a + b
}

Finally, name your functions to describe their action and effect, not just their type. CalculateTotal is better than GetTotal, and SaveUser is clearer than WriteUser. If you find yourself writing a function that takes more than three arguments, consider grouping them into a struct or splitting the logic further. Clean Go code relies on simplicity and explicit intent.