Byte Ebi's Logo

Byte Ebi 🍀

A Bit everyday A Byte every week

[A Tour of Go Study Notes] 07 method

Understanding Go method using official tutorials

Ray

Entering the world of Golang, we first acquaint ourselves with basic Golang usage using the official tutorial: A Tour of Go .
This article introduces methods in Go.

Method

Go is not strictly object-oriented and doesn’t have classes.
However, it can achieve similar functionality using Type in conjunction with a receiver parameter.
Methods are functions with a special receiver parameter.

The receiver appears in its own parameter list between the func declaration and the method name.
In the example, the Abs function has a receiver of type Vertex named v.

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 main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

/*
>>> 5
*/

Methods Are Functions

A method is just a function with a receiver.

Below is a regular function with no significant change in functionality:

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 main() {
	v := Vertex{3, 4}
	fmt.Println(Abs(v))
}

/*
>>> 5
*/

Method declarations can also use non-struct types.

Only types declared within the same package can be used as receivers.
Even built-in types like int are disallowed if declared in another package.

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
*/

Pointer Receivers

You can also declare methods with pointer receivers.
Adding * before the type indicates a pointer receiver, where the receiver type of the method is a pointer.

In the example, the function Scale has a receiver declared as *Vertex.

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 directly modify the value they point to, not just perform operations.
Because methods often modify the receiver, pointers are commonly used as receivers.

If you remove the * from the Scale receiver in the above example, turning it into a regular method, it would make a copy of the Vertex value and operate on it.

If executed, the result would be 5, as v.Scale(10) wouldn’t affect the result obtained from v := Vertex{3, 4} directly printed as 5.

Pointers and Functions

Now, let’s rewrite Abs and Scale methods as functions.

Previously, v.Scale(10) could directly call a pointer receiver, but function parameters must use the & symbol to declare a pointer.

package main

import "fmt"

type Vertex struct {
	X, Y float64
}

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

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

func main() {
	v := Vertex{3, 4}
	v.Scale(2)
	ScaleFunc(&v, 10)

	p := &Vertex{4, 3}
	p.Scale(3)
	ScaleFunc(p, 8)

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

Comparing with the previous two examples, you’ll notice that functions with parameters must receive a pointer, or else:

var v Vertex
ScaleFunc(v, 5)  // Compile error!
ScaleFunc(&v, 5) // OK

But methods with a pointer receiver can be directly called with a value or pointer:

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

For v.Scale(5), even though v is a value rather than a pointer, a method with a pointer receiver can still be called.
Essentially, Go interprets v.Scale(5) as (&v).Scale(5) for convenience.

The same happens in reverse; if a function with parameters expects a value, and a pointer is passed:

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

But when used as a method, both values and pointers can be passed directly:

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

Because p.Abs() is interpreted as (*p).Abs() in this case.

Choosing a Receiver Type (Value/Pointer)

Typically, using a “pointer” as a receiver is chosen because:

  • Methods can modify the value they point to.
  • Avoids copying data on every method call, which is beneficial for large struct types.

In the example, Scale and Abs have receivers of type *Vertex.
Even though Abs doesn’t need to modify the receiver, generally, all methods with a type should have either value or pointer receivers but not mix both.

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

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

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

func main() {
	v := &Vertex{3, 4}
	fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
	v.Scale(5)
	fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
/*
>>> Before scaling: &{X:3 Y:4}, Abs: 5
>>> After scaling: &{X:15 Y:20}, Abs: 25
/*

Recent Posts

Categories

Tags