Technically, Go is not an object-oriented programming language. It doesn’t have classes, objects, and inheritance.

However, Go has types. And, you can define methods on types. This allows for an object-oriented style of programming in Go.

Let’s deep dive and see how?

Go Methods

A method is nothing but a function with a special receiver argument.

The receiver argument has a name and a type. It appears between the func keyword and the method name -

func (receiver Type) MethodName(parameterList) (returnTypes) {
}

The receiver can be either a struct type or a non-struct type.

Let’s see an example to understand how to define a method on a type and how to invoke such a method -

package main

import (
	"fmt"
)

// Struct type - `Point`
type Point struct {
	X, Y float64
}

// Method with receiver `Point`
func (p Point) IsAbove(y float64) bool {
	return p.Y > y
}

func main() {
	p := Point{2.0, 4.0}

	fmt.Println("Point : ", p)

	fmt.Println("Is Point p located above the line y = 1.0 ? : ", p.IsAbove(1))
}
Point :  {2 4}
Is Point p located above the line y = 1.0 ? :  true

Notice the way we called the method IsAbove() on the Point instance p. It’s exactly like the way you call methods in an object-oriented programming language.

Methods are functions

Since a method is just a function with a receiver argument. We can write the above example using a regular function as well -

package main

import (
	"fmt"
)

// Struct type - `Point`
type Point struct {
	X, Y float64
}

func IsAboveFunc(p Point, y float64) bool {
	return p.Y > y
}

/*
Compare the above function with the corresponding method -
func (p Point) IsAbove(y float64) bool {
	return p.Y > y
}
*/

func main() {
	p := Point{2.5, -3.0}

	fmt.Println("Point : ", p)

	fmt.Println("Is Point p located above the line y = 1.0 ? : ", IsAboveFunc(p, 1))
}

Why Methods instead of Functions?

Methods help you write object-oriented style code in Go. Method calls are much easier to read and understand than function calls.

Moreover, Methods help you avoid naming conflicts. Since a method is tied to a particular receiver, you can have the same method names on different receiver types.

Let’s see an example -

package main

import (
	"fmt"
	"math"
)

type ArithmeticProgression struct {
	A, D float64
}

// Method with receiver `ArithmeticProgression`
func (ap ArithmeticProgression) NthTerm(n int) float64 {
	return ap.A + float64(n-1)*ap.D
}

type GeometricProgression struct {
	A, R float64
}

// Method with receiver `GeometricProgression`
func (gp GeometricProgression) NthTerm(n int) float64 {
	return gp.A * math.Pow(gp.R, float64(n-1))
}

func main() {
	ap := ArithmeticProgression{1, 2} // AP: 1 3 5 7 9 ...
	gp := GeometricProgression{1, 2}  // GP: 1 2 4 8 16 ...

	fmt.Println("5th Term of the Arithmetic series = ", ap.NthTerm(5))
	fmt.Println("5th Term of the Geometric series = ", gp.NthTerm(5))
}
5th Term of the Arithmetic series =  9
5th Term of the Geometric series =  16

Methods with Pointer receivers

All the examples that we saw in the previous sections had a value receiver.

With a value receiver, the method operates on a copy of the value passed to it. Therefore, any modifications done to the receiver inside the method is not visible to the caller.

Go also allows you to define a method with a pointer receiver -

// Method with pointer receiver
func (receiver *Type) MethodName(parameterList) (returnTypes) {
}

Methods with pointer receivers can modify the value to which the receiver points. Such modifications are visible to the caller of the method as well -

package main

import (
	"fmt"
)

type Point struct {
	X, Y float64
}

/* 
  Translates the current Point, at location (X,Y), by dx along the x axis and dy along the y axis 
  so that it now represents the point (X+dx,Y+dy).
*/
func (p *Point) Translate(dx, dy float64) {
	p.X = p.X + dx
	p.Y = p.Y + dy
}

func main() {
	p := Point{3, 4}
	fmt.Println("Point p = ", p)

	p.Translate(7, 9)
	fmt.Println("After Translate, p = ", p)
}
Point p =  {3 4}
After Translate, p =  {10 13}

Methods with Pointer receivers as Functions

Just like methods with value receivers, we can also write methods with pointer receivers as functions. The following example shows how to rewrite the previous example with functions -

package main

import (
	"fmt"
)

