Revisiting Go

2020-06-26 · 8 min read

I started learning Go a year ago, amazed at how it checks for errors at compile time (haha my first time). I went for a Golang course (sponsored by company hehe) but didn't practise much until now. I used a programming problem to practise Golang.

I blogged about Gophercon 2019 here

Golang - OOP...?

According to the official site, Yes and No. The closest equivalent to a class in OOP is a struct in Golang and it in itself has many differences.

type Robot struct {
X int
Y int
Direction Direction
}

func (r Robot) GetPosition() string {
return fmt.Sprintf("%d, %d", r.X, r.Y)
}

A type can satisfy many interfaces as well, unlike certain OOP languages which restrict that.

Learning about Package Visibility

I came across the term unexported types when I was googling for OOP in Golang. The first time I saw the word unexported, I thought it was a careless typo.

And so, it turns out the Capitalized letter we see in structs/method calls is intentional. It represents exported which means a method in another package can call it. If it is unexported, you can't use it.

As there is no idea of constructors in Golang, I came across this article that suggests we unexport classes (I'm using this term loosely) to prevent access from other package and export instead a New method which will return the unexported type.

type employee struct {  
firstName string
lastName string
}

func New(firstName string, lastName string) employee {
e := employee {firstName, lastName}
return e
}

However, I later faced problems with this pattern. I was in package main, trying to use the unexported type from package robot in another struct, but faced the error Unable to use unexported type. I couldn't find a solution so I chose to revert to exporting the type :p

I guess the important lesson here is knowing how unexporting work and how you can use it as one of the means of encapsulation (or not.

Using *robot and robot

Okay I keep forgetting this!!

For every type T, there exists another type *T where the star represents the address of value of type. In Go, everything is pass by value.

Here's an example to illustrate

func main() {
apples := 10
fmt.Println(apples) //10
decrement(apples)
fmt.Println(apples) //10

myAge := Age{40}
fmt.Printf("age is %d\n", myAge)
myAge.increment()
fmt.Printf("age is %d\n", myAge)
}

func decrement(num int) {
num--
}

type Age struct {
age int
}

func (age Age) increment() {
age.age++
}

This will return an error. This is because the variable age that is called on, lives at a different address than the variable myAge. When the variable is passed as a parameter, a new copy is created and hence lives at a different memory address. What is effectively inc/decremented is a duplicate. In order to increment correctly, we have to specify the address of the variable we already defined.

decrement(&apples) // pass in pointer
myAge.increment() // don't have to dereference

func decrement(num *int) {
num--
}

func (age *Age) increment() {
age.age++
}

This will work! One note is that using the & will dereference the variable - giving us the pointer address. For Structs, we don't have to explicitly dereference it as it is cumbersome

I found this answer which sums it up nicely:

func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct) valueMethod() { } // method on value

Thus we can use Get methods (eg. get robot position) as value methods and have to use Set methods (eg. change robot x, y coor / direction) as pointer methods.

Using const and iota

I was toying with enums and even a global map[string][int] (but unable to initialize it outside of a main function) to implement direction. Incidentally, I came across the use of const.

const declares a constant value and can be used like how a var works.

iota on the other hand, represents consecutive integers that resets to 0 when const appears and increments after each declaration.

type Direction int
const (
N Direction = iota
E Direction = iota
S Direction = iota
W Direction = iota
)

I thought it was suitable for Direction to be an integer as I can then denote a clockwise and anticlockwise turn with a +1 and -1 respectively, while also checking the bounds that it remain between the values 0 to 3.

Slice and array

I encountered this error at the last stages as I was parsing the list of commands.

instructions := [3]string{"M", "L", "M"}
room.RobotInstructions(instructions)
// ERR: cannot use (type [3]string) as type []string

I asked for help and help answered to just remove the length 3. I was shocked! So I guess it was time to be properly schooled.

Arrays in Go are values as they are fixed-length sequences (assigning one array to another means copying all elements and that passing an array will result in a copy of it, not a pointer). Slices are reference types (cheap to assign, to pass, to resize).

Hence, slices are more powerful, more flexible and more efficient.

Composite Literal error

There was a composite literal error warning. Not sure why but I just had to change robot.Robot{3, 5, N} to robot.Robot{X: 3, Y: 5, Direction: N}

Error and Exception handling

We can throw an error like this

errors.New("Invalid Command")

And test it like this:

err := room.MoveRobot("A")
if err == nil {
t.Errorf("Expected error but received no error")
}
var ErrInvalidCommand = errors.New("Invalid command")
if errors.Is(err, ErrInvalidCommand) {
t.Errorf("Unexpected error %v, but received %v", "Invalid command", err)
}

Concluding Thoughts

I think the mars rover problem or any other problem is a good way to practise a new programming language. I get to practise my TDD and also explore different ways of implementing the solution. Different solutions also show different advantages / efficiency of different languages. :)

R
Rong Ying

Did you know this was built with 11ty and tailwind? And works even with Javascript disabled? Yeah I don't care either.