net/go.book
All Parts Marketing

Functions and Methods

The previous chapter was about data sitting still — a labeled box, holding a value, refusing to change shape. This chapter is about motion: taking that data and doing something to it, in a way you can name, reuse, and trust to behave the same way every single time you call it. That's the entire idea of a function, and Go's take on it carries a few genuinely unusual design choices that are worth understanding properly, because they shape the look of nearly every piece of Go code you'll read for the rest of this book.

A function in Go is a small machine with a clearly labeled input slot and output slot — and Go, unusually, lets that output slot have more than one compartment. A method is that same machine, permanently bolted to one particular type so it always has a specific piece of data to work on.

Functions are how Go breaks a program into named, reusable pieces of behavior. Methods extend that idea by attaching a function to a type, which is how Go achieves what other languages get from classes — without classes, and without the tangled inheritance trees that come with them. This chapter covers both, along with a handful of Go-specific features (multiple return values, named returns, variadic parameters, closures) that shape how idiomatic Go code looks and reads.

Function Basics

A function declaration names its parameters and their types, and its return type, if any:

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

When consecutive parameters share a type, Go lets you drop the repeated type name:

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

Calling it looks exactly like you'd expect:

sum := add(3, 4) // 7

Multiple Return Values

Here's where Go starts to feel genuinely different from most languages you may have touched before, and it's worth pausing on, because this one decision ripples through nearly every chapter left in this book. A Go function can return more than one value, most commonly a result paired with an error:

func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, fmt.Errorf("divide by zero: %g / %g", a, b)
	}
	return a / b, nil
}

The caller receives both values and is expected to check the error before trusting the result:

result, err := divide(10, 2)
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println("result:", result)

Go's multiple return values, combined with its lack of exceptions, mean an error is not something exceptional that interrupts control flow — it's an ordinary value, checked at the exact point it can occur, right next to the code that produced it.

A couple of chapters ahead covers error handling in real depth; for now, just notice the pattern, because you'll see it in literally every networking example from here to the end of the book: the zero value of the successful return type (0 for float64 above) is paired with a non-nil error, so callers who forget to check err at least get a harmless zero rather than a value that looks plausible but isn't.

Named Return Values

A function can name its return values in the signature, which pre-declares them as zero-valued local variables and lets a bare return send back whatever they currently hold:

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

