Learn Go: Defining Methods in Golang #24

In this chapter, we will focus on Gos use of methods

Gopher

Lets go!!

Published · Feb 3 2020

Previous Tutorial: Defining Structs in Golang #23

While technically Go isn’t an Object Oriented Programming language, types and methods allow for an object-oriented style of programming. The big difference is that Go does not support type inheritance but instead has a concept of interface.

. . .

Introduction

Functions allow you to organize logic into repeatable procedures that can use different arguments each time they run.

In the course of defining functions, you’ll often find that multiple functions might operate on the same piece of data each time.

Go recognizes this pattern and allows you to define special functions, called methods, whose purpose is to operate on instances of some specific type, called a receiver.

Adding methods to types allows you to communicate not only what the data is, but also how that data should be used.
. . .

Defining a Method

The syntax for defining a method is similar to the syntax for defining a function. The only difference is the addition of an extra parameter after the func keyword for specifying the receiver of the method.

The receiver is a declaration of the type that you wish to define the method on. The following example defines a method on a struct type:
package main import "fmt" type Creature struct { Name string Greeting string } func (c Creature) Greet() { fmt.Printf("%s says %s", c.Name, c.Greeting) } func main() { sammy := Creature{ Name: "Sammy", Greeting: "Hello!", } sammy.Greet() }


If you run this, the output will be the same as the previous example:
Output Sammy says Hello!

We have used dot notation to invoke the Greet method using the Creature stored in the sammy variable as the receiver.

This is a shorthand notation for the function invocation in the first example. The standard library and the Go community prefers this style so much that you will rarely see the function invocation style shown earlier.
. . .

Methods vs Functions

The above program can be rewritten using only functions and without methods.
package main

import "fmt"

type Creature struct {
Name string
Greeting string
}

func Greet(c Creature) {
fmt.Printf("%s says %s", c.Name, c.Greeting)
}

func main() {
sammy := Creature{
Name: "Sammy",
Greeting: "Hello!",
}
Greet(sammy)
}


In the program above, the Greet method is converted to a function and the Creature struct is passed as a parameter to it. This program also produces the exact same output Sammy says Hello!

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 behavior similar to classes. Methods allow a logical grouping of behavior related to a type similar to classes. In the above sample program, all behaviors related to the Employee type can be grouped by creating methods using Employee receiver type. For example, we can add methods like calculatePension, 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 and Circle structure. It's possible to define a method named Area on both Square and Circle. This is done in the program below.
package main import ( "fmt" "math" ) type Rectangle struct { length int width int } type Circle struct { radius float64 } func (r Rectangle) Area() int { return r.length * r.width } func (c Circle) Area() float64 { return math.Pi * c.radius * c.radius } func main() { r := Rectangle{ length: 10, width: 5, } fmt.Printf("Area of rectangle %d\n", r.Area()) c := Circle{ radius: 12, } fmt.Printf("Area of circle %f", c.Area()) }


This program prints
Area of rectangle 50 Area of circle 452.389342

The above property of methods is used to implement interfaces.
. . .

Pointer 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.
package main import "fmt" type Boat struct { Name string occupants []string } func (b *Boat) AddOccupant(name string) *Boat { b.occupants = append(b.occupants, name) return b } func (b Boat) Manifest() { fmt.Println("The", b.Name, "has the following occupants:") for _, n := range b.occupants { fmt.Println("\t", n) } } func main() { b := &Boat{ Name: "S.S. DigitalOcean", } b.AddOccupant("Sammy the Shark") b.AddOccupant("Larry the Lobster") b.Manifest() }


You’ll see the following output when you run this example:
#Output The S.S. DigitalOcean has the following occupants: Sammy the Shark Larry the Lobster

This example defined a Boat type with a Name and occupants. We want to force code in other packages to only add occupants with the AddOccupant method, so we’ve made the occupants field unexported by lowercasing the first letter of the field name.

We also want to make sure that calling AddOccupant will cause the instance of Boat to be modified, which is why we defined AddOccupant on the pointer receiver. Pointers act as a reference to a specific instance of a type rather than a copy of that type.

Knowing that AddOccupant will be invoked using a pointer to Boat guarantees that any modifications will persist.

Within main, we define a new variable, b, which will hold a pointer to a Boat (*Boat). We invoke the AddOccupant method twice on this instance to add two passengers.

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.
package main import ( "fmt" ) type address struct { city string state string } func (a address) fullAddress() { fmt.Printf("Full address: %s, %s", a.city, a.state) } type person struct { firstName string lastName string address } func main() { p := person{ firstName: "Elon", lastName: "Musk", address: address { city: "Los Angeles", state: "California", }, } p.fullAddress() //accessing fullAddress method of address struct }


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 following:
Full address: Los Angeles, California
. . .

Conclusion

Declaring methods in Go is ultimately no different than defining functions that receive different types of variables.

The same rules of working with pointers apply. Go provides some conveniences for this extremely common function definition and collects these into sets of methods that can be reasoned about by interface types.

Using methods effectively will allow you to work with interfaces in your code to improve testability and leaves better organization behind for future readers of your code.
. . .

. . .
References:

On a mission to build Next-Gen Community Platform for Developers