Methods
In Go, methods are functions that are attached to a type.
Here, we have a type called Coordinates
and we have a method called Move
that is attached to the Coordinates
type.
type Coordinates struct {
X int
Y int
}
func (i Coordinates) Move(x, y int) {
i.X = x
i.Y = y
}
func main() {
i := Coordinates{
Y: 10,
X: 20,
}
fmt.Printf("i: %#v\n", i)
i.Move(100, 200)
fmt.Printf("i (move): %#v\n", i)
}
However, there is a small bug.
i: main.Coordinates{X:20, Y:10}
i (move): main.Coordinates{X:20, Y:10}
The Move
method is not actually changing the value of i
. This is because Go passes everything by value - that includes receivers.
To fix this, the method needs to be a pointer receiver.
func (i *Coordinates) Move(x, y int) {
i.X = x
i.Y = y
}
i: main.Coordinates{X:20, Y:10}
i (move): main.Coordinates{X:100, Y:200}
When considering whether to use a pointer receiver or a value receiver, consider the following:
- Do I want to share the value (allows mutating the value)?
- Or should everyone have their own copy?
A rule of thumb is:
Value Receiver:
- If using types that are like built in types (int, string, etc) use value receiver.
- If using your own types and they're not going to change, use value receiver.
Pointer Receiver:
- If using your own types and they're going to change, use pointer receiver.
However, it is recommended for consistency to use either pointer or value receivers for all methods on a type.
Embedding
Go uses embedding to compose types together in a powerful yet simple way.
Embedding promotes simplicity and clarity in structuring code via composition. Inheritance, especially multiple inheritance, can lead to complex hierarchies and relationships between classes that are often difficult to understand and maintain.
Embedding also allows a struct to "borrow" fields and methods of another struct via composition. This means modifying or extend behavior can be done without modifying the original struct.
Here we define a Player
type that has a Name
and also embeds the Coordinates
field. This means that the Player
type will have access to the Move
method.
It is important to clarify that embedding is not inheritance. The Player
type does not inherit the Move
method, it just has access to it.
type Player struct {
Name string
Coordinates // embedded
}
func main() {
p1 := Player {
Name: "Player 1",
Coordinates: Coordinates{
X: 10,
Y: 20,
},
}
fmt.Printf("p1: %#v\n", p1)
p1.Move(100, 200)
fmt.Printf("p1: %#v\n", p1)
}
p1: main.Player{Name:"Player 1", Coordinates:main.Coordinates{X:10, Y:20}}
p1: main.Player{Name:"Player 1", Coordinates:main.Coordinates{X:100, Y:200}}
Interfaces
If we want to define a function that can be used by multiple types, we can use an interface. An interface is a set of method signatures and if a type implements all the methods in the interface, then it is said to implement the interface.
func moveAll(ms []mover, x, y int) {
for _, m := range ms {
m.Move(x, y)
}
}
Here the function moveAll
takes a slice of mover
types and calls the Move
method on each one. This means all the values in the slice must implement the Move
method. In Go, we do this by defining an interface that has a single method called Move
.
Note that in Go, the powerful thing about interfaces is that it defines what we need, not what we provide, so we can implement an interface without explicitly declaring it. If a type has all the methods defined in an interface, then it implements the interface.
In this case, the interface mover
states that "I want something we can Move()
". Now any type that has a Move
method will implement the mover
interface.
type mover interface {
Move(x, y int)
}
Because i Coordinates
and p Player
both have a Move
method, they both implement the mover
interface, and therefore can be passed into the moveAll
function.
func main() {
i := Coordinates{
Y: 10,
X: 20,
}
p := Player {
Name: "Player 1",
Coordinates: Coordinates{
X: 10,
Y: 20,
},
}
ms := []mover{
&i,
&p,
}
moveAll(ms, 100, 200)
}