Functions and Methods

The Refactoring That Changed My Mind

Week two with Go, I was refactoring our authentication service. In Python, I'd written:

def authenticate_user(email, password):
    try:
        user = database.get_user(email)
        if check_password(user, password):
            return create_token(user)
        else:
            return None
    except UserNotFound:
        return None
    except DatabaseError as e:
        log_error(e)
        return None

Every error path returned None. The caller had to guess why authentication failed. Was it bad credentials? Database error? User not found?

In Go, I rewrote it:

func authenticateUser(email, password string) (string, error) {
    user, err := database.GetUser(email)
    if err != nil {
        return "", fmt.Errorf("failed to get user: %w", err)
    }
    
    if !checkPassword(user, password) {
        return "", errors.New("invalid credentials")
    }
    
    token, err := createToken(user)
    if err != nil {
        return "", fmt.Errorf("failed to create token: %w", err)
    }
    
    return token, nil
}

Two return values changed everything. The caller gets both the result AND the error. No guessing. No exceptions. No hidden control flow.

This article covers how Go's approach to functions eliminates entire classes of bugs.


Function Declaration

Basic Function

Syntax: func keyword, name, parameters in parentheses, body in braces.

Function with Return Value

Return type comes after parameters.

Multiple Parameters of Same Type


Multiple Return Values

This is Go's killer feature for error handling.

Returning Multiple Values

Pattern: Return (result, error) - result first, error last.

Real-World Example: File Reading

Every step can fail. Every error is handled explicitly.

Ignoring Return Values

Use _ to explicitly ignore values. The compiler won't complain.


Named Return Values

Named returns create variables automatically:

Benefits:

  • Documents what's being returned

  • Variables auto-initialized to zero values

  • Naked return returns named values

When I use them: Complex functions where return values need documentation.

Named Returns for Clarity

vs


Variadic Functions

Functions that accept variable number of arguments:

The ... creates a slice of the type.

Passing Slice to Variadic Function

Real Example: Logging Function

This is how fmt.Printf works internally!


Functions as Values

Functions are first-class citizens in Go.

Assign Function to Variable

Anonymous Functions

Immediately Invoked Function

Real Example: Middleware Pattern

From my HTTP server:


Closures

Functions can capture variables from surrounding scope:

Real Example: Rate Limiter


Methods on Types

Go doesn't have classes, but you can define methods on types.

Method Declaration

The (u User) is the receiver.

Value Receivers vs Pointer Receivers

This tripped me up initially:

Rule of thumb:

  • Use pointer receivers when modifying the receiver

  • Use pointer receivers for large structs (avoid copying)

  • Use value receivers for small, immutable types

Real Example: HTTP Client


Methods on Non-Struct Types

You can define methods on any type you define:

Real Example: Custom String Type


Function Types

You can define function types:

Real Example: Validation Pipeline


Defer with Functions

Defer can wrap cleanup logic:

Defer for Timing


Practical Example: Builder Pattern

Real code for building database queries:


Common Patterns

Options Pattern


Your Challenge

Build a simple calculator with these features:


Key Takeaways

  1. Multiple returns: Return both result and error explicitly

  2. Named returns: Document what's being returned

  3. Variadic functions: Accept variable arguments with ...

  4. Functions as values: Pass functions around like data

  5. Closures: Functions capture surrounding variables

  6. Pointer receivers: Modify the receiver or avoid copying

  7. Value receivers: For small, immutable types

  8. Method chaining: Return *self for fluent APIs


What I Learned

The authentication refactoring taught me that Go's approach to functions isn't restrictive - it's liberating:

  • Multiple returns eliminated exception handling confusion

  • Explicit errors made error paths visible

  • Methods on types organized code without classes

  • Closures enabled powerful patterns (middleware, options)

Coming from Python's single return value and exceptions, this felt verbose. But in production, explicit error handling caught bugs at compile time that would have been runtime crashes.


Next: Data Structures - Arrays, Slices, Maps

In the next article, we'll explore Go's built-in data structures. You'll learn about slices (Go's most-used data structure), maps, and the memory leak that taught me how they really work.

Last updated