Anatomy of methods in Go

Go does not support the Object-Oriented paradigm but structure resembles the class architecture. To add methods to a structure, we need to use functions with a receiver.

Even Go does not provide classes, we can use structures to create objects as we have learned in the previous tutorial. But in OOP, classes have properties (fields) as well as behaviors (methods) and so far we have only learned about properties of a struct that are structure fields.

💡 Behavior is a action that an object can perform. For example, Dog is a type of Animal and Dog can bark. Hence barking is a behavior of the class Dog. Hence any objects (instances) of the class Dog will have this behavior.

We have seen in the structures lesson, especially in the function field section that a struct field can also be a function. We can add a bark field of type function which takes no arguments and returns a string woof woof!. This could be one way to add methods to the struct.

But this does not adhere to the OOP concept as struct fields do not have any idea of struct they belong to. Hence methods come to the rescue.


What is a method?

In the previous tutorial, we played with function fields of a struct, hence the concept of method will be very easy for you to understand.

A method is nothing but a function, but it belongs to a certain type. A method is defined with slightly different syntax than a normal function. It required an additional parameter known as a receiver which is a type to which the function belongs. This way, a method (function) can access the properties of the receiver it belongs to (like fields of a struct).

Let’s write a program to get the full name of an Employee struct using a simple function.

package main

import "fmt"

type Employee struct {
	FirstName, LastName string
}

func fullName(firstName string, lastName string) (fullName string) {
	fullName = firstName + " " + lastName
	return
}

func main() {
	e := Employee{
		FirstName: "Ross",
		LastName:  "Geller",
	}

	fmt.Println(fullName(e.FirstName, e.LastName))
}

// Ross Geller

In the above program, we have created a simple struct type Employee which has two string fields FirstName and LastName. Then we’ve defined the function fullName which takes two strings arguments and returns a string. The fullname function returns the full name of an employee by concatenating these two strings.

Then we created a struct e of type Empoyee by providing FirstName and LastName fields values. To get the full name of the employee e, we use the fullName function and provided the appropriate arguments.

This works, but the sad thing is, every time we need to get the full name of an employee (and there could be thousands), we need to pass firstName and lastName values to fullName function manually.

A method can solve this problem easily. To convert a function to the method, we just need an extra receiver parameter in the function definition. The syntax for defining a method is as follows.

func (r Type) functionName(...Type) Type {
    ...
}

From the above syntax, we can tell that method and function have the same syntax except for one receiver argument declaration (r Type) just before the function name. Type is any legal type in Go and function arguments and return values are optional (as usual).

Let’s create fullName method using the above syntax.

package main

import "fmt"

type Employee struct {
	FirstName, LastName string
}

func (e Employee) fullName() string {
	return e.FirstName + " " + e.LastName
}

func main() {
	e := Employee{
		FirstName: "Ross",
		LastName:  "Geller",
	}
	fmt.Println(e.fullName())
}

// Ross Geller

In the above program, we have defined fullName method which does not take any arguments but returns a string. As we can tell from the receiver declaration, this method belongs to Employee type.

The fullName method will belong to any object of the type Employee. Hence that object will automatically get this method as a property. When this method is called on the object (just like a struct field function seen in the previous tutorial), it will receive the object as the receiver e.

The receiver of the method is accessible inside the method body. Hence we can access e inside the method body of fullName. In the above example, since the receiver is a struct of type Employee, we can access any fields of the struct. Like we did in the previous example, we are concatenating FirstName and LastName fields and returning the result.

As a method belongs to a receiver type and it becomes available on that type as a property, we can call that method using Type.methodName(...)syntax. In the above program, we have used e.fullName() to get the full name of an employee since fullName method belongs to Employee.

💡 This is no different than what we learned in structs lesson where fullName function was a field of struct. But in case of methods, we don’t have to provide properties of struct because method already knows about them.

Methods with the same name

One major difference between functions and methods is we can have multiple methods with same name while no two functions with the same name can be defined in a package.

We are allowed to create methods with same name as long as their receivers are different. Let’s create two struct types Circle and Rectangle and create two methods of the same name Area which calculates the area of their receiver.

package main

import (
	"fmt"
	"math"
)

type Rect struct {
	width  float64
	height float64
}

type Circle struct {
	radius float64
}

func (r Rect) Area() float64 {
	return r.width * r.height
}

func (c Circle) Area() float64 {
	return math.Pi * c.radius * c.radius
}

