Go for Pythonistas: Building Web Apps with Go
Section 6 of 13

Go functions methods and interfaces for Python developers

Functions, Methods, Interfaces, and the Duck Typing You Left Behind

Now that you understand how Go structures and organizes data—through structs, maps, and the strict type system—it's time to see how that data actually moves through your program. In the previous section, we built request and response structs, stored them in maps, and used the comma-ok pattern to safely retrieve values. But structs sitting in maps don't do anything by themselves. Functions and methods are what bring them to life.

If there's one section of this course that will genuinely change how you think about type systems—not just in Go, but in Python too—it's this one. Functions and methods are mostly what you'd expect from Python, with a few pleasant surprises. Interfaces are something else entirely. They're the part of Go that Python developers initially find weird, then quietly love, then start wishing Python had a better version of. And here's the key: once you understand how Go functions work—especially their signature contracts and multiple return values—you'll see why those struct definitions from the last chapter are so powerful. Let's work through it systematically.

Function Declarations: Say What You Mean

In Python, a function signature is a suggestion:

def fetch_user(user_id):
    # might return a User, might return None, might raise an exception
    pass

You have to read the docs or the code to know what could go wrong. Go takes the opposite approach:

func FetchUser(userID int) (*User, error)

There it is, right in the signature: this function returns two things—a pointer to a User, or it doesn't. And an error, or it doesn't. The caller sees immediately: this function might fail. You don't need to read the docs or the implementation. The signature is the contract.

