[A Tour of Go Study Notes] 07 method
Understanding Go method using official tutorials
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
/*