Methods

There are no classes in Go. But, we can define methods on types.

A method is a function with special receiver argument. The receiver appears in its own argument list between the func keyword and the method name.

Example (Method)

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Dist() float64 {
    return math.Sqrt(v.X * v.X + v.Y * v.Y)
}

func main() {
    v := Vertex{5, 12}
    fmt.Println(v.Dist())
}
13

Note that methods are still functions, just with a receiver argument.

Example (Regular Function)

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func Dist(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{5, 12}
	// written in regular function form
	// pass in v Vertex as paramter
	// same functionality
	fmt.Println(Dist(v))
}
13

Methods on Non-Struct Types

We can declare a method on non-struct types, too.

In this example we see a numeric type MyFloat with an Abs method.

You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int).

package main

import (
	"fmt"
	"math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}
1.4142135623730951

Here we see a numeric type MyFloat with an Abs method.

Pointer Receivers

We can declare methods with pointer receivers. The receiver type has *T for some type T (Note that T cannot be a pointer itself, say *int)

Example 1 (Pointer Receiver)

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}
50

Methods with pointer receivers can modify the value to which the receiver points (as Scale does here). Since methods often need to modify their receiver, pointer receivers are more common than value receivers.

Example 2 (Value Receiver)

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}
5

We only removed the * from the function declaration of scale. Notice we do not need to change v.Scale(10) on line 23 to (&v).Scale(10). Keep reading :)

Now, with a value receiver, the original Vertex value is not changed. This is because the Scale method operates on a copy of the original Vertex value. (This is the same behavior as for any other function argument.) The Scale method must have a pointer receiver to change the Vertex value declared in the main function.

Example 3 (Function Version)

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	Scale(&v, 10)
	fmt.Println(Abs(v))
}
50

Here are the Abs and Scale methods rewritten as functions. If we change this to passing by value (instead of reference) by just removing the * from line 16, the code would not compile:

./prog.go:23:8: cannot use &v (type *Vertex) as type Vertex in argument to Scale

We also need to change &v to v. And the output is:

5

Methods and Pointer Indirection 1

The previous examples lead us to some differences the pointer and value receivers in actual code writing.

Example 4

package main

import "fmt"

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {			// Method
	v.X = v.X * f
	v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {	// Function
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(2)					// {6, 8}
	ScaleFunc(&v, 10)		// {60, 80}

	p := &Vertex{4, 3}
	p.Scale(3)					// {12, 9}
	ScaleFunc(p, 8)			// {96, 72}

	fmt.Println(v, p)
}
{60 80} &{96 72}

In this example, for the function version, the function with a pointer argument must take a pointer.

var v Vertex
ScaleFunc(v, 5)    // Compile error as we saw in Example 3
ScaleFunc(&v, 5)   // OK

However, for the method version, the method with pointer receivers take either a value of a pointer as the receiver when they are called:

var v Vertex
v.Scale(5)    // OK

p := &v
p.Scale(5)    // OK

For the statement v.Scale(5), even though v is a value and not a pointer, the method with the pointer receiver is called automatically. That is, as a convenience, Go interprets the statement v.Scale(5) as (&v).Scale(5) since the Scale method has a pointer receiver. (&v).Scale(5) still works certainly!

Methods and Pointer Indirection 2

Conversely, we have the same thing.

Example

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
	fmt.Println(AbsFunc(v))

	p := &Vertex{4, 3}
	fmt.Println(p.Abs())
	fmt.Println(AbsFunc(*p))
}
5
5
5
5

For functions that take a value argument, they must take a value of that specific type.

var v Vertex
fmt.Println(AbsFunc(v))    // OK
fmt.Println(AbsFunc(&v))   // Compile error

For methods with value receivers, they take either a value or a pointer as the receiver when they are called:

var v Vertex
fmt.Println(v.Abs())    // OK
p := &v
fmt.Println(p.Abs())    // OK

Similarly, p.Abs() is interpreted as (*p).Abs() by Go.

Choosing a Value or Pointer Receiver

Two main reasons:

  • To modify the value that its receiver points to

  • To avoid copying the values on each method call. For example, if the receiver is a large struct, this can be efficient.

In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both. (introduced later)

Last updated