Learning Go (part 1) - Conway’s Game of Life

The Go programming language is currently fashionable, and it’s been four years since the last time I started to learn a new programming language (Python), so I decided to start a series about my learning attempts. As I also want to know more about Kubernetes, my goal is to learn enough Go in the next few months to read the Kubernetes code base comfortably. As a starting point for this multi-part series, I’ve chosen Conway’s Game of Life [1], because its rules are simple but more complex than a typical hello world example.

Change history:
Date Change description
2018-04-13 The first release

Note

As this is a learning endeavor, there will be mistakes. Don’t confuse the code below with any kind of actual recommendation.

Installing the latest Go runtime

The installation of the Go runtime [2] is coded in the following Ansible playbook:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
---

- hosts: all
  become: true

  tasks:

    - name: "The apt cache should be updated."
      apt:
        update_cache: true
        cache_valid_time: 3600

    - name: "The basic build tools should be installed."
      apt:
        name: gcc,make
        state: present

    - name: "The (old) OS packaged GO lang should NOT be available."
      apt:
        name: golang-go
        state: absent

    - name: "The working directory of GO should be created."
      file:
        name: "{{ ansible_env.HOME }}/go"
        state: directory

    - name: "The GO lang archive should be downloaded and extracted."
      unarchive:
        src: https://dl.google.com/go/go1.9.3.linux-amd64.tar.gz
        dest: /usr/local/
        remote_src: yes
        creates: /usr/local/go/bin/go

    - name: "The GO PATHs should be set globally."
      lineinfile:
        path: /etc/profile
        line: "{{ item }}"
      with_items:
        - GOPATH=$HOME/go
        - PATH=$PATH:/usr/local/go/bin
        - GOROOT=$HOME/go
        - GOBIN=$HOME/go/bin

This is only one way of doing it, and it’s not necessary to have the latest greatest for the code below.

Setting up the project

We need a unique package name for our code, to avoid import clashes [3]. The recommendation for code which will live on Github looks like this:

1
$ mkdir -p $GOPATH/src/github.com/markuszoeller/cgol

