Object-Oriented Programming with Go: the constructor
Since Go (1.17.6) doesn't provide specific Object-Oriented Programming keywords (no class, no constructor), the programmer has to know some of Go bricks and put them together to build Object-Oriented Programming mechanisms with Go. This article aims to illustrate the process that leads to the implementation of a mandatory constructor. It assumes the reader knows the meaning of terms like class, constructor, property and object in the scope of Object-Oriented Programming. It also assumes that the reader can write, build and run a "hello world" program with Go and search for additional information if needed (for instance, the examples mention slices but don't detail on them, suffice to know they are Go's dynamic arrays). When aspects are detailed it is in line with the primary objective of the article.
Introduction
My first program in Go was a challenge: build the Snake Game in the command line.
When writing commands or services it makes sense to go fully imperative or functional : the clients make requests, they got served. Go, as a cloud-oriented language, was designed with simplicity, composition, and concurrency in mind rather than traditional OOP. It avoids deep inheritance and class-based structures, leading some to argue that discussing OOP in Go misses the point entirely.
Yet, in certain scenarios, object-oriented thinking remains the most natural way to model complex problems. When the application presents a user interface and manages state, it becomes natural to think in term of "objects". This was the case with GoSnake: the Candy, the Snake and the Board all had to be independent objects that manage their own state: in short, we needed a classes-like approach.
The problematic
This article focuses on constructors, specifically ensuring that they are executed when an object is instantiated. Let's first discuss how to implement "classes" alike with struct and methods. We will then introduce packages and see how we implement a mandatory constructor.
Struct and methods
In Go (1.17.6) there is no class keyword. We use Struct to create custom compound types:
// Direction specifies deltas in X and Y
type Direction struct {
DX int
DY int
}
// Position defines coordinates
type Position struct {
X int
Y int
}
aside note: some examples in this articles are copied as is from [GoSnake](https://github.com/Amari-Mecheri/GoSnakeText
We may then have a variable of type Direction and access its properties DX and DY
var goLeft = Direction{
DX: -1,
DY: 0,
}
// goLeft.DX = -1 and goLeft.DY = 0
It is also possible to add methods to a struct:
// the snake is defined as a slice of Positions (one for each part of its body) and a Direction it is moving to.
type Snake struct {
body []Position
direction Direction
}
func (aSnake Snake) Size() int {
return len(aSnake.body)
}
func (aSnake *Snake) SetDirection(direction Direction) {
aSnake.direction = direction
}
Size() and SetDirection() are two methods on the struct Snake. Which means they are associated with the type struct Snake. In Go words, the struct Snake is the receiver.
Hence, the signature of the method func (aSnake Snake) Size() int
This form is (almost) equivalent to func Size(aSnake Snake) int
The difference is that, after var s Snake, with the first form we would call the method s.Size() while with the second form we would call the function Size(s). As a consequence s, may be nil when it is an argument of a function call but has to be defined to allow any method call.
The trailing int indicates the type of the returned value.
We also notice that the signature of the method SetDirection is slightly different func (aSnake *Snake) SetDirection(direction Direction). Here, the receiver is of type pointer. SetDirection will modify the direction of a given snake (it needs a reference/pointer to it) while Size has a receiver of type value since it only reads and returns the length of the body (it receives a copy of it, not it ).
// case 1: The receiver is of type value
func (aSnake Snake) SetDirection(direction Direction){
aSnake.direction = direction
}
// when s is created, by default, its direction is set to (DX:0, DY:0)
var s Snake
s.SetDirection(goLeft)
// we expect s.direction to be (DX:-1, DY:0) but it is still (DX:0, DY:0)
// since SetDirection actually modified a copy of s, not s.
// case 2: The receiver is of type pointer
func (aSnake *Snake) SetDirection(direction Direction){
aSnake.direction = direction
}
var s Snake
s.SetDirection(goLeft)
// s.direction is, as expected, set to (DX:-1, DY:0)
GoSnake uses receivers of type pointers even in the case of methods like Size(). Some linters will alert of such cases. Indeed, it might be good practice to distinguish between methods that do modify the object (by providing a pointer) and those that don't (by providing a value) but be aware that in case of a mistake, there wouldn't be any error raised. The program would build normally. The issue would manifest during proper unit testing or, worse, at runtime. Linters correctly configured would warn of such situations.
OK, at this point, we have a Snake type and methods associated to it.
Let's reconsider the Size() method. It returns the length of the snake's body return len(aSnake.body); body is defined as a slice []Position. Hence, body is a pointer to a slice and by default it has a value of nil; it has not been initialized, no actual slice has been created. Still, in Go we may let things that way, since functions like len or append are able to deal with 'nil' values as long as the argument they receive is of type slice. This is the reason the method Size() will return 0 even though aSnake.body is equal to nil. Let's check:
package main
import (
"fmt"
)
func main() {
type Position struct {
X int
Y int
}
var body []Position
fmt.Println(body)
fmt.Println(len(body))
fmt.Println(body==nil) //is body nil? yes
}
output:
[]
0
true
Case 2 explicitly initializes body to an empty slice:
package main
import (
"fmt"
)
func main() {
type Position struct {
X int
Y int
}
var body = []Position{}
fmt.Println(body)
fmt.Println(len(body))
fmt.Println(body==nil) //is body nil? no
}
output:
[]
0
false
fmt.Println and len functions rely on the type of the argument to determine the way to handle nil values. Indeed, Go source files for the built-in len function provide these indications:
// The len built-in function returns the length of v, according to its type:
// Slice, or map: the number of elements in v; if v is nil, len(v) is zero.
func len(v Type) int
OK, this is part of the specifications and we can conclude that the method Size() will work fine even though we haven't explicitly initialized Snake.body
As mentioned previously, the imperative form allows nil values as arguments. In JavaScript , for instance, arrays are objects and have to be initialized before use. When converting snake.go to Typescript for ReactSnake and AngularSnake, body had to be initialized to an empty array in the constructor.
For reference, the Typescript version:
class snake {
private body: Position[]
private direction: Direction
constructor(){
this.body = new Array(0)
}
Size(): number {
return this.body.length
}
SetDirection(direction: Direction) {
this.direction = direction
}
}
What if we want to do the same in Go and have the slice explicitly created prior to any method call?
There are many situations where we need to perform initialization at instantiation; if for instance, our struct holds references to other objects. Those objects would need to be created before use. For now, and for simplicity let's stick with the snake and see how we can implement a constructor and initialize the body just as we did in Typescript.
Packages
Go let us organize our code in packages. They are independent units of code. In order to use a package, we need to import it and the package itself needs to import any other package it needs access to. But that's not all. Go uses a convention were anything (constant, variable, custom type, function or method) starting with a capital letter is exported (public), meaning it can be accessed outside the package otherwise it is private to the package.
For example, The entry point to a Go program is the main package.
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello world!")
}
The package imports the fmt package which provides a Println function. Println is accessible to the main package because its name starts with a Capital letter.
This is exactly, the behavior we are going to use to add a mandatory constructor to the struct snake.
The constructor
In Go there is no built-in mechanism to automatically call a constructor when instantiating a struct. So even, if we include a function that initializes the properties, we have no guarantee that it will be effectively used by the consumer of our package. Hence, to prevent direct instantiation, we let the struct private to the package where it belongs and provide a New() function that will instantiate, initialize and return the object ready for use.
Let's see an example with the snake package. To create the snake package, we need a snake folder in which we add a snake.go file:
package snake
// Direction specifies deltas in X and Y
type Direction struct {
DX int
DY int
}
// Position defines coordinates
type Position struct {
X int
Y int
}
type snake struct {
body []Position
direction Direction
}
// New returns an instance of snake
func New() snake {
var aSnake snake
aSnake.body = []Position{}
return aSnake
}
func (aSnake *snake) Size() int {
return len(aSnake.body)
}
func (aSnake *snake) SetDirection(direction Direction) {
aSnake.direction = direction
}
Direction and Position are included for testing purposes, in GoSnake they are in a common package and accessed via common.Direction and common.Position
In the code above, we introduced a function New() snake. This function acts as a fabric/constructor. Since the struct snake is private to the package, it can't be accessed. We have to call snake.New() to get an instance of the snake.
package main
import (
"fmt"
"gosnake/pkg/snake"
)
func main() {
var s snake.snake
fmt.Println(s.Size())
}
output: cannot refer to unexported name snake.snake
var s snake.snake: just like we wrote fmt.Println to access the Println function from the fmt package, we are trying to access the type snake struct from the snake package. As we know, the type snake struct is not exported thus it is not accessible. The program won't build.
aside note : import "gosnake/pkg/snake"
GoSnake uses a convention for directory structure where the main package is in the cmd directory while other packages like the snake package lie in the pkg directory.
package main
import (
"fmt"
"gosnake/pkg/snake"
)
func main() {
var s = snake.New()
fmt.Println(s.Size())
}
output:
0
This time we call the New() function from snake package to create a snake for us. Since the function is inside the snake package it has access to the type snake struct and its properties.
func New() snake {
var aSnake snake
aSnake.body = []Position{}
return aSnake
}
The function creates aSnake variable, initializes aSnake.body to an empty slice and returns the variable aSnake. In the main function, we now call s.Size() and print the result.
Returning a pointer
In the previous example, the New function creates a snake with var aSnake snake and actually returns a copy of aSnake not aSnake. If we want to return aSnake itself we need to return a pointer to snake and thus return &aSnake. The simplest form is to use the new keyword to create aSnake.
func New() *snake {
aSnake := new(snake) //aSnake is of type pointer to snake
aSnake.body = []Position{}
return aSnake
}
Conclusion
We met our objective: We know how to implement a constructor in Go and guarantee that the initialization process has been executed before any access to the properties and before any call to the methods.
Still, the situation is not ideal yet. We can't use the private struct to compose other structs since we can't access the protected type struct. In order to solve this limitation we will implement polymorphism with the help of interfaces. Let's see how in the next article Object-Oriented Programming with Go: inheritance and composition.
Thank you.