This post will describe the key differences between functions and methods in Go, and when it’s best to use them.
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!