Named returns are most useful in short functions where the names document intent, and in functions using defer to modify a return value (a pattern you'll meet properly in the error-handling chapter). Overusing them in long functions tends to hurt readability, since the bare return no longer shows at a glance what's being returned.

Naked returns in long functions
A bare return relies on the reader remembering what x and y were set to several lines earlier. In a function longer than about ten lines, this becomes a real readability cost — prefer writing return x, y explicitly once the function grows past a trivial size, even though the names remain declared in the signature.

Variadic Functions

A parameter prefixed with ... accepts zero or more arguments of that type, collected into a slice inside the function:

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

An existing slice can be passed to a variadic parameter by spreading it with ... at the call site:

values := []int{4, 5, 6}
total := sum(values...)

fmt.Println and fmt.Printf are themselves variadic — that's how they accept any number of arguments of any type, which you've already been relying on since the very first "Hello, Go!" without necessarily noticing.

defer: Guaranteed Cleanup

A defer statement schedules a function call to run right before the enclosing function returns, regardless of how it returns — normally, via an early return, or even via a panic:

func readFile(path string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	// ... read from f, possibly returning early on error ...
	return nil
}

defer f.Close() runs no matter which return statement in the function actually executes, which is exactly why "open, then immediately defer the matching close" is one of the most common two-line idioms in Go — it puts the cleanup right next to the setup that requires it, instead of leaving it to be duplicated at every exit point. Once you start writing networking code that opens files, sockets, and database connections, you'll type this particular idiom often enough that your fingers learn it before your brain does.

Deferred calls stack up in last-in-first-out order when a function defers more than one, and — a detail worth knowing before it surprises you — the deferred call's arguments are evaluated immediately when defer runs, not when the deferred call itself later executes:

func trace(label string) {
	fmt.Println("start:", label)
	defer fmt.Println("end:", label) // "label" captured now, not later
}

A deferred method call still needs a valid receiver later
defer conn.Close() is safe because conn itself is captured by reference (it's already a pointer-like type), but deferring a call whose arguments depend on state that changes later in the function can be surprising, since those arguments were fixed at the moment defer ran, not at the moment the deferred call executes.

Functions as Values and Closures

Functions in Go are first-class values: they can be assigned to variables, passed as arguments, and returned from other functions. A function that references variables from an enclosing scope is called a closure, and it captures those variables by reference, not by copying their value at creation time:

func counter() func() int {
	count := 0
	return func() int {
		count++
		return count
	}
}
next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3

Each call to counter() creates a fresh count variable, and the returned closure keeps a live reference to that specific variable — calling next repeatedly increments the same count, while a second call to counter() would start an entirely independent one.

Methods: Functions Bound to a Type

A method is a function with an extra parameter, called the receiver, written before the function name:

type Rectangle struct {
	Width, Height float64
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}
rect := Rectangle{Width: 3, Height: 4}
fmt.Println(rect.Area()) // 12

Calling rect.Area() looks like calling a method in an object-oriented language, but underneath it is ordinary function dispatch: the compiler resolves rect.Area() to a call of Area with rect passed as the receiver. There is no hidden this-like runtime lookup — Go decides which function to call at compile time, based on the receiver's type. If you've come from a language with heavier object machinery, this is often the moment Go starts to feel refreshingly literal: there's no magic here, just a function with one extra, conveniently-placed parameter.

Value Receivers vs. Pointer Receivers

The receiver in the Rectangle example above, (r Rectangle), is a value receiver — the method receives a copy of the struct. A pointer receiver, (r *Rectangle), receives a pointer to the original instead, which lets the method modify the caller's actual data:

func (r *Rectangle) Scale(factor float64) {
	r.Width *= factor
	r.Height *= factor
}
rect := Rectangle{Width: 3, Height: 4}
rect.Scale(2)
fmt.Println(rect) // {6 8}

Note that rect.Scale(2) works even though rect is not a pointer — Go automatically takes its address as a convenience when calling a pointer-receiver method on an addressable value. The reverse also happens: a value-receiver method can be called on a pointer, which Go automatically dereferences.

Choosing the wrong receiver kind is a classic silent bug
If Scale above had used a value receiver, func (r Rectangle) Scale(...), it would modify only its own local copy of r, and the caller's rect would be completely unchanged after the call — with no compiler error or warning of any kind, because the code is entirely valid Go, just not doing what you probably meant.

The rule of thumb: use a pointer receiver whenever the method needs to modify the receiver, when the struct is large enough that copying it on every call would be wasteful, or when any method on the type already uses a pointer receiver (for consistency — mixing receiver kinds on the same type is a common source of confusion). Use a value receiver for small, immutable-feeling types, especially ones you want to be safely copied and compared with ==.

Receiver kind Syntax Can modify caller's data Best for
Value func (r Rectangle) M() No — operates on a copy Small, read-only-feeling types
Pointer func (r *Rectangle) M() Yes — operates on the original Mutating methods, large structs, consistency with other methods

Try It Yourself

Add a Perimeter method to Rectangle, and a second shape to compare value versus pointer receiver behavior directly:

type Circle struct {
	Radius float64
}

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

func (c *Circle) Grow(amount float64) {
	c.Radius += amount
}
  1. Add Perimeter() float64 to Rectangle (formula: 2*(Width+Height)) using a value receiver, and print it for a 3x4 rectangle.
  2. Add the Circle type and both methods shown above (remember to import "math"), and call Area() on a Circle{Radius: 2}.
  3. Call Grow(1) on a Circle value and print its Radius before and after — confirm the change actually took effect, since Grow uses a pointer receiver.
  4. Bonus: change Grow to a value receiver, call it again, and observe that the radius no longer changes — this reproduces the silent bug described above on purpose so you can recognize its symptom later.

A method with the wrong receiver kind doesn't crash — it just quietly does nothing useful, which is exactly why this mistake survives code review more often than it should.

Frequently Asked Questions

Why does Go let a function return two values instead of just throwing an exception like most languages? Because Go's designers wanted every possible failure point visible right in the function signature, not hidden behind a catch block somewhere else in the codebase. Pairing a result with an error — as divide does in this chapter — means the caller checks failure at the exact line it can happen, which is the pattern you'll see in literally every networking example for the rest of this book.

I called Scale on my Rectangle and nothing changed — what went wrong? You almost certainly declared Scale with a value receiver, func (r Rectangle) Scale(...), which means the method only ever modifies its own private copy of r and the caller's original struct is untouched. This is exactly the "classic silent bug" covered in this chapter: there's no compiler error because the code is perfectly valid Go, it just isn't doing what you meant. Switch the receiver to a pointer, func (r *Rectangle) Scale(...), and the mutation will stick.

Do I need to fully understand closures before I get to goroutines later in the book? It helps enormously to at least understand the core idea now: a closure captures variables by reference, not by taking a snapshot of their value. That's precisely what explains the classic loop-variable-capture bug in concurrent code, and while Go 1.22 fixed the most common version of it by giving each loop iteration its own variable, the underlying rule of "closures hold a live reference" is worth having internalized before you're debugging a goroutine that captured the wrong thing.

When should I actually use named return values instead of just writing return x, y? Named returns pull their weight in short functions, where the names in the signature double as documentation, and in functions that use defer to adjust a return value on the way out. Once a function stretches past ten lines or so, a bare return forces the reader to remember what you set several lines earlier, so writing the values out explicitly is the better call at that point.

Why does rect.Scale(2) work even though rect isn't declared as a pointer? Go automatically takes the address of rect for you when you call a pointer-receiver method on an addressable value — it's a convenience the compiler handles at the call site, not something that happens by magic at runtime. The same convenience runs in reverse too: calling a value-receiver method on a pointer automatically dereferences it.

Where This Goes From Here

You've now got the two ingredients Go uses to build almost everything: functions that take data and produce results, honestly reporting failure as a second return value instead of hiding it in an exception; and methods, which are just functions with a permanent, dedicated piece of data to work on. Neither of those ingredients defines what that data looks like yet, though — a Rectangle showed up in this chapter as if it had always existed, and it's fair to wonder how you build a type like that in the first place.

That's exactly where the next chapter picks up: structs, for grouping data together, and interfaces, for describing behavior without caring what concrete type provides it. Between what you already know from this chapter and what's coming next, you'll have essentially everything Go offers instead of classes — and, once you see it work, you may find yourself not missing classes much at all.