This post will describe the key differences between functions and methods in Go, and when it’s best to use them.

banner image

Functions and methods are both used extensively in Go to provide abstractions and make our programs easier to read and reason about. On the surface, functions and methods both look similar, but there are some important semantic differences which can make a big difference to the readability of your code.

Syntax

Declaration syntax

A function is declared by specifying the types of the arguments, the return values, and the function body:

type Person struct {
  Name string
  Age int
}

// This function returns a new instance of `Person`
func NewPerson(name string, age int) *Person {
  return &Person{
    Name: name,
    Age: age,
  }
}

A method on the other hand is declared by additionally specifying the “receiver” (which in OOP terms would be the “class” that the method is a part of):

// The `Person` pointer type is the receiver of the `isAdult` method
func (p *Person) isAdult() bool {
  return p.Age > 18
}

In the above method declarations, we declared the isAdult method on the *Person type.

Execution syntax

Functions are called independently with the arguments specified, and methods are called on the type of their receiver:

p := NewPerson("John", 21)

fmt.Println(p.isAdult())
// true

Interchangeability

Functions and methods can theoretically be interchanged. For example, we could turn the isAdult method into a function, and have the NewPerson function as a method:

type PersonFactory struct {}

// `NewPerson` is now a method of a `PersonFactory` struct
func (p *PersonFactory) NewPerson(name string, age int) *Person {
  return &Person{
    Name: name,
    Age: age,
  }
}

// `isAdult` is now a function where we're passing the `Person` as an argument
// instead of a receiver
func isAdult(p *Person) bool {
  return p.Age > 18
}

The execution syntax in this case, looks a bit weird:

factory := &PersonFactory{}

p := factory.NewPerson("John", 21)

fmt.Println(isAdult(p))
// true

The above code looks more unnecessary and complex than it needs to be. This shows us that the difference in methods and functions is mostly syntactic, and you should use the appropriate abstraction depending on the use case.

Use cases

Let’s go through some common use cases encountered in Go applications, and the appropriate abstraction (function or method) to use for each of them:

Method chaining

One very useful property of methods is the ability to chain them together, while still keeping your code clean. Let’s take an example of setting some attributes of Person using chaining:

type Person struct {
	Name string
	Age  int
}

func (p *Person) withName(name string) *Person {
	p.Name = name
	return p
}

func (p *Person) withAge(age int) *Person {
	p.Age = age
	return p
}

func main() {
	p := &Person{}

	p = p.withName("John").withAge(21)
	
  fmt.Println(*p)
  // {John 21}
}

If we were to use functions for the same thing, it would look pretty horrible:

p = withName(withAge(p, 18), "John")

Stateful vs stateless execution

In the interchangeability example, we saw the use of a PersonFactory object in order to create a new instance of person. As it turns out, this is an anti-pattern and should be avoided.

It’s much better to use functions like the NewPerson function declared previously, for stateless execution.

“Stateless” here means any piece of code that always returns the same output for the same input

The corollary here, is that if you find that a function reads and modifies a lot of values of a particular type, it should probably be a method of that type.

Semantics

Semantics refers to how the code read. If you read the code aloud in your spoken language, what makes more sense?

Let’s look at function and method implementation for isAdult

customer := NewPerson("John", 21)

// Method
customer.isAdult()

// Function
isAdult(customer)

Here customer.isAdult() reads much better for asking “Is the customer an adult?” as compared to isAdult(customer). Further when you ask “is x and adult?”, it is always asked in the context of the person x.

Conclusion

Although we went over some of the key differences and use cases for functions and methods in Go, there are always exceptions! It’s important to not take any of these rules as set-in-stone.

In the end, the difference between functions and methods is in how the resulting code reads. If you or your team feel that one way reads better than the other,then that’s the correct abstraction!