Methods in Go
Welcome to tutorial no. 17 in Golang tutorial series.
Introduction
A method is just a function with a special receiver type between the func
keyword and the method name. The receiver can either be a struct type or non-struct type.
The syntax of a method declaration is provided below.
func (t Type) methodName(parameter list) {
}
The above snippet creates a method named methodName
with receiver type Type
. t
is called as the receiver and it can be accessed within the method.
Sample Method
Let’s write a simple program that creates a method on a struct type and calls it.
1package main
2
3import (
4 "fmt"
5)
6
7type Employee struct {
8 name string
9 salary int
10 currency string
11}
12
13/*
14 displaySalary() method has Employee as the receiver type
15*/
16func (e Employee) displaySalary() {
17 fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
18}
19
20func main() {
21 emp1 := Employee {
22 name: "Sam Adolf",
23 salary: 5000,
24 currency: "$",
25 }
26 emp1.displaySalary() //Calling displaySalary() method of Employee type
27}
In line no. 16 of the program above, we have created a method displaySalary
on Employee
struct type. The displaySalary()
method has access to the receiver e
inside it. In line no. 17, we are using the receiver e
and printing the name, currency and salary of the employee.
In line no. 26 we have called the method using syntax emp1.displaySalary()
.
This program prints Salary of Sam Adolf is $5000
.
Methods vs Functions
The above program can be rewritten using only functions and without methods.
1package main
2
3import (
4 "fmt"
5)
6
7type Employee struct {
8 name string
9 salary int
10 currency string
11}
12
13/*
14 displaySalary() method converted to function with Employee as parameter
15*/
16func displaySalary(e Employee) {
17 fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
18}
19
20func main() {
21 emp1 := Employee{
22 name: "Sam Adolf",
23 salary: 5000,
24 currency: "$",
25 }
26 displaySalary(emp1)
27}
In the program above, the displaySalary
method is converted to a function and the Employee
struct is passed as a parameter to it. This program also produces the exact same output Salary of Sam Adolf is $5000
.
So why do we have methods when we can write the same program using functions. There are a couple of reasons for this. Let’s look at them one by one.
-
Go is not a pure object-oriented programming language and it does not support classes. Hence methods on types are a way to achieve behaviour similar to classes. Methods allow a logical grouping of behaviour related to a type similar to classes. In the above sample program, all behaviours related to the
Employee
type can be grouped by creating methods usingEmployee
receiver type. For example, we can add methods likecalculatePension
,calculateLeaves
and so on. -
Methods with the same name can be defined on different types whereas functions with the same names are not allowed. Let’s assume that we have a
Square
andCircle
structure. It’s possible to define a method namedArea
on bothSquare
andCircle
. This is done in the program below.
1package main
2
3import (
4 "fmt"
5 "math"
6)
7
8type Rectangle struct {
9 length int
10 width int
11}
12
13type Circle struct {
14 radius float64
15}
16
17func (r Rectangle) Area() int {
18 return r.length * r.width
19}
20
21func (c Circle) Area() float64 {
22 return math.Pi * c.radius * c.radius
23}
24
25func main() {
26 r := Rectangle{
27 length: 10,
28 width: 5,
29 }
30 fmt.Printf("Area of rectangle %d\n", r.Area())
31 c := Circle{
32 radius: 12,
33 }
34 fmt.Printf("Area of circle %f", c.Area())
35}
This program prints
Area of rectangle 50
Area of circle 452.389342
The above property of methods is used to implement interfaces. We will discuss this in detail in the next tutorial when we deal with interfaces.
Pointer Receivers vs Value Receivers
So far we have seen methods only with value receivers. It is possible to create methods with pointer receivers. The difference between value and pointer receiver is, changes made inside a method with a pointer receiver is visible to the caller whereas this is not the case in value receiver. Let’s understand this with the help of a program.
1package main
2
3import (
4 "fmt"
5)
6
7type Employee struct {
8 name string
9 age int
10}
11
12/*
13Method with value receiver
14*/
15func (e Employee) changeName(newName string) {
16 e.name = newName
17}
18
19/*
20Method with pointer receiver
21*/
22func (e *Employee) changeAge(newAge int) {
23 e.age = newAge
24}
25
26func main() {
27 e := Employee{
28 name: "Mark Andrew",
29 age: 50,
30 }
31 fmt.Printf("Employee name before change: %s", e.name)
32 e.changeName("Michael Andrew")
33 fmt.Printf("\nEmployee name after change: %s", e.name)
34
35 fmt.Printf("\n\nEmployee age before change: %d", e.age)
36 (&e).changeAge(51)
37 fmt.Printf("\nEmployee age after change: %d", e.age)
38}
In the program above, the changeName
method has a value receiver (e Employee)
whereas the changeAge
method has a pointer receiver (e *Employee)
. Changes made to Employee
struct’s name
field inside changeName
will not be visible to the caller and hence the program prints the same name before and after the method e.changeName("Michael Andrew")
is called in line no. 32. Since changeAge
method has a pointer receiver (e *Employee)
, changes made to age
field after the method call (&e).changeAge(51)
will be visible to the caller. This program prints,
Employee name before change: Mark Andrew
Employee name after change: Mark Andrew
Employee age before change: 50
Employee age after change: 51
In line no. 36 of the program above, we use (&e).changeAge(51)
to call the changeAge
method. Since changeAge
has a pointer receiver, we have used (&e)
to call the method. This is not needed and the language gives us the option to just use e.changeAge(51)
. e.changeAge(51)
will be interpreted as (&e).changeAge(51)
by the language.
The following program is rewritten to use e.changeAge(51)
instead of (&e).changeAge(51)
and it prints the same output.
1package main
2
3import (
4 "fmt"
5)
6
7type Employee struct {
8 name string
9 age int
10}
11
12/*
13Method with value receiver
14*/
15func (e Employee) changeName(newName string) {
16 e.name = newName
17}
18
19/*
20Method with pointer receiver
21*/
22func (e *Employee) changeAge(newAge int) {
23 e.age = newAge
24}
25
26func main() {
27 e := Employee{
28 name: "Mark Andrew",
29 age: 50,
30 }
31 fmt.Printf("Employee name before change: %s", e.name)
32 e.changeName("Michael Andrew")
33 fmt.Printf("\nEmployee name after change: %s", e.name)
34
35 fmt.Printf("\n\nEmployee age before change: %d", e.age)
36 e.changeAge(51)
37 fmt.Printf("\nEmployee age after change: %d", e.age)
38}
When to use pointer receiver and when to use value receiver
Generally, pointer receivers can be used when changes made to the receiver inside the method should be visible to the caller.
Pointers receivers can also be used in places where it’s expensive to copy a data structure. Consider a struct that has many fields. Using this struct as a value receiver in a method will need the entire struct to be copied which will be expensive. In this case, if a pointer receiver is used, the struct will not be copied and only a pointer to it will be used in the method.
In all other situations, value receivers can be used.
Methods of anonymous struct fields
Methods belonging to anonymous fields of a struct can be called as if they belong to the structure where the anonymous field is defined.
1package main
2
3import (
4 "fmt"
5)
6
7type address struct {
8 city string
9 state string
10}
11
12func (a address) fullAddress() {
13 fmt.Printf("Full address: %s, %s", a.city, a.state)
14}
15
16type person struct {
17 firstName string
18 lastName string
19 address
20}
21
22func main() {
23 p := person{
24 firstName: "Elon",
25 lastName: "Musk",
26 address: address {
27 city: "Los Angeles",
28 state: "California",
29 },
30 }
31
32 p.fullAddress() //accessing fullAddress method of address struct
33
34}
In line no. 32 of the program above, we call the fullAddress()
method of the address
struct using p.fullAddress()
. The explicit direction p.address.fullAddress()
is not needed. This program prints
Full address: Los Angeles, California
Value receivers in methods vs Value arguments in functions
This topic trips most go newbies. I will try to make it as clear as possible 😀.
When a function has a value argument, it will accept only a value argument.
When a method has a value receiver, it will accept both pointer and value receivers.
Let’s understand this by means of an example.
1package main
2
3import (
4 "fmt"
5)
6
7type rectangle struct {
8 length int
9 width int
10}
11
12func area(r rectangle) {
13 fmt.Printf("Area Function result: %d\n", (r.length * r.width))
14}
15
16func (r rectangle) area() {
17 fmt.Printf("Area Method result: %d\n", (r.length * r.width))
18}
19
20func main() {
21 r := rectangle{
22 length: 10,
23 width: 5,
24 }
25 area(r)
26 r.area()
27
28 p := &r
29 /*
30 compilation error, cannot use p (type *rectangle) as type rectangle
31 in argument to area
32 */
33 //area(p)
34
35 p.area()//calling value receiver with a pointer
36}
function func area(r rectangle)
in line no.12 accepts a value argument and method func (r rectangle) area()
in line no. 16 accepts a value receiver.
In line no. 25, we call the area function with a value argument area(r)
and it will work. Similarly, we call the area method r.area()
using a value receiver and this will work too.
We create a pointer p
to r
in line no. 28. If we try to pass this pointer to the function area which accepts only a value, the compiler will complain. I have commented line no. 33 which does this. If you uncomment this line, then the compiler will throw error *compilation error, cannot use p (type rectangle) as type rectangle in argument to area. This works as expected.
Now comes the tricky part, line no. 35 of the code p.area()
calls the method area
which accepts only a value receiver using the pointer receiver p
. This is perfectly valid. The reason is that the line p.area()
, for convenience will be interpreted by Go as (*p).area()
since area
has a value receiver.
This program will output,
Area Function result: 50
Area Method result: 50
Area Method result: 50
Pointer receivers in methods vs Pointer arguments in functions
Similar to value arguments, functions with pointer arguments will accept only pointers whereas methods with pointer receivers will accept both pointer and value receiver.
1package main
2
3import (
4 "fmt"
5)
6
7type rectangle struct {
8 length int
9 width int
10}
11
12func perimeter(r *rectangle) {
13 fmt.Println("perimeter function output:", 2*(r.length+r.width))
14
15}
16
17func (r *rectangle) perimeter() {
18 fmt.Println("perimeter method output:", 2*(r.length+r.width))
19}
20
21func main() {
22 r := rectangle{
23 length: 10,
24 width: 5,
25 }
26 p := &r //pointer to r
27 perimeter(p)
28 p.perimeter()
29
30 /*
31 cannot use r (type rectangle) as type *rectangle in argument to perimeter
32 */
33 //perimeter(r)
34
35 r.perimeter()//calling pointer receiver with a value
36
37}
Line no. 12 of the above program defines a function perimeter
which accepts a pointer argument and line no. 17 defines a method that has a pointer receiver.
In line no. 27 we call the perimeter function with a pointer argument and in line no.28 we call the perimeter method on a pointer receiver. All is good.
In the commented line no.33, we try to call the perimeter function with a value argument r
. This is not allowed since a function with a pointer argument will not accept value arguments. If this line is uncommented and the program is run, the compilation will fail with error *main.go:33: cannot use r (type rectangle) as type rectangle in argument to perimeter.
In line no.35 we call the pointer receiver method perimeter
with a value receiver r
. This is allowed and the line of code r.perimeter()
will be interpreted by the language as (&r).perimeter()
for convenience. This program will output,
perimeter function output: 30
perimeter method output: 30
perimeter method output: 30
Methods with non-struct receivers
So far we have defined methods only on struct types. It is also possible to define methods on non-struct types, but there is a catch. To define a method on a type, the definition of the receiver type and the definition of the method should be present in the same package. So far, all the structs and the methods on structs we defined were all located in the same main
package and hence they worked.
1package main
2
3func (a int) add(b int) {
4}
5
6func main() {
7
8}
In the program above, in line no. 3 we are trying to add a method named add
on the built-in type int
. This is not allowed since the definition of the method add
and the definition of type int
is not in the same package. This program will throw compilation error cannot define new methods on non-local type int
The way to get this working is to create a type alias for the built-in type int
and then create a method with this type alias as the receiver.
1package main
2
3import "fmt"
4
5type myInt int
6
7func (a myInt) add(b myInt) myInt {
8 return a + b
9}
10
11func main() {
12 num1 := myInt(5)
13 num2 := myInt(10)
14 sum := num1.add(num2)
15 fmt.Println("Sum is", sum)
16}
In line no.5 of the above program, we have created a type alias myInt
for int
. In line no. 7, we have defined a method add
with myInt
as the receiver.
This program will print Sum is 15
.
I have created a program with all the concepts we discussed so far and it’s available in github.
That’s it for methods in Go. I hope you liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter and LinkedIn. Have a good day.
Next tutorial - Interfaces - I