type Point struct {
	X, Y float64
}

/* 
  Translates the current Point, at location (X,Y), by dx along the x axis and dy along the y axis 
  so that it now represents the point (X+dx,Y+dy).
*/
func TranslateFunc(p *Point, dx, dy float64) {
	p.X = p.X + dx
	p.Y = p.Y + dy
}

func main() {
	p := Point{3, 4}	
	fmt.Println("Point p = ", p)

	TranslateFunc(&p, 7, 9)
	fmt.Println("After Translate, p = ", p)
}

Methods and Pointer indirection

1. Methods with Pointer receivers vs Functions with Pointer arguments

A method with a pointer receiver can accept both a pointer and a value as the receiver argument. But, a function with a pointer argument can only accept a pointer -

package main

import (
	"fmt"
)

type Point struct {
	X, Y float64
}

// Method with Pointer receiver
func (p *Point) Translate(dx, dy float64) {
	p.X = p.X + dx
	p.Y = p.Y + dy
}

// Function with Pointer argument
func TranslateFunc(p *Point, dx, dy float64) {
	p.X = p.X + dx
	p.Y = p.Y + dy
}

func main() {
	p := Point{3, 4}	
	ptr := &p
	fmt.Println("Point p = ", p)

	// Calling a Method with Pointer receiver
	p.Translate(2, 6)		// Valid
	ptr.Translate(5, 10)	// Valid

	// Calling a Function with a Pointer argument
	TranslateFunc(ptr, 20, 30)  // Valid
	TranslateFunc(p, 20, 30)   // Not Valid
}

Notice how both p.Translate() and ptr.Translate() calls are valid. Since Go knows that the method Translate() has a pointer receiver, It interprets the statement p.Translate() as (&p).Translate(). It’s a syntactic sugar provider by Go for convenience.

2. Methods with Value receivers vs Functions with Value arguments

A method with a value receiver can accept both a value and a pointer as the receiver argument. But, a function with a value argument can only accept a value -

package main

import (
	"fmt"
)

// Struct type - `Point`
type Point struct {
	X, Y float64
}

func (p Point) IsAbove(y float64) bool {
	return p.Y > y
}

func IsAboveFunc(p Point, y float64) bool {
	return p.Y > y
}

func main() {
	p := Point{0, 4}
	ptr := &p

	fmt.Println("Point p = ", p)

	// Calling a Method with Value receiver
	p.IsAbove(1)   // Valid
	ptr.IsAbove(1) // Valid

	// Calling a Function with a Value argument
	IsAboveFunc(p, 1)   // Valid
	IsAboveFunc(ptr, 1) // Not Valid
}

In the above example, both p.IsAbove() and ptr.IsAbove() statements are valid. Go knows that the method IsAbove() has a value receiver. So for convenience, It interprets the statement ptr.IsAbove() as (*ptr).IsAbove().

Method definition restrictions

Note that, For you to be able to define a method on a receiver, the receiver type must be defined in the same package.

Go doesn’t allow you to define a method on a receiver type that is defined in some other package (this includes built-in types such as int as well).

In all the previous examples, the structs and the methods were defined in the same package main. Therefore, they worked. However, if you try to define a method on a type defined in some other package, the compiler will complain.

Let’s see an example. Consider the following package hierarchy -

src/example
	main
		main.go
    model
        person.go

Here are the contents of person.go -

package model

type Person struct {
	FirstName string
	LastName string
	Age int
}

Let’s try to define a method on the struct Person -

package main

import (
	"example/model"
)

// ERROR: cannot define new methods on non-local types model.Person
func (p model.Person) getFullName() string {
	return p.FirstName + " " + p.LastName
}

func main() {	
}

Defining Methods on non-struct types

Go allows you to define methods on non-struct types too. In the following example, I’ve defined a method called reverse() on the type MyString.

package main

import (
	"fmt"
)

type MyString string

func (myStr MyString) reverse() string {
	s := string(myStr)
	runes := []rune(s)

	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}
	return string(runes)
}

func main() {
	myStr := MyString("OLLEH")
	fmt.Println(myStr.reverse())
}
# Output
HELLO

Conclusion

That’s all in this article folks! I hope you enjoyed the article. Thanks for reading. See you in the next post.

Code Samples: github.com/callicoder/golang-tutorials