Functions and Methods
Master functions, closures, anonymous functions, and methods. Understand value receivers vs pointer receivers and how methods enable polymorphism.
Programs decompose problems into smaller pieces. Calculate a price. Validate user input. Format a date. Process a file. Each operation becomes a function—a named block of code that takes inputs, performs a task, and returns outputs. Functions are Go's primary abstraction mechanism.
Functions organize code into reusable units. You pass data in, get results out, and the logic lives in one place. But some operations naturally belong to specific types. A rectangle has an area. A database connection opens and closes. A user validates their email format. These operations are conceptually tied to the data they operate on.
Go provides both functions and methods. Functions are standalone operations. Methods are functions associated with types. Understanding both enables you to structure code cleanly.
Functions
A function has a name, parameters, return types, and a body:
func add(x int, y int) int {
return x + y
}
Call it by name with arguments:
result := add(3, 5)
fmt.Println(result) // 8
If consecutive parameters have the same type, you can omit the type until the last parameter:
func add(x, y int) int {
return x + y
}
Functions can return multiple values:
func divide(x, y float64) (float64, error) {
if y == 0 {
return 0, errors.New("division by zero")
}
return x / y, nil
}
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 5
Multiple return values enable returning both a result and an error. This pattern is ubiquitous in Go code.
Named Return Values
Functions can name their return values:
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // returns x and y
}
Named returns act as variables declared at the top of the function. A bare return returns the current values of named returns. This works but can reduce clarity—explicit returns are often clearer:
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return x, y // explicit, clearer
}
Named returns document what the function returns and are useful for generated documentation.
Functions Are Values
Functions are first-class values. You can assign them to variables, pass them as arguments, and return them from functions:
func compute(fn func(int, int) int, a, b int) int {
return fn(a, b)
}
func add(x, y int) int {
return x + y
}
func multiply(x, y int) int {
return x * y
}
func main() {
result1 := compute(add, 3, 4) // 7
result2 := compute(multiply, 3, 4) // 12
}
The compute function takes a function as its first parameter. The function type is func(int, int) int—a function taking two ints and returning an int.
Anonymous Functions
Functions can be defined without names—anonymous functions or function literals:
func main() {
add := func(x, y int) int {
return x + y
}
result := add(3, 5)
fmt.Println(result) // 8
}
Anonymous functions are often used inline:
result := compute(func(x, y int) int {
return x * x + y * y
}, 3, 4)
fmt.Println(result) // 25
This creates a function on the fly and passes it to compute. The function calculates the sum of squares without needing a separate named function.
Closures
Functions can capture variables from their surrounding scope. This creates a closure—a function that references variables defined outside its body:
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
c1 := counter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c1()) // 3
c2 := counter()
fmt.Println(c2()) // 1 - separate counter
}
The counter function returns a closure. The closure captures the count variable from counter's scope. Each call to c1() increments and returns the captured count. The variable count persists between calls because the closure holds a reference to it.
Multiple closures from the same counter call share the same count:
func main() {
increment := counter()
decrement := counter()
fmt.Println(increment()) // 1
fmt.Println(increment()) // 2
fmt.Println(decrement()) // 1 - different counter
}
Each invocation of counter() creates a new count variable. Closures from the same invocation share that variable, but closures from different invocations have separate variables.
Closure Memory Model
┌────────────────────────────────────┐
│ Heap │
├────────────────────────────────────┤
│ │
│ counter() call 1: │
│ count: 3 ◄─────┐ │
│ │ │
│ counter() call 2: │ │
│ count: 1 ◄────┐ │ │
│ │ │ │
└──────────────────│─│───────────────┘
│ │
Stack: │ │
┌──────────────────│─│───────────────┐
│ main(): │ │ │
│ c1: (closure) ─┘ │ │
│ c2: (closure) ───┘ │
└────────────────────────────────────┘
Closures reference variables that outlive their creating function's stack frame. Go automatically moves these variables to the heap. The closure holds a pointer to the heap-allocated variable.
Practical Closure Uses
Configuration builders:
func newServer(addr string) *Server {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Server running at %s", addr)
})
return &Server{addr: addr, mux: mux}
}
The handler closure captures addr. Each server instance's handlers access their specific address without passing it explicitly.
Iterator callbacks:
func filter(numbers []int, predicate func(int) bool) []int {
result := []int{}
for _, num := range numbers {
if predicate(num) {
result = append(result, num)
}
}
return result
}
threshold := 10
large := filter(numbers, func(n int) bool {
return n > threshold
})
The predicate closure captures threshold. The filter operates on any criteria without the filter function knowing the details.
Middleware chains:
func logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
The returned handler closure captures next. This pattern chains HTTP handlers for logging, authentication, and other cross-cutting concerns.
Methods
Beyond standalone functions, Go provides methods—functions associated with types. Methods aren't special. They're functions with a receiver parameter that specifies what type they operate on. This simplicity distinguishes Go from object-oriented languages where methods are tightly coupled to classes and inheritance hierarchies.
Basic Method Syntax
Attach a method to a type by specifying a receiver:
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
The receiver (r Rectangle) appears between func and the method name. It declares that Area and Perimeter are methods on the Rectangle type. The receiver gives you access to the struct's fields.
Call methods using dot notation:
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50
fmt.Println(rect.Perimeter()) // 30
The method call syntax looks like accessing a field, but you're invoking a function. The receiver is implicitly passed as the first argument.
Methods Are Functions
Methods are syntactic sugar over functions. The method:
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
Is equivalent to the function:
func RectangleArea(r Rectangle) float64 {
return r.Width * r.Height
}
You can call methods as functions explicitly:
rect := Rectangle{Width: 10, Height: 5}
area := Rectangle.Area(rect) // Call method as function
fmt.Println(area) // 50
This reveals what's happening—the receiver is just the first parameter. Methods are functions with special call syntax.
Value Receivers
The methods above use value receivers—the receiver is a copy of the struct:
func (r Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
rect := Rectangle{Width: 10, Height: 5}
rect.Scale(2)
fmt.Println(rect.Width) // 10, not 20
The Scale method receives a copy of rect. Modifying the copy doesn't affect the original. This is the same behavior as passing structs to functions—structs are values, and values are copied.
Value receivers work well for small types or when you don't need to modify the receiver:
func (r Rectangle) IsSquare() bool {
return r.Width == r.Height
}
This method only reads fields, so a copy is fine.
Pointer Receivers
To modify the receiver, use a pointer receiver:
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
rect := Rectangle{Width: 10, Height: 5}
rect.Scale(2)
fmt.Println(rect.Width) // 20
The receiver type is *Rectangle, a pointer. Inside the method, r is a pointer to the struct. Changes to r.Width modify the original struct.
Go lets you call pointer receiver methods on values directly:
rect := Rectangle{Width: 10, Height: 5}
rect.Scale(2) // Go automatically takes the address: (&rect).Scale(2)
The compiler rewrites rect.Scale(2) to (&rect).Scale(2) automatically. This convenience means you don't clutter code with address-of operators when calling methods.
The reverse works too—calling value receiver methods on pointers:
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
rect := &Rectangle{Width: 10, Height: 5}
area := rect.Area() // Go automatically dereferences: (*rect).Area()
fmt.Println(area) // 50
Go dereferences the pointer automatically. This bidirectional convenience keeps method calls clean.
Choosing Value vs Pointer Receivers
Use pointer receivers when:
The method modifies the receiver:
func (u *User) Activate() {
u.Active = true
u.ActivatedAt = time.Now()
}
The receiver is large:
type BigStruct struct {
Data [10000]int
}
func (b *BigStruct) Process() {
// Avoid copying 80KB
}
Copying large structs on every method call is expensive. Passing a pointer (8 bytes on 64-bit systems) is cheaper.
Consistency—if some methods need pointers, use pointers everywhere:
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func (c *Counter) Value() int {
return c.count
}
The Value method doesn't modify the receiver, but using a pointer receiver maintains consistency with Increment. Mixing value and pointer receivers on the same type is confusing.
Use value receivers when:
The receiver is small and won't be modified:
type Point struct {
X, Y int
}
func (p Point) Distance(other Point) float64 {
dx := p.X - other.X
dy := p.Y - other.Y
return math.Sqrt(float64(dx*dx + dy*dy))
}
The type is a basic type alias:
type Celsius float64
func (c Celsius) ToFahrenheit() float64 {
return float64(c)*9/5 + 32
}
Basic types copy cheaply.
The method should work with read-only values:
Sometimes you want to prevent modification. Value receivers guarantee the original won't change.
Methods on Any Type
Methods aren't restricted to structs. You can attach methods to any type you define:
type Counter int
func (c *Counter) Increment() {
*c++
}
func (c Counter) Value() int {
return int(c)
}
var count Counter
count.Increment()
count.Increment()
fmt.Println(count.Value()) // 2
This works for any named type—integers, strings, slices, maps. You cannot attach methods to types from other packages or to unnamed types, but you can create aliases:
type MyInt int
func (m MyInt) Double() MyInt {
return m * 2
}
This pattern is useful for adding behavior to basic types in your domain. If user IDs are integers, create a type:
type UserID int64
func (id UserID) Valid() bool {
return id > 0
}
Now user IDs have associated validation logic.
Methods on Slices
You can define methods on slice types:
type IntSlice []int
func (s IntSlice) Sum() int {
total := 0
for _, v := range s {
total += v
}
return total
}
func (s IntSlice) Average() float64 {
if len(s) == 0 {
return 0
}
return float64(s.Sum()) / float64(len(s))
}
numbers := IntSlice{1, 2, 3, 4, 5}
fmt.Println(numbers.Sum()) // 15
fmt.Println(numbers.Average()) // 3
This adds domain-specific operations to slices without wrapping them in structs.
Method Values and Expressions
Methods can be assigned to variables. A method value binds the receiver:
rect := Rectangle{Width: 10, Height: 5}
areaFunc := rect.Area // Method value: receiver is bound to rect
fmt.Println(areaFunc()) // 50
The variable areaFunc is a function with no parameters—the receiver is already bound. Calling areaFunc() is like calling rect.Area().
Method expressions keep the receiver as a parameter:
areaFunc := Rectangle.Area // Method expression: receiver is a parameter
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(areaFunc(rect)) // 50
Now areaFunc is a function that takes a Rectangle and returns float64. You pass the receiver explicitly.
Method values are useful for callbacks:
type Handler struct {
path string
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Handling %s", h.path)
}
handler := &Handler{path: "/api"}
http.HandleFunc("/api", handler.ServeHTTP) // Method value as callback
Methods and Interfaces
Methods enable Go's interface system. An interface defines a set of methods, and any type implementing those methods satisfies the interface. This happens implicitly—no explicit declaration required.
type Shape interface {
Area() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
Both Rectangle and Circle satisfy the Shape interface because they have an Area method with the correct signature. You can use them interchangeably:
func PrintArea(s Shape) {
fmt.Println(s.Area())
}
rect := Rectangle{Width: 10, Height: 5}
circle := Circle{Radius: 3}
PrintArea(rect) // 50
PrintArea(circle) // 28.27...
The function PrintArea accepts any type that implements Shape. This is polymorphism without inheritance—types satisfy interfaces through their methods.
We'll explore interfaces deeply in the next article, but methods are the mechanism that makes interfaces work.
Method Sets
Every type has a method set—the collection of methods you can call on that type. For value receivers, the rules are straightforward:
type T struct{}
func (t T) ValueMethod() {}
func (t *T) PointerMethod() {}
var v T
v.ValueMethod() // OK: T's method set includes ValueMethod
v.PointerMethod() // OK: Go takes address automatically
var p *T
p.ValueMethod() // OK: Go dereferences automatically
p.PointerMethod() // OK: *T's method set includes PointerMethod
Both values and pointers can call both methods thanks to automatic address-taking and dereferencing.
However, method sets matter for interfaces:
type Incrementer interface {
Increment()
}
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
var inc Incrementer
c := Counter{}
inc = c // Compile error: Counter doesn't implement Incrementer
inc = &c // OK: *Counter implements Incrementer
The interface variable requires a type whose method set includes Increment. Counter has no Increment method—only *Counter does. You must pass a pointer.
The rule: a type's method set includes methods with value receivers. A pointer to that type's method set includes methods with both value and pointer receivers.
This is why mixing value and pointer receivers on the same type creates problems—some values satisfy an interface while others don't.
Nil Receivers
Methods can be called on nil pointers:
type Tree struct {
Value int
Left *Tree
Right *Tree
}
func (t *Tree) Sum() int {
if t == nil {
return 0
}
return t.Value + t.Left.Sum() + t.Right.Sum()
}
var root *Tree
fmt.Println(root.Sum()) // 0, no panic
The method checks if t is nil before accessing fields. This pattern handles recursive data structures gracefully—nil represents an empty subtree.
Calling methods on nil receivers is safe if the method handles it. If the method doesn't check, it panics when accessing fields:
func (t *Tree) Value() int {
return t.Value // Panics if t is nil
}
var root *Tree
fmt.Println(root.Value()) // panic: nil pointer dereference
Defensive methods check for nil. Methods that assume a valid receiver document that assumption.
Methods vs Functions
When should you use methods instead of functions? Methods make sense when:
The operation logically belongs to the type:
func (r Rectangle) Area() float64 // Method: area of this rectangle
vs
func CalculateRectangleArea(width, height float64) float64 // Function: calculate any area
The method connects the operation to the type. You call rect.Area() instead of CalculateRectangleArea(rect.Width, rect.Height).
You need to satisfy an interface:
Methods enable polymorphism through interfaces. Functions don't participate in Go's interface system.
You want consistent API across types:
user.Save()
order.Save()
product.Save()
Methods provide a uniform interface. All types with Save methods work the same way.
Functions make sense when:
The operation involves multiple types equally:
func Distance(p1, p2 Point) float64
Neither point is more important—the function operates on both equally.
The operation doesn't belong to any particular type:
func ParseConfig(filename string) (Config, error)
Parsing isn't naturally attached to strings or configs—it's a standalone operation.
You're working with types from other packages:
You can't add methods to types you don't own. Use functions instead:
func IsEmpty(s string) bool {
return len(s) == 0
}
You can't add methods to string (it's in another package), so you write a function.
Method Chaining
Pointer receivers enable method chaining by returning the receiver:
type QueryBuilder struct {
table string
where string
limit int
}
func (q *QueryBuilder) Table(name string) *QueryBuilder {
q.table = name
return q
}
func (q *QueryBuilder) Where(condition string) *QueryBuilder {
q.where = condition
return q
}
func (q *QueryBuilder) Limit(n int) *QueryBuilder {
q.limit = n
return q
}
func (q *QueryBuilder) Build() string {
return fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT %d",
q.table, q.where, q.limit)
}
query := &QueryBuilder{}
sql := query.
Table("users").
Where("age > 18").
Limit(10).
Build()
fmt.Println(sql) // SELECT * FROM users WHERE age > 18 LIMIT 10
Each method modifies the builder and returns it, enabling fluent APIs. This pattern appears in builders, configuration objects, and domain-specific languages.
Embedding and Method Promotion
When you embed a type, its methods promote to the outer type:
type Engine struct {
Running bool
}
func (e *Engine) Start() {
e.Running = true
fmt.Println("Engine started")
}
func (e *Engine) Stop() {
e.Running = false
fmt.Println("Engine stopped")
}
type Car struct {
Engine
Make string
Model string
}
car := Car{
Make: "Toyota",
Model: "Camry",
}
car.Start() // Calls car.Engine.Start()
fmt.Println(car.Running) // true
The Car type doesn't have Start or Stop methods, but the embedded Engine does. Those methods promote to Car, so you can call car.Start() directly.
This is composition—Car has an Engine, and Engine's methods become available on Car. It's not inheritance. Car isn't an Engine. It has an Engine.
You can override promoted methods:
func (c *Car) Start() {
fmt.Println("Starting", c.Make, c.Model)
c.Engine.Start()
}
car := Car{Make: "Toyota", Model: "Camry"}
car.Start()
// Output:
// Starting Toyota Camry
// Engine started
The Car's Start method shadows the promoted method. Inside, you can still call the embedded method explicitly with c.Engine.Start().
What's Next
Methods attach behavior to types. They're functions with receivers, nothing more. This simplicity makes Go's object model approachable—there's no hidden state, no virtual dispatch to trace, no inheritance hierarchy to navigate.
The power of methods emerges when combined with interfaces. Interfaces define contracts through method signatures, and any type implementing those methods satisfies the interface automatically. This implicit satisfaction enables polymorphism without coupling types to specific interfaces.
The next article explores interfaces—Go's primary abstraction mechanism. You'll see how interfaces let you write flexible, testable code that depends on behavior rather than concrete types. Methods are the foundation, interfaces are the abstraction layer.
Ready to explore interfaces and how they enable Go's approach to polymorphism?
