What Is the Difference Between a Function and a Method in Go

Functions are standalone code blocks, while methods are functions bound to a specific type via a receiver.

When a function needs a home

You are building a game server. You have a Player struct that tracks position, health, and inventory. A packet arrives telling the player to move north. You need to update the coordinates. You could write a standalone function movePlayer(p *Player, dx int, dy int) and call it with movePlayer(&player, 0, 1). Or you could attach the logic to the type: func (p *Player) move(dx int, dy int) and call it with player.move(0, 1).

Both approaches compile. Both produce the same machine code. The difference is organization and intent. A function is a standalone operation. A method is an operation bound to a specific type. In Go, methods let you group behavior with data, satisfy interfaces, and use dot notation that reads like natural language. They also come with rules about receivers, addressability, and method sets that trip up developers coming from class-based languages.

The secret: methods are functions with a receiver

Go has no classes. There is no hidden object model. A method is literally a function with a special first parameter called a receiver. That is the entire mechanism.

When you define a method, you place a parameter list in parentheses before the method name. This parameter is the receiver. It specifies the type the method belongs to. When you call a method using dot notation, the compiler rewrites the call to pass the receiver as the first argument.

Think of a function as a recipe in a cookbook. You bring the ingredients to the recipe. A method is like a recipe printed on the back of the ingredient's packaging. The ingredient knows how to transform itself. Under the hood, the kitchen still uses the same recipe. The packaging just makes it convenient to find the right instructions for that specific item.

The compiler translation is mechanical. If you write p.move(0, 1), the compiler looks up the type of p. If p is a *Player, it finds the method func (p *Player) move(dx int, dy int) and rewrites the call to move(p, 0, 1). The dot notation is syntactic sugar. The receiver is explicit. There is no implicit this pointer that you cannot see. If you need to pass the receiver to another function, you just pass it.

Minimal example: function vs method

Here is the mechanical difference. A function stands alone. A method binds to a type. Notice how the receiver sits in parentheses before the name, and how the call site changes.

package main

import "fmt"

type Counter struct {
	value int
}

// IncrementFunc is a standalone function.
// It takes a Counter as an argument and returns a new Counter.
// The caller must pass the struct and capture the result.
func IncrementFunc(c Counter) Counter {
	c.value++
	return c
}

// IncrementMethod is a method on *Counter.
// The receiver (c) is a pointer, so it modifies the original struct.
// The receiver name c follows convention: one or two letters matching the type.
func (c *Counter) IncrementMethod() {
	c.value++
}

func main() {
	c := Counter{value: 0}

	// Function call: pass the value, capture the return.
	// IncrementFunc gets a copy, modifies the copy, and returns it.
	c = IncrementFunc(c)

	// Method call: dot notation.
	// The compiler rewrites this to IncrementMethod(&c).
	// The method modifies the original struct directly via the pointer.
	c.IncrementMethod()

	fmt.Println(c.value) // 2
}

Goroutines are cheap. Methods are just functions. The receiver is the only difference.

How the compiler handles the dot

The compiler does more than rewrite the call. It handles addressability automatically. If you define a method with a pointer receiver, you can still call it on a value variable, as long as the variable is addressable.

In the example above, c is a Counter value. IncrementMethod has a *Counter receiver. The call c.IncrementMethod() works because c is a variable stored in memory. The compiler sees the mismatch, checks that c is addressable, takes the address, and calls IncrementMethod(&c). You do not need to write (&c).IncrementMethod().

This convenience has limits. Map values are not addressable. You cannot take the address of a map element. If you define a pointer method on a struct stored in a map, calling that method on the map value fails.

package main

import "fmt"

type Item struct {
	name string
}

// UpdateName modifies the item.
// Pointer receiver allows mutation.
func (i *Item) UpdateName(newName string) {
	i.name = newName
}

func main() {
	store := map[string]Item{
		"key": {name: "old"},
	}

	// This fails. Map values are not addressable.
	// The compiler cannot take the address of store["key"] to pass to UpdateName.
	// Error: cannot call pointer method on store["key"]
	// store["key"].UpdateName("new")

	// Fix: extract the value, modify it, and put it back.
	item := store["key"]
	item.UpdateName("new")
	store["key"] = item

	fmt.Println(store["key"].name) // new
}

The compiler error is explicit: cannot call pointer method on store["key"]. The fix is to extract the value, call the method, and store the result. This rule keeps Go's memory model simple. Map values live in a hash table. Taking their address would require the map to allocate a stable location, which breaks the data structure's invariants.

Realistic example: satisfying an interface

Methods shine when you need to satisfy an interface. Go interfaces are structural. A type satisfies an interface if it has all the methods in the interface set. You cannot implement an interface with standalone functions. You must define methods on a type.

The standard library relies on this pattern. io.Reader requires a Read method. io.Writer requires a Write method. If you want to use io.Copy or pipe data through a library, your type must have the correct methods.

