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:
anyis an escape hatch, not a default. If you're usinganyeverywhere, 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.
Only visible to you
Sign in to take notes.