One of the things that is most punted about with the Go language is its ease of understanding the code that has been written. The language was built to be as simple as possible, and features that the maintainers deem to be superfluous are left out. Like generics. Some view this as a disadvantage though, as it takes and increased number of key strokes and lines of code to achieve what may have been achieved by a 1 line lambda anonymous function in other languages.
As the code base grows larger in size, some of the simplicity and understanding that was seen in the “Molo Mhlaba”* tutorial that we started learning the language with, starts to disappear. We end up with files with several hundred lines, and really long methods, making it harder to understand the intention of the developer who wrote the code, and therefore making it hard to change without fear of breaking everything.
Go started off as a language mainly for infrastructure, but is now increasingly being used by businesses and organizations who execute tasks within their specific domain. And in specific domains, the code becomes easier to understand if it reflects the phrases and terms that are used within that domain.
Imagine the case of a go struct that holds an order, as well as order items.
type Order struct {
OrderItems []OrderItem
}
type OrderItem struct {
Quantity int
ProductCode string
UnitPriceExcludingTax float64
UnitPriceIncludingTax float64
}
type Proudct struct {
Name string
Code string
PriceExcludingTax float64
}
In this case, when we want to add items to the order, we could do it as follows:
func NewOrder(products []*Products) {
taxRate := float64(0.15)
order := *Order{ OrderItems: []OrderItem{}}
for _, product := range products {
orderItem := OrderItem{
Quantity: 1,
ProductCode: product.Code,
UnitPriceExcludingTax: product.PriceExcludingTax,
UnitPriceIncludingTax: product.PriceExcludingTax * (float64(1) + taxRate)
}
order.OrderItems = append(order.OrderItems, orderItem)
}
}
Seems simple enough, and easy enough to read, but there is a way that can possibly make it easier to understand and more readable. We can extract a method to add an item to an order, to more clearly reveal the intention of the code. The function can look as follows:
func AddProductToOrder(order *Order, product *Product, quantity int) {
taxRate := float64(0.15)
orderItem := OrderItem{
Quantity: quantity,
ProductCode: product.Code,
UnitPriceExcludingTax: product.PriceExcludingTax,
UnitPriceIncludingTax: product.PriceExcludingTax * (float64(1) + taxRate)
}
order.OrderItems = append(order.OrderItems, orderItem)
}
The issue with the above is that it is not obvious that it is changing the state of the Order parameter that is passed in. We could follow a functional paradigm, and assume the Order to be immutable, and in changing the state, we are returning a new Object as follows:
func AddProductToOrder(order Order, product *Product, quantity int) Order {
taxRate := float64(0.15)
orderItem := OrderItem{
Quantity: quantity,
ProductCode: product.Code,
UnitPriceExcludingTax: product.PriceExcludingTax,
UnitPriceIncludingTax: product.PriceExcludingTax * (float64(1) + taxRate)
}
order.OrderItems = append(order.OrderItems, orderItem)
return order
}
In the above example, we are passing a copy of the Order into the function and not a pointer reference. Therefore, we are passing a new, updated Order to the caller.
A second approach can be the object-oriented approach. In this approach, the data and the methods that manipulate that data stay together. In the example above, this would mean that adding an item to an Order should also be part of the Order, something we can achieve with pointer receivers:
func (order *Order) AddProductToOrder(product *Product, quantity int) Order {
taxRate := float64(0.15)
orderItem := OrderItem{
Quantity: quantity,
ProductCode: product.Code,
UnitPriceExcludingTax: product.PriceExcludingTax,
UnitPriceIncludingTax: product.PriceExcludingTax * (float64(1) + taxRate)
}
order.OrderItems = append(order.OrderItems, orderItem)
}
In the above example, the calling function calls order.AddProductToOrder(product, 2)
, manipulating the state of the order.
Whichever approach is followed, having specific functions to update the state, named in terms of the domain behavior makes it clear what the intention is, making the code clearer, easier to understand, and more maintainable.
*Molo Mhlaba is Hello World is isiXhosa