package main

import (
	"fmt"
	"io"
)

// CustomReader implements io.Reader.
// The Read method must have the exact signature required by the interface.
// The receiver binds this logic to CustomReader.
type CustomReader struct {
	data []byte
	pos  int
}

// Read satisfies io.Reader.
// It copies data into the provided buffer and advances the position.
// Returning io.EOF signals the end of the stream.
func (r *CustomReader) Read(p []byte) (n int, err error) {
	if r.pos >= len(r.data) {
		return 0, io.EOF
	}
	n = copy(p, r.data[r.pos:])
	r.pos += n
	return n, nil
}

func main() {
	reader := &CustomReader{data: []byte("Go methods")}
	buf := make([]byte, 4)

	// io.Copy uses the Read method.
	// It does not know about CustomReader.
	// It only knows the method exists and matches the interface.
	n, _ := reader.Read(buf)
	fmt.Printf("Read %d bytes: %s\n", n, buf)
}

The convention "accept interfaces, return structs" guides this design. Functions should accept interfaces like io.Reader to allow callers to pass any compatible type. Functions should return concrete structs so callers have access to all fields and methods. Methods enable the interface satisfaction that makes this pattern work.

Pitfalls: method sets and external types

Two common pitfalls involve method sets and package boundaries.

You cannot define a method on a type defined in another package. Go restricts method definitions to the package that defines the type. This prevents conflicts and keeps the type's behavior localized. If you try to add a method to string or net.Conn, the compiler rejects it.

package main

// This fails. string is defined in the built-in package.
// You cannot add methods to types from other packages.
// Error: invalid receiver type string (string is not from current package)
// func (s string) Shout() string {
//     return strings.ToUpper(s)
// }

func main() {
	// Use a wrapper type instead.
	type LoudString string

	s := LoudString("hello")
	fmt.Println(s) // hello
}

The error is invalid receiver type string (string is not from current package). The solution is to define a new type based on the original type. type LoudString string creates a distinct type. You can define methods on LoudString. You can convert between string and LoudString using LoudString(s) and string(s).

Method sets are the second pitfall. The method set of a type determines which interfaces it satisfies. The method set of a type T includes only methods with receiver T. The method set of a type *T includes methods with receiver T AND methods with receiver *T.

This means a pointer type satisfies more interfaces than the value type. If an interface requires a method with a pointer receiver, only the pointer type satisfies the interface. The value type does not.

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Circle struct {
	radius float64
}

// Area has a value receiver.
// Circle satisfies Shape.
// *Circle also satisfies Shape.
func (c Circle) Area() float64 {
	return 3.14 * c.radius * c.radius
}

type Box struct {
	width, height float64
}

// Area has a pointer receiver.
// *Box satisfies Shape.
// Box does NOT satisfy Shape.
func (b *Box) Area() float64 {
	return b.width * b.height
}

func main() {
	var s Shape

	// Works: Circle has value receiver.
	s = Circle{radius: 5}

	// Works: *Box has pointer receiver, so *Box satisfies Shape.
	s = &Box{width: 4, height: 5}

	// Fails: Box does not satisfy Shape.
	// Error: cannot use Box literal (value of type Box) as Shape value in assignment:
	// Box does not implement Shape (Area method has pointer receiver)
	// s = Box{width: 4, height: 5}
}

The compiler error is Box does not implement Shape (Area method has pointer receiver). This happens because Box lacks the Area method in its method set. Only *Box has it. If you need the value type to satisfy the interface, change the receiver to a value receiver, provided the method does not need to mutate the struct.

Methods are functions with a receiver. The dot is sugar. Method sets control interface satisfaction. Check the receiver.

Decision: functions, methods, and receivers

Use a standalone function when the operation does not logically belong to a single type, or when you need to work with types from other packages. Use a function when you are combining multiple types, like fmt.Println or sort.Slice. Use a function when you want to keep the type definition clean and avoid bloat.

Use a method when the action is intrinsic to the type's behavior, or when you need to satisfy an interface. Use a method when dot notation improves readability, like conn.Close() or req.Header.Set(). Use a method when you are building a type that other packages will use, and you want to expose a clear API.

Use a pointer receiver when the method modifies the struct, or when the struct is large and copying is expensive. Use a pointer receiver when the method must handle nil receivers gracefully. Use a pointer receiver when you need the type to satisfy an interface that requires mutation.

Use a value receiver when the method only reads data, the struct is small, and you want to allow calls on unaddressable values like map elements. Use a value receiver when you want to ensure the method cannot mutate the receiver, providing a guarantee to callers. Use a value receiver when the type is immutable by design.

Use a wrapper type when you need to add behavior to a built-in type or a type from another package. Define a new type based on the original, implement the methods, and provide conversion functions. This pattern lets you extend functionality without violating package boundaries.

Don't fight the type system. Wrap the value or change the design.

Where to go next