In Go, there are no explicit definitions for Class, Object and Inheritance like in traditional OOP languages but it offers similar features with a unique take.
Methods, Interfaces and Type Embedding are the building blocks for the OOP-like structure in Golang. Let's have a look at how they work.
Methods
A method is simply a function with a receiver argument.
Methods are bound to the type. So, we can't call methods directly like functions. It should be done through an instance of the specific type.
We can attach multiple methods to the type.
Not just limited to just structs. Methods can be defined for any type except pointer or interface.
Methods allow us to add actions/behaviors to our type just like class, object and method.
type plane struct {
// similar to Class attributes
passengers int
}
// receiver p is of type 'plane'
// fly() has a value receiver
func (p plane) fly() {
fmt.Println("air: flying with", h.passengers, "passengers")
}
func main() {
// p is like a instance of plane
p := plane{passengers: 100}
// similar to class method
p.fly()
}
Pointer vs Value Receivers in Method
Use a pointer receiver if you want to modify the original data of the receiver.
// Here the method is expecting a pointer receiver
func (p *plane) changeStatus() {
// this will modify original value of p even outside this function
p.status = "flying"
}
Interesting fact:
No matter whether the method is a pointer or a value receiver, we can use both value or pointer to call methods as shown below:
// ------- both will work -------
// using value
p := plane{passengers: 100}
p.fly()
// using pointer
p := &plane{passengers: 100}
p.fly()
So, we can have multiple methods in the same type, some being pointer receivers and some value receivers depending on their use case.
Interfaces
Interface in Go is a set of method signatures. It is very useful because it helps us to write loosely coupled, testable code.
Implementing an interface simply means implementing all of the methods defined by the interface. There is no other syntax to define the relationship. This is determined by Go implicitly.
In the example below:
flyer
interface has a method calledfly()
.
Structplane
andheli
are our custom types which have their implementation of thefly()
function i.e. different behavior.Function
flyPassengers(f flyer)
is expecting an interface of typeflyer
and it invokes thefly()
method.Since both
plane
andheli
implement the interface,flyPassenger
function will accept both instances of plane and heli structs passed as the parameter.
Having said that, its behavior will depend on the concrete type it received and how they have implemented thefly()
method. This is an example of polymorphism.Due to this polymorphic nature, we can use interfaces in dependency injection and mocking methods for tests.
package main
import "fmt"
type flyer interface {
fly()
}
type plane struct {
passengers int
}
// type plane satisfied flyer interface with fly() fn
func (p plane) fly() {
fmt.Println("plane: flying with", p.passengers, "passengers")
}
type heli struct {
passengers int
}
// type heli also satisfies flyer interface with fly() fn
func (h heli) fly() {
fmt.Println("heli: flying with", h.passengers, "passengers")
}
// this is an extra method which is not present in the interface
// but it will still satify the interface
func (h heli) hover() {
fmt.Println("heli: hovering")
}
// flyPassengers accepts any type which implements the flyer interface
// example of polymorphic function
func flyPassengers(f flyer) {
f.fly()
}
func main() {
a := plane{passengers: 100}
h := heli{passengers: 5}
// plane and heli are acceptable types for flyPassengers()
flyPassengers(a)
flyPassengers(h)
}
Interface implementation: Value vs Pointer Receivers
A receiver's type is important in interface implementation.
If we use a pointer receiver, e.g.
func (p *plane) fly()
, only pointers will satisfy the interface. This means in the function which is expecting interface type, we will have to pass a pointer:flyPassengers(&p)
Similarly for value type,
flyPassengers
will expect a value rather than a pointer.
type plane struct {
passengers int
}
func (p *plane) fly() {
fmt.Println("plane: flying with", p.passengers, "passengers")
}
// function expects a pointer to interface
flyPassengers(&p)
If we have additional methods in our type, will it still satisfy the interface?
Yes, as long as you implement all the methods listed by the interface. In the first example, you can see that we have hover()
method in heli
struct. Since heli
implements fly()
method, it satisfies the interface, so the other additional methods in heli
struct won't matter.
Type Embedding
type aircraft struct {
company string
}
// plane is outer type
type plane struct {
aircraft // inner type
passengers int
}
// You can get a hint of parent-child class.
// Usage eg:
p := plane{
aircraft: aircraft{company: "Airbus"}
passengers: 100
}
p.aircraft.company
Embedding is a way to combine methods from structs and interfaces.
If the embedded inner type satisfies the interface, the outer type will also indirectly satisfy the interface even if it does not implement all interface methods.
Interfaces can embed interfaces only.
Example:
package main
import "fmt"
type flyer interface {
fly()
}
// satisfies the interface
type aircraft struct {
company string
}
func (a aircraft) fly() {
fmt.Println("aircraft: flying", a.company)
}
// plane is the 'parent' type
type plane struct {
aircraft // embedded type
passengers int
}
// it will still work if we comment out fly method from plane,
// because the embedded type 'aircraft' will satisfy flyer interface
func (p plane) fly() {
fmt.Println("plane: flying", p.aircraft.company)
}
func flyPassengers(f flyer) {
// calls p.fly() if plane implements fly()
// or calls p.aircraft.fly()
f.fly()
}
func main() {
p := plane{
aircraft: aircraft{company: "Airbus"},
passengers: 100,
}
fmt.Println("directly accessing embedded type:", p.aircraft.company)
flyPassengers(p)
}
// The parent/outer type 'plane' will overwrite fly() method
// from embedded type 'aircarft' and the output will be:
// "plane: flying Airbus"
Effect on interface implementation:
As shown in the first example, if we comment out or delete fly()
method from plane struct, fly()
method from the embedded type aircraft will be called instead and the output will be:aircraft: flying Airbus
This means the parent type plane
will satisfy the interface indirectly if the embedded type implements the interface.
Constructors
In Go, there is a convention to create a constructor from a function named New
or New{TYPE_NAME}
.
For example, if you have a package name client
, you will ideally have a code structure like below:
pacakge client
type Client struct {
TimeoutSeconds int
}
// constructure returns empty initialized instance of Client
// the fn can be called as packageName.New() e.g. client.New()
func New()*Client {
return &Client{}
}
But if you have the possibility of another object initialization in the same package, it is better to name the function as NewClient
to avoid clashes.
References:
- Embedding: https://go.dev/doc/effective_go#embedding