Later, when we build the source code, it will be added to the pkg directory, and we get this structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
root@golang:~/go# tree
.
|-- pkg
|   `-- linux_amd64
|       `-- github.com
|           `-- markuszoeller
|               `-- cgol.a
`-- src
    `-- github.com
        `-- markuszoeller
            `-- cgol
                |-- cells.go
                `-- cells_test.go

One interesting thing to notice is, that the test code lives in the very same directory of the functional code, and Go ignores that test code when building the source.

Writing the functional code

Before we dive into the code, here’s a short recap of the rules of Conway’s Game of Life, straight from Wikipedia [1]:

Any live cell with fewer than two live neighbours dies, as if caused by underpopulation. Any live cell with two or three live neighbours lives on to the next generation. Any live cell with more than three live neighbours dies, as if by overpopulation. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

I’ve decided to go with the simplest solution I could think of, a two-dimensional array of boolean values which represent the cells in my world. A true value is a cell which is alive, a false value is a cell which is dead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package cells

var world [10][10]bool

func resetWorld() {
	// world = [10][10]bool  # doesn't work
	for row := 0; row < len(world); row++ {
		for col := 0; col < len(world[row]); col++ {
			world[row][col] = false
		}
	}
}

func resurrectCell(row int, col int) {
	world[row][col] = true
}

func killCell(row int, col int) {
	world[row][col] = false
}

func getCell(row int, col int) bool {
	return world[row][col]
}

func getAliveNeighbours(row int, col int) int {
	count := 0
	// count += int(getCell(row-1, col-1))  # does not work
	if getCell(row-1, col-1) {
		count++
	}
	if getCell(row-1, col) {
		count++
	}
	if getCell(row-1, col+1) {
		count++
	}
	if getCell(row, col-1) {
		count++
	}
	if getCell(row, col+1) {
		count++
	}
	if getCell(row+1, col-1) {
		count++
	}
	if getCell(row+1, col) {
		count++
	}
	if getCell(row+1, col+1) {
		count++
	}
	return count
}

func getNextGenState(row int, col int) bool {
	alive := getCell(row, col)
	alive_neighbours := getAliveNeighbours(row, col)

	if alive && alive_neighbours < 2 {
		return false
	}
	if alive && alive_neighbours >= 2 && alive_neighbours <= 3 {
		return true
	}
	if alive && alive_neighbours > 3 {
		return false
	}
	if !(alive) && alive_neighbours == 3 {
		return true
	}

	// should not happen
	return false
}

A few things to notice here:

  • world = [10][10]bool was an attempt to reset all values to false for the tests, but I couldn’t make it work, so I took the dummy approach by iterating over all rows and columns of my world.
  • row := 0 shows the shorthand for variable definition and value assignment. It’s equivalent to var row int = 0.
  • I didn’t find a built-in way to convert the boolean value to an Integer value, so count += int(getCell(row-1, col-1)) didn’t work, therefore I used those ugly conditionals. Go doesn’t seem to have a ternary operator or any other way to do that in one line.
  • Declaring bool as a return type of a function didn’t allow me to return nil (the “nothing” of Go), so I used false at the end.
  • !(alive) shows an example of boolean negation. In Python it would look like not alive.

Writing the unit tests

Let’s test what we have so far:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package cells

import "testing"

func TestResurrectAndKillCell(t *testing.T) {
	resetWorld() // there is no builtin `setup` and `teardown`
	resurrectCell(1, 5)
	if getCell(1, 5) == false {
		t.Errorf("Should be alive")
	}
	killCell(1, 5)
	if getCell(1, 5) == true {
		t.Errorf("Should be dead")
	}
}

func TestGetAliveNeighbours(t *testing.T) {
	resetWorld()
	resurrectCell(3, 4)
	count := getAliveNeighbours(3, 4)
	if count != 0 {
		t.Errorf("Should be exactly 0")
	}
	resurrectCell(3, 3)
	count = getAliveNeighbours(3, 4)
	if count != 1 {
		t.Errorf("Should be exactly 1")
	}
}

func TestGetNextGenStateUnderPopulation(t *testing.T) {
	resetWorld()
	resurrectCell(3, 7)
	alive := getNextGenState(3, 7)
	if alive {
		t.Errorf("3,7 Should be dead because of under population")
	}
	resurrectCell(4, 7)
	alive = getNextGenState(3, 7)
	if alive {
		t.Errorf("3,7 Should be dead because of under population")
	}
}

func TestGetNextGenStateOkayPopulation(t *testing.T) {
	resetWorld()
	resurrectCell(3, 7)
	resurrectCell(4, 7)
	resurrectCell(5, 7)
	alive := getNextGenState(4, 7)
	if !(alive) {
		t.Errorf("4,7 Should be alive")
	}
}

func TestGetNextGenStateOverPopulation(t *testing.T) {
	resetWorld()
	resurrectCell(3, 6)
	resurrectCell(4, 6)
	resurrectCell(5, 6)
	resurrectCell(4, 7)
	resurrectCell(5, 7)
	alive := getNextGenState(4, 7)
	if alive {
		t.Errorf("4,7 Should be dead")
	}
}

func TestGetNextGenStateReproduction(t *testing.T) {
	resetWorld()
	resurrectCell(3, 6)
	resurrectCell(4, 6)
	resurrectCell(5, 6)
	alive := getNextGenState(4, 7)
	if !(alive) {
		t.Errorf("4,7 Should be alive")
	}
}

Things to notice are:

  • t *testing.T is the one parameter every test function needs to accept to have access to the built-in testing framework. I’m not quite sure how to read it, but I guess it means we accept a reference (*) to an instance of the class T in package testing and store it in parameter t.
  • I didn’t find a built-in way of executing code before and after every test function, like setup and teardown, which are known from other XUnit testing frameworks. Honestly, this was quite odd to me. This forced me to reset the world with resetWorld() in each test method. Very odd.
  • Other XUnit frameworks I used in the past usually had assertion methods like assertEqual or assertTrue. Go doesn’t have that. Nothing stops you to implement that yourself as convenience methods, but I was very surprised to not find it built-in.

Format, test and build the project

Finally, we can check that our code is formatted like expected with go fmt, then we execute the tests with go test and at last we build the project with go build:

1
2
3
$ go fmt
$ go test
$ go build

The go fmt command, which seems to be the authoritative source of how the source code should be formatted, replaced all my spaces by tabs. Frankly, I thought the whole fight about tabs versus spaces for formatting source code was over and the usage of spaces won. Apparently I was wrong. But I wasted too many hours of my life discussing that, so I just accept it that Go decided to do it this way.

Summary and next steps

This post showed only how to write basic library code in a very basic way. In the next post of this series, I’d like to add a CLI which imports the library, takes some user input and then displays some generations of the cells, according the rules we specified.

Learning more about the object oriented side of Go is definitely also something on my plate. Note sure if will be already in the next post of this series, as I expect it to be a bit more complex.

It’s too early for me to make a statement about how I feel about Go. The next posts will explore more things and most likely uncover some mistakes I made in this post here. Time will tell.

Comments

comments powered by Disqus