This is Go's most distinctive pattern for function declarations: multiple return values. Most functions return one thing. Many return two things—a result and an error. Some return three. It's not common to return more than that (though it's possible).

func Add(a, b int) int {
    return a + b
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

func ParseConfig(filename string) (*Config, []Warning, error) {
    // ...
}

The most common pattern is result, error. That second return value is always error information—either nil (no error) or an actual error. By convention, the error is always the last return value.

result, err := SomeFunction()
if err != nil {
    // handle the error
    return err
}
// use result

Remember: The pattern result, err := someFunction() is the single most common Go pattern you'll encounter. You'll see it hundreds of times in any real Go codebase. Get comfortable with it now.

Named Return Values: Power Tool, Use Sparingly

Go lets you name your return values in the signature:

func minMax(nums []int) (min, max int) {
    min = nums[0]
    max = nums[0]
    for _, n := range nums {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return // "naked return" — returns min and max
}

Named return values do two things: they document what each return value represents (the reader knows at a glance what you're returning), and they initialize those variables automatically so you can use a "naked return" at the end without explicitly listing what to return.

In practice: use named returns sparingly. They're genuinely useful in short functions where they add clarity, and they're occasionally useful for modifying return values in defer statements (an advanced pattern we'll touch on later). In longer functions, naked returns make the code harder to follow because you lose track of what's being returned.

The Go by Example site shows this pattern in action if you want to see it side-by-side with regular returns.

Variadic Functions: Go's *args

Python's *args lets you accept any number of positional arguments. Go has the same idea:

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)       // 6
sum(1, 2, 3, 4, 5) // 15

The ...int syntax means "zero or more ints." Inside the function, nums is a slice ([]int), so you can range over it, check its length, index into it — anything you'd do with a regular slice.

You can also spread a slice into a variadic function, which is Go's equivalent of Python's *my_list unpacking:

nums := []int{1, 2, 3, 4}
sum(nums...) // spread the slice

The ... at the call site means "unpack this slice." It's syntactically different from Python but semantically identical.

First-Class Functions and Closures: Familiar Territory

If you use Python's lambda, decorators, or pass functions as arguments, you'll be right at home. Go treats functions as first-class values — you can assign them to variables, pass them as arguments, and return them from functions.

// Assigning a function to a variable
double := func(x int) int {
    return x * 2
}
fmt.Println(double(5)) // 10

// Passing a function as an argument
func apply(nums []int, f func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = f(n)
    }
    return result
}

nums := []int{1, 2, 3, 4}
doubled := apply(nums, double) // [2, 4, 6, 8]

Closures work exactly as you'd expect — an inner function captures variables from its enclosing scope:

func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

counter := makeCounter()
counter() // 1
counter() // 2
counter() // 3

Each call to makeCounter() creates a new count variable, and the returned function closes over it. This is conceptually identical to Python's closures.

This pattern shows up constantly in Go web development — middleware, handlers, and request lifecycle logic all lean heavily on functions-as-values.

Methods: Behavior Without Classes

Go doesn't have classes. This is the fact Python developers stare at and then slowly realize isn't as limiting as it sounds.

In Python, you attach behavior to data by putting it in a class:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

In Go, you attach behavior to a struct using a method — a function with a special "receiver" argument that specifies which type it belongs to:

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

You call it the same way:

rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50

The (r Rectangle) part before the function name is the receiver — it's essentially self in Python, just written in a different position and with an explicit type. r is the conventional name for the receiver (though you can use anything — some Go developers use the first letter of the type name, so rect or just r for Rectangle).

Pointer Receivers vs. Value Receivers: The Concept With No Python Equivalent

This is where things get genuinely new. In Go, when you define a method, you choose whether the receiver is a value or a pointer:

// Value receiver — operates on a copy
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Pointer receiver — operates on the original
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

The difference matters because Go is pass-by-value by default. When you call a method with a value receiver, Go makes a copy of your struct and passes that copy to the method. Changes inside the method don't affect the original.

With a pointer receiver (*Rectangle), Go passes the address of your struct. Changes inside the method do affect the original.

rect := Rectangle{Width: 10, Height: 5}

rect.Scale(2)
fmt.Println(rect.Width)  // 20 — modified in place
fmt.Println(rect.Height) // 10 — modified in place
fmt.Println(rect.Area()) // 200 — reads from the modified struct

The practical rule is straightforward:

graph TD
    A[Does the method modify the struct?] -->|Yes| B[Use pointer receiver: *MyType]
    A -->|No| C[Does the struct contain a mutex or large data?]
    C -->|Yes| B
    C -->|No| D[Use value receiver: MyType]
    B --> E[Changes visible to caller]
    D --> F[Works on a copy — original unchanged]

Warning: If you mix pointer and value receivers on the same type without thinking it through, you'll get confusing behavior. The convention is: if any method on a type uses a pointer receiver, use pointer receivers for all methods on that type. Consistency prevents bugs.

Python developers sometimes find this confusing at first because Python always passes references to objects (with some nuance around immutable types). In Go, the distinction is explicit and in your control. Once it clicks, you'll actually appreciate knowing exactly whether a method mutates or reads.

Interfaces: Where Everything Gets Interesting

This is the big one. Clear your mental cache for a moment.

In Python, if you want to enforce that multiple classes implement the same interface, you have options:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass

class Rectangle(Shape):
    def area(self) -> float:
        return self.width * self.height

You explicitly declare that Rectangle implements Shape. The relationship is stated. Python's newer Protocol class (from typing) is a bit more relaxed — it uses structural typing — but the mental model is still "I need to declare something."

Go interfaces work completely differently, and understanding this difference is one of the most important reframes in this course.

In Go, interfaces are satisfied implicitly.

There's no implements keyword. There's no inheritance declaration. If your type has all the methods an interface requires, it automatically satisfies that interface. Period.

Here's what that looks like:

// Define an interface
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Rectangle — no mention of Shape anywhere
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Circle — also no mention of Shape
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// This function accepts anything that implements Shape
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

// Both work — no casting, no explicit declaration
printShapeInfo(Rectangle{Width: 10, Height: 5})
printShapeInfo(Circle{Radius: 7})

Rectangle and Circle never mention Shape. They just happen to have the right methods. Go sees that they do and lets them be used wherever a Shape is expected.

This is structural typing — types are compatible if they have the right structure (methods), regardless of what they say about themselves. Go does not require explicit "class implements interface" declarations — and that's a feature, not an oversight.

Remember: Go's interface satisfaction is checked at compile time, not runtime. This is the key difference from Python's duck typing. The duck-typing philosophy (if it walks like a duck and quacks like a duck, treat it as a duck) is preserved, but Go verifies at compile time that the duck actually has the required methods. Python checks at runtime. One gives you safety; the other gives you flexibility.

Why Implicit Interfaces Are Powerful

At first this feels risky. How do you know which interfaces a type satisfies? You have to check — or use your IDE.

But think about what implicit interfaces enable from the other direction. Imagine you're consuming a third-party library that returns a *sql.DB struct. You want to write tests that don't actually hit a database. In Python, you'd need to subclass or monkey-patch. In Go, you just define an interface that has the methods you need from *sql.DB, and now you can write a mock that satisfies it — without touching the database package at all:

// Define only the database operations you actually use
type UserStore interface {
    QueryRow(query string, args ...interface{}) *sql.Row
    Exec(query string, args ...interface{}) (sql.Result, error)
}

// Your function takes the interface, not the concrete type
func GetUser(db UserStore, id int) (*User, error) {
    // ...
}

// In tests, you pass a mock. In production, you pass *sql.DB.
// *sql.DB satisfies UserStore because it has those methods.

You defined the interface where you use it, not where it's implemented. This is Go's "accept interfaces, return concrete types" idiom — one of the most important Go design principles. It makes code testable, composable, and decoupled without requiring any coordination between the library and your code.

Python's Protocol class (PEP 544) aims for the same thing and it's genuinely good, but it still requires importing typing and explicitly annotating things. Go's version requires nothing — it just works.

The Empty Interface: Go's Escape Hatch

Sometimes you genuinely don't know the type of something at compile time. Go provides an escape hatch: the empty interface.

// Old syntax (Go < 1.18)
var anything interface{}

// New syntax (Go 1.18+)
var anything any

anything = 42
anything = "hello"
anything = []int{1, 2, 3}

The empty interface interface{} (aliased as any in modern Go) has no methods, so every type in Go satisfies it. It's like Python's object — everything is one.

You'll see this in the standard library and in older Go code. The canonical example is fmt.Println, which accepts ...any — that's why it can print integers, strings, structs, and everything else.

Warning: any is an escape hatch, not a default. If you're using any everywhere, you're writing Python in Go syntax and missing the point of the type system. Use it when you genuinely have heterogeneous data (JSON deserialization, certain generic utilities, fmt functions). Use concrete types or interfaces everywhere else.

Type Assertions and Type Switches

When you have a value stored as an interface, you sometimes need to get the concrete type back. Go gives you two tools.

Type assertion — extract the concrete type:

var val any = "hello"

// Safe type assertion — returns value and success bool
s, ok := val.(string)
if ok {
    fmt.Println(s) // "hello"
} else {
    fmt.Println("not a string")
}

// Unsafe type assertion — panics if wrong type
s := val.(string) // fine if val is a string; panics otherwise

Prefer the safe two-value form unless you're certain about the type. Panics in production are unpleasant.

Type switch — handle multiple possible types:

func describe(val any) string {
    switch v := val.(type) {
    case int:
        return fmt.Sprintf("integer: %d", v)
    case string:
        return fmt.Sprintf("string: %q", v)
    case bool:
        return fmt.Sprintf("boolean: %v", v)
    default:
        return fmt.Sprintf("unknown type: %T", v)
    }
}

The switch v := val.(type) syntax is a Go-specific idiom. Inside each case, v is already typed as the concrete type — so in the case string: branch, v is a string and you can call string methods on it. This is cleaner than Python's isinstance checks:

# Python equivalent
def describe(val):
    if isinstance(val, int):
        return f"integer: {val}"
    elif isinstance(val, str):
        return f"string: {val!r}"
    # ...

The type switch is Go's pattern for this. It's exhaustive, compiler-friendly, and handles the branching cleanly.

Practical Example: A Handler Interface for Web Processing

Let's ground all of this in something concrete. One of the first interfaces you'll encounter in Go web development is http.Handler from the standard library. Understanding how it works will make the rest of your Go web journey click.

Here's the interface definition (from Go's standard library):

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

That's it. One method. Any type that has a ServeHTTP(ResponseWriter, *Request) method is an http.Handler. No registration, no declaration.

Let's build a simplified version to see the pattern:

package main

import (
    "fmt"
    "net/http"
)

// A struct with some state
type GreetingHandler struct {
    Greeting string
}

// Implement the http.Handler interface — no explicit declaration
func (g GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s, %s!", g.Greeting, r.URL.Path[1:])
}

func main() {
    handler := GreetingHandler{Greeting: "Hello"}
    // http.ListenAndServe accepts an http.Handler
    // GreetingHandler satisfies that interface automatically
    http.ListenAndServe(":8080", handler)
}

Now let's add middleware — a wrapper that adds behavior before or after the handler runs. This is where interfaces really shine:

// A middleware that logs requests
type LoggingMiddleware struct {
    Next http.Handler
}

func (l LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Request: %s %s\n", r.Method, r.URL.Path)
    l.Next.ServeHTTP(w, r) // call the wrapped handler
    fmt.Printf("Response sent\n")
}

func main() {
    handler := GreetingHandler{Greeting: "Hello"}
    logged := LoggingMiddleware{Next: handler}
    
    // Both satisfy http.Handler — they compose naturally
    http.ListenAndServe(":8080", logged)
}
graph TD
    A[HTTP Request] --> B[LoggingMiddleware.ServeHTTP]
    B --> C[logs request]
    C --> D[GreetingHandler.ServeHTTP]
    D --> E[writes response]
    E --> F[logs completion]
    F --> G[HTTP Response]

LoggingMiddleware wraps any http.Handler and is itself an http.Handler. This is Go's composable middleware pattern — and it only works this elegantly because of implicit interface satisfaction. You can wrap handlers inside handlers, chain middleware, and swap implementations without any of them knowing about each other.

This is the pattern that powers most Go web frameworks, including Gin, Chi, and Echo. Once you understand it, the frameworks stop feeling like magic and start feeling like a natural extension of the language.

Interfaces vs. Python Protocols: The Full Comparison

Let's be explicit about what you're trading away and what you're gaining:

Concept Python Go
Declaration Explicit (class Foo(ABC)) or Protocol Implicit — just have the methods
When checked Runtime (or mypy static analysis) Always compile time
Defining interfaces for others' types Requires monkey-patching or Protocol Just define the interface in your package
Cost of checking if type satisfies interface Runtime isinstance Zero — compiler handles it
Partial satisfaction isinstance still works if subclassed All methods required — no partial credit

The implicit nature means you can define interfaces after the implementing types exist — even if those types are in packages you don't own. You're describing a capability, not a lineage. This fundamentally changes how you architect code.

Interfaces in Go are one of the core ways you achieve polymorphism without inheritance. Go's designers made an explicit choice: composition and interfaces over inheritance hierarchies. After working with both, many developers find Go's approach cleaner for anything larger than a few hundred lines.

Putting It Together: The Mental Model Shift

Here's the frame that makes everything in this section cohere:

Go's type system is duck typing, but the duck has been checked by a vet before it's allowed into the pond.

Python's duck typing says: "if it walks and quacks, we'll assume it's a duck at runtime and find out if we're wrong." Go's interface system says: "if it walks and quacks according to these method signatures, the compiler has verified it's duck-compatible before your code runs."

The philosophy is identical. The implementation is different. And that difference is what makes Go web services reliable at scale — the entire class of "I passed the wrong thing somewhere" runtime errors disappears, and in exchange you write slightly more explicit code.

Functions in Go are explicit, composable, and safe. Methods attach behavior to types without the inheritance overhead. Interfaces wire it all together with structural typing that the compiler enforces. Once you internalize this model, you'll find Go code surprisingly easy to navigate — even in large codebases you've never seen before. The types tell you everything.

Next, we'll tackle the two things that genuinely do require adjusting your mental model: pointers and error handling.