Composition over Inheritance
From my understanding, there is no constructor in Go, since Go does not advocate the concept of Object-Oriented Programming. Go prefers the idea of composition, which is more flexible than inheritance. Also, this is how C works, and Go is a language that is inspired by C.
How to construct structs?
My personal ranking of these 3:
Functional Options > Direct Field Access > “Constructors”
0. Setting
Suppose we have this Pineapple struct, with 3 fields:
from
where pineapple is producedweight
weight of pineappleprice
price of pineapple
1. Direct Field Access
The most straightforward way to construct a struct is to directly access the fields of the struct.
Struct Definition
package pineapple
// define pineapple struct
type Pineapple struct {
from string
weight int64
price float64
}
// "Constructor" for Pineapple
func NewPineapple() *Pineapple {
// default values for Pineapple
return &Pineapple{
from: "Hawaii",
weight: 1000,
price: 10.0,
}
}
How to use
package main
func main() {
p := pineapple.NewPineapple()
fmt.Printf("Pineapple from %s, weight %d, price %.2f\n", p.From, p.Weight, p.Price)
p.From = "USA"
p.Weight = 150
p.Price = 15.0
fmt.Printf("Pineapple from %s, weight %d, price %.2f\n", p.From, p.Weight, p.Price)
}
We will get result:
Pineapple from Hawaii, weight 1000, price 10.00
Pineapple from USA, weight 150, price 15.00
Pros
Simple and straightforward
.Slightly Flexible
: imagine, we need to add a new filedcolor
to the struct, we don’t need to change that much, just add the default value ofcolor
to the NewPineapple function and it is done.Readable
: it is very easy to understand what is happening.
Cons
- All fields have to be public. This is a huge problem.
- Mutability. In this way, the constructed pineapple is mutable.
- Not thread-safe, as it is mutable.
2. “Constructors”
The traditional OOP way, not a good fit for this scenario.
Struct Definition
package pineapple
type PineConstructorApple struct {
from string
weight int64
price float64
}
func New(price float64, weight int64) *PineConstructorApple {
return &PineConstructorApple{
from: "Hawaii",
weight: weight,
price: price,
}
}
func NewWithFrom(from string, price float64, weight int64) *PineConstructorApple {
return &PineConstructorApple{
from: from,
weight: weight,
price: price,
}
}
How to use
func main() {
p := pineapple.NewWithFrom("Hawaii", 10.0, 1000)
fmt.Printf("Pineapple %+v\n", p)
}
Pros
Immutable
, since we haven’t put setters there.
Cons
Exponential complexity
: imagine we have 5 fields, we need to have 2^5 = 32 constructors.Inflexible
: imagine we need to add a new fieldcolor
, how many constructors more do we need to add?
3. Functional Options
The most flexible way to construct a struct in Go.
Struct Definition
package pineapple
type PineWithApple struct {
from string
weight int64
price float64
}
type PineWithAppleOption func(*PineWithApple)
func WithFrom(from string) PineWithAppleOption {
return func(opts *PineWithApple) {
opts.from = from
}
}
func WithWeight(weight int64) PineWithAppleOption {
return func(opts *PineWithApple) {
opts.weight = weight
}
}
func WithPrice(price float64) PineWithAppleOption {
return func(opts *PineWithApple) {
opts.price = price
}
}
func NewPineWithApple(opts ...PineWithAppleOption) *PineWithApple {
// default values for PineWithApple
options := PineWithApple{
from: "Hawaii",
weight: 1000,
price: 10.0,
}
for _, opt := range opts {
opt(&options)
}
return &options
}
How to use
func main() {
p := pineapple.NewPineWithApple(
pineapple.WithFrom("Taiwan"),
pineapple.WithWeight(1234),
)
fmt.Printf("Pineapple: %+v\n", p)
}
Pros
Simplicity
: it is very easy to understand what is happening.Flexible
: imagine we need to add a new fieldcolor
, we just need to add a newWithColor
function and it is done.Immutable
: cannot change the constructed pineapple.Thread-safe
: as it is immutable.
Cons
- Need some functional programming understanding.