func main() {
	rect := Rect{5.0, 4.0}
	cir := Circle{5.0}
	fmt.Printf("Area of rectangle rect = %0.2f\n", rect.Area())
	fmt.Printf("Area of circle cir = %0.2f\n", cir.Area())
}

// Area of rectangle rect = 20.00
// Area of circle cir = 78.54

In the above program, we have created struct types Rect and Circle and created two methods of the same name Area with receiver type Rect and Circle. When we call Area() method on Rect and Circle, their respective methods get executed.

Pointer receivers

So far, we have seen methods belong to a type. But a method can also belong to the pointer of a type.

When a method belongs to a type, its receiver receives a copy of the object on which it was called. To verify that, we can create a method that mutates a struct it receives. Let’s create a method changeName that changes the name field of an Employee struct.

package main

import "fmt"

type Employee struct {
	name   string
	salary int
}

func (e Employee) changeName(newName string) {
	e.name = newName
}

func main() {
	e := Employee{
		name:   "Ross Geller",
		salary: 1200,
	}

	// e before name change
	fmt.Println("e before name change =", e)

	// change name
	e.changeName("Monica Geller")

	// e after name change
	fmt.Println("e after name change =", e)
}

// e before name change = {Ross Geller 1200}
// e after name change = {Ross Geller 1200}

In the above program, we have called method changeName on struct e of type Employee. In the method, we are mutating the name field value of that struct.

From the above result, we can verify that even we have mutated the receiver object, it did not impact the original object on which the method was called.

This proves that the method changeName received just a copy of the actual struct e(from main method). Hence any changes made to the copy inside the method did not affect the original struct.

But a method can also belong to the pointer of a type. The syntax for method definition which belongs to the pointer of a type is as follows.

func (r *Type) functionName(...Type) Type {
    ...
}

As you can see from the above definition, the syntax to define a method with a pointer receiver is very similar to the normal method. In the below definition, we instructed Go that this method will belong to the pointer of the Type instead of the value of the Type.

When a method belongs to the pointer of a type, its receiver will receive the pointer to the object instead of a copy of the object. Let’s re-write the previous example with a method that receives a pointer receiver.

package main

import "fmt"

type Employee struct {
	name   string
	salary int
}

func (e *Employee) changeName(newName string) {
	(*e).name = newName
}

func main() {
	e := Employee{
		name:   "Ross Geller",
		salary: 1200,
	}

	// e before name change
	fmt.Println("e before name change =", e)
	// create pointer to `e`
	ep := &e
	// change name
	ep.changeName("Monica Geller")
	// e after name change
	fmt.Println("e after name change =", e)
}

// e before name change = {Ross Geller 1200}
// e after name change = {Monica Geller 1200}

Let’s see what changes we made.

  • We changed the definition of the method to receive a pointer receiver using *Employee syntax. This way, the receiver e is the pointer to the struct object this method was called on.
  • Inside the method body, we are converting pointer of the receiver to the value of the receiver using pointer dereferencing syntax (*p). Hence (*e) will be the actual value of the struct stored in the memory.
  • Then we changed the value of the name field of struct e. Any change made to e will be reflected in the original struct.
  • In the main method, we created a pointer ep which points to struct e.
  • Since the changeName method belongs to the pointer of type Employee or type *Employee, it can be called on value of type *Employee.
  • Since the type of ep is *Employee, we can call changeName method on it using ep.changeName() syntax. This will pass the pointer ep to the method as a receiver (instead of value e).

💡 In above program, we solely created the pointer ep from e just to call method the changeName on it, but you can also use (&e).changeName("Monica Geller")syntax instead of creating new pointer.

Calling methods with pointer receiver on values

If you are wondering, do I always need to create a pointer to work with methods with pointer receiver then Go already figured that out.

Let’s rewrite the above programming using Go’s shortcuts.

package main

import "fmt"

type Employee struct {
	name   string
	salary int
}

func (e *Employee) changeName(newName string) {
	e.name = newName
}

func main() {
	e := Employee{
		name:   "Ross Geller",
		salary: 1200,
	}

	// e before name change
	fmt.Println("e before name change =", e)
	// change name
	e.changeName("Monica Geller")
	// e after name change
	fmt.Println("e after name change =", e)
}

// e before name change = {Ross Geller 1200}
// e after name change = {Monica Geller 1200}

The above program will work just fine like before. So what changed.

  • If a method has a pointer receiver, then you don’t necessarily need to use the pointer dereferencing syntax (*e) to get the value of the receiver. You can use simple e which will be the address of the value that pointer points to but Go will understand that you are trying to perform an operation on the value itself and under the hood, it will convert e to (*e).
  • Also, you don’t necessarily need to call a method on a pointer if the method has a pointer receiver. You are allowed to call this method on the value instead and Go will pass the pointer of the value as the receiver.

💡 You can decide between method with pointer receiver or value receiver depending on your use case. But generally, even if you do not wish to mutate the receiver, methods with pointer receiver are preffered as no new memory is created for operations (in case of methods with value receiver).

Methods on nested struct

We learned a great deal about the nested structure in the previous lesson. As a struct field also can be a struct, we can define a method on parent struct and access nested struct to do anything we want.

package main

import "fmt"

type Contact struct {
	phone, address string
}
type Employee struct {
	name    string
	salary  int
	contact Contact
}

func (e *Employee) changePhone(newPhone string) {
	e.contact.phone = newPhone
}

func main() {
	e := Employee{
		name:    "Ross Geller",
		salary:  1200,
		contact: Contact{"011 8080 8080", "New Delhi, India"},
	}
	// e before phone change
	fmt.Println("e before phone change =", e)
	// change phone
	e.changePhone("011 1010 1222")
	// e after phone change
	fmt.Println("e after phone change =", e)
}

// e before phone change = {Ross Geller 1200 {011 8080 8080 New Delhi, India}}
// e after phone change = {Ross Geller 1200 {011 1010 1222 New Delhi, India}}

In the above example, we have defined changePhone method on *Employee which receive the pointer of e. Inside this method, we can access the properties of e which also contains the nested struct of type Contact.

Since e is the pointer in the method, we can mutate the nested struct. In the above example, we have changed the contact nested struct by mutating the phone field value.

Methods on nested struct

A nested struct can also have methods. If the inner struct implements a method, then you can call a method on it using . (dot) accessor.

package main

import "fmt"

type Contact struct {
	phone, address string
}
type Employee struct {
	name    string
	salary  int
	contact Contact
}

func (c *Contact) changePhone(newPhone string) {
	c.phone = newPhone
}

func main() {
	e := Employee{
		name:   "Ross Geller",
		salary: 1200,
		contact: Contact{
			phone:   "011 8080 8080",
			address: "New Delhi, India",
		},
	}
	// e before phone change
	fmt.Println("e before phone change =", e)
	// change phone
	e.contact.changePhone("011 1010 1222")
	// e after phone change
	fmt.Println("e after phone change =", e)
}

// e before phone change = {Ross Geller 1200 {011 8080 8080 New Delhi, India}}
// e after phone change = {Ross Geller 1200 {011 1010 1222 New Delhi, India}}

Anonymously nested struct

In the previous lesson, we also learned about anonymous fields and field promotions. In a nutshell, if a field of a struct an anonymous struct, the nested struct fields will be promoted to the parent.

Let’s see how we can use the promoted fields inside a method.

package main

import "fmt"

type Contact struct {
	phone, address string
}
type Employee struct {
	name   string
	salary int
	Contact
}

func (e *Employee) changePhone(newPhone string) {
	e.phone = newPhone
}

func main() {
	e := Employee{
		name:   "Ross Geller",
		salary: 1200,
		Contact: Contact{
			phone:   "011 8080 8080",
			address: "New Delhi, India",
		},
	}
	// e before phone change
	fmt.Println("e before phone change =", e)
	// change phone
	e.changePhone("011 1010 1222")
	// e after phone change
	fmt.Println("e after phone change =", e)
}

// e before phone change = {Ross Geller 1200 {011 8080 8080 New Delhi, India}}
// e after phone change = {Ross Geller 1200 {011 1010 1222 New Delhi, India}}

As we can see from the above example, since Contact struct is anonymously nested inside Employee struct, its fields will be promoted to Employee and we will be able to access it on the object e.

Hence any method that accepts a struct receiver will also have access to the promoted fields. Using this principle, we were able to access phone property of the nested field Contact on the object e of type Employee.

Like promoted fields, methods implemented by the anonymously nested struct are also promoted to the parent struct. As we saw in the previous example, Contact field is anonymously nested. Hence we could access phone field of the inner struct on the parent.

In the same scenario, any method implemented by Contact struct will be available on Employee struct. Let’s rewrite the previous example.

package main

import "fmt"

type Contact struct {
	phone, address string
}

type Employee struct {
	name   string
	salary int
	Contact
}

func (c *Contact) changePhone(newPhone string) {
	c.phone = newPhone
}

func main() {
	e := Employee{
		name:   "Ross Geller",
		salary: 1200,
		Contact: Contact{
			phone:   "011 8080 8080",
			address: "New Delhi, India",
		},
	}
	// e before phone change
	fmt.Println("e before phone change =", e)
	// change phone
	e.changePhone("011 1010 1222")
	// e after phone change
	fmt.Println("e after phone change =", e)
}

// e before phone change = {Ross Geller 1200 {011 8080 8080 New Delhi, India}}
// e after phone change = {Ross Geller 1200 {011 1010 1222 New Delhi, India}}

We made only one change to changePhone method. Instead of receiving *Employee type, this method now expects receiver of type *Contact. Since fields of the nested struct Contact are promoted, any method implemented by it will be promoted too. Hence we could call e.changePhone() as if the type Employee of struct e implemented changePhone method.

💡 However, one thing to remeber here that even we are calling changePhone() method on e, the receiver sent by the Go will be of type *Contact since this method belongs to it.

Methods can accept both pointer and value

When a normal function has a parameter definition, it will only accept the argument of the type defined by the parameter. If you passed a pointer to the function which expects a value, it will not work. This is also true when function accepts pointer but you are passing a value instead.

💡 You should look at this from the perspective of data type. A function which accepts a value of type Type has func (arg Type) parameter definition while a function that accepts a pointer has func (arg *Type) definition.

But when it comes to methods, that’s not a strict rule. We can define a method with value or pointer receiver and call it on pointer or value. Go does the job of type conversion behind the scenes as we’ve seen in the earlier examples.

package main

import "fmt"

type Employee struct {
	name   string
	salary int
}

func (e *Employee) changeName(newName string) {
	e.name = newName
}

func (e Employee) showSalary() {
	e.salary = 1500
	fmt.Println("Salary of e =", e.salary)
}

func main() {
	e := Employee{
		name:   "Ross Geller",
		salary: 1200,
	}
	// e before change
	fmt.Println("e before change =", e)
	// calling `changeName` pointer method on value
	e.changeName("Monica Geller")
	// calling `showSalary` value method on pointer
	(&e).showSalary()
	// e after change
	fmt.Println("e after change =", e)
}

// e before change = {Ross Geller 1200}
// Salary of e = 1500
// e after change = {Monica Geller 1200}

In the above program, we defined changeName method which receives a pointer but we called on the value e which is legal because Go under the hood will pass a pointer of e (of type Employee) to it.

Also, we defined showSalary method which receives value but we called it on the pointer to e which is legal because Go under the hood will pass the value of the pointer (of type Employee) to it.

💡 We tried to change salary of e inside showSalary method but did not work as we can see from the result. This is because even we’re calling this method on a pointer, Go will sent only copy of the value to that method.

Methods on non-struct type

So far we have seen methods belonging to struct type but from the definition of the methods, it is a function that can belong to any type. Hence a method can receive any type as long as the type definition and method definition is in the same package.

So far, we defined struct and method in the same main package, hence it worked. But to verify if we can add methods on foreign types, we will try to add a method toUpperCase on the built-in type string.

package main

import (
	"fmt"
	"strings"
)

func (s string) toUpperCase() string {
	return strings.ToUpper(s)
}

func main() {
	str := "Hello World"
	fmt.Println(str.toUpperCase())
}

// ./prog.go:8:7: cannot define new methods on non-local type string
// ./prog.go:14:18: str.toUpperCase undefined (type string has no field or method toUpperCase)

From the above program, we created the method toUpperCase which accepts string as receiver type. Hence we expect string.toUpperCase() to work and return uppercase version of the receiver s.

💡 We used strings build-in package to convert a string to uppercase.

But the above program will run into a compilation error.

program.go:8: cannot define new methods on non-local type string
program.go:14: str.toUpperCase undefined (type string has no field or method toUpperCase)

This is because of the type string and method toUpperCase are not defined in the same package. Let’s create a new derived type MyString from string. This way, both the method and a newly defined MyString type will belong to the same package and it should work.

package main

import (
	"fmt"
	"strings"
)

type MyString string

func (s MyString) toUpperCase() string {
	normalString := string(s)
	return strings.ToUpper(normalString)
}

func main() {
	str := MyString("Hello World")
	fmt.Println(str.toUpperCase())
}

// HELLO WORLD

From the above program, we created the method toUpperCase which now belongs to MyString type. We needed to modify the internals of this method to pass string type to strings.ToUpper function, but we get it.

Now we can call str.toUpperCase() because str is of type MyString as we used type conversion on the line no. 16 to convert from string type to MyString type.

#golang #methods