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.
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.
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.
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.
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
.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.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
.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
.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.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.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.
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.