We've previously learned about functions in Go, and know that they allow us to organize code into repeatable procedures. These procedures can take and process different arguments each time we execute them.
In this topic, we'll learn about a special function type that we can implement on both struct and non-struct types.
What is a method?
In brief, a method is a function with a defined receiver argument. The receiver appears in its argument list between the func keyword and the method name.
Since Go doesn't have classes like other Object-Oriented programming languages, methods are usually defined on struct types; however, we can define them on non-struct types, too!
Now let's go ahead and take a look at an example that creates a method on the User struct and calls it:
package main
import "fmt"
type User struct {
FirstName, LastName, Email string
}
// FormatUserInfo method definition on the 'User' struct type:
func (u User) FormatUserInfo() string {
return fmt.Sprintf("%s %s email is: %s", u.FirstName, u.LastName, u.Email)
}
func main() {
spongebob := User{
FirstName: "SpongeBob",
LastName: "SquarePants",
Email: "[email protected]",
}
fmt.Println(spongebob.FormatUserInfo()) // here we call and print the method
}
// Output:
// SpongeBob SquarePants email is: [email protected]
The syntax for defining a method is very similar to the syntax for defining a function. The key difference is the addition of an extra parameter (u User) after the func keyword for specifying the receiver of the method. In very simple terms, the receiver is a declaration of the type that we decide to define the method on; in this case, we define it on a struct type: User.
After defining the method, we create an instance of the User type within the spongebob variable, and then we can simply call the method using the . dot operator via the spongebob.FormatUserInfo() syntax.
func main() {
...
fmt.Println(User.FormatUserInfo(spongebob)) // another way to call and print the method
}
Defining methods on non-struct types
We can also define methods on non-struct types, as long as the type and the method definitions are present in the same package.
Let's take a look at the implementation of a method in two custom numeric types SquareSide and CircleRadius:
package main
import (
"fmt"
"math"
)
type SquareSide float64 // 'SquareSide' is a non-struct type!
type CircleRadius float64 // 'CircleRadius' is a non-struct type!
// Area method definition on a non-struct type:
func (s SquareSide) Area() float64 {
return math.Pow(float64(s), 2)
}
// Area method with the same name on a different type:
func (c CircleRadius) Area() float64 {
return math.Pi * math.Pow(float64(c), 2)
}
func main() {
s := SquareSide(1.219)
fmt.Printf("Area of square ⏹️: %.3f\n", s.Area())
c := CircleRadius(2.438)
fmt.Printf("Area of circle ⭕: %.3f", c.Area())
}
// Output:
// Area of square ⏹️: 1.486
// Area of circle ⭕: 18.673
Another interesting detail is that we can define methods with the same name on different types. As you can see in the above code, we can define the Area() method on both SquareSide and CircleRadius types. This is one of the key differences between methods and standard Go functions.
Methods with pointer receivers
The previous examples have showcased methods with value receivers; but besides them, we can also create methods with pointer receivers.
When we create a method with a pointer receiver, all the changes made within it are visible to the caller. This is the key difference between methods with value receivers and methods with pointer receivers.
To better understand the concept of methods with pointer receivers, let's take a look at the following example:
package main
import "fmt"
type Animal struct {
Name, Emoji string
}
// UpdateEmoji method definition with pointer receiver '*Animal':
func (a *Animal) UpdateEmoji(emoji string) {
a.Emoji = emoji
}
func main() {
monkey := Animal{
Name: "Monkey",
Emoji: "🐒",
}
fmt.Printf("Monkey's emoji (Before): %s\n", monkey.Emoji)
monkey.UpdateEmoji("🙉")
fmt.Printf("Monkey's emoji (After): %s\n", monkey.Emoji)
}
// Output:
// Monkey's emoji (Before): 🐒
// Monkey's emoji (After): 🙉
Methods with pointer receivers can modify the value to which the receiver points, as UpdateEmoji() does in the above example. Since methods often need to modify their receiver, pointer receivers are more commonly used than value receivers.
UpdateEmoji() with a pointer receiver *Animal, it is not required to use the & operator before the monkey variable. The Go compiler will accept as valid both monkey.UpdateEmoji(...) and (&monkey).UpdateEmoji(...) calls to modify the value of the monkey.Emoji field.Deciding over value or pointer receivers
Now that we've seen both value and pointer receivers, you might be thinking: "What type of receiver should I implement for the methods in my Go program?"
There are two valid reasons to use a pointer receiver:
- The first is so that our method can modify the value that its receiver points to.
- The second is to avoid copying the value on each method call. This tends to be more efficient if the receiver is a large struct with many fields, for example, a struct that holds a large JSON response.
Now let's look in more detail at the use case of pointer receivers and focus on the large struct case described above. In this case, the large struct will not be copied and only a pointer to it will be used in the method, making it more memory efficient.
In all other cases we may use value receivers; however, remember that value receivers will not allow us to modify the value that its receiver points to.
Conclusion
In this topic, we've learned about methods: a special type of function that requires a defined receiver argument that can be either of a struct or a non-struct type.
We've covered the following theory about methods:
- What methods are, and the most basic implementation of methods onto
structtypes. - How to define methods on non-struct types, as long as the non-struct type and method definitions are contained within the same package.
- Implementing methods with pointer receivers that allow us to modify the value that its receiver points to.
- The key differences between value and pointer receivers, and how to decide when we should use either value or pointer receivers for the methods in our Go program.
Now, to make sure you remember the theory and know how to implement methods to struct and non-struct types in your future Go programs, let's work on a few coding and theory problems.