2
\$\begingroup\$

I decided to give Go a try and implemented a Game of Life Kata exercise in GOLang. I have no prior experience in Go and the majority of my experience comes from Java, C#, and Python. My code appears to be working as intended but not sure if I've implemented it in the GO way, or idiomatic Go.

I've seen a few Go examples online where the properties of the struct were always public, but that feels foreign coming from the object-orient world. The way I've implemented it the Board struct should never be able to get into an invalid state. I don't know if that is a mindset that is shared by the Go community.

I have a colleague who is big into Go and favors immutability and no side effects. I can see how that would be ideal for concurrency, but does the general Go community prefer avoiding mutations? As an example I could have implemented my Evolve method to return a new Board struct rather than mutate its state property.

Is there anything else that stands out as not being Go like?

package board

import (
    "errors"
    "math/rand"
    "strconv"
    "time"
)

const CellDead = 0
const CellAlive = 1

type board struct {
    state [][]int
    rows int
    columns int
}

/* Creates a new Board with the given dimensions. The dimensions, rows and columns,
must be positive integers greater than 0.
Returns a board populated with a random state. */
func NewRandomBoard(rows, columns int) (board, error) {

    if rows < 1 || columns < 1 {
        return board{}, errors.New("rows and columns must be a positive integer greater than 0")
    }

    initState := make([][]int, rows)
    for i := range initState {
        initState[i] = make([]int, columns)
    }

    rand.Seed(time.Now().UnixNano())

    // Populate random state
    for i := range initState {
        for j := range initState[i] {
            initState[i][j] = rand.Intn((1 -0 + 1) + 0)
        }
    }

    return board{state: initState, rows:rows, columns:columns}, nil
}

func NewBoard(initialState [][]int) (board, error) {

    if initialState == nil {
        return board{}, errors.New("initialState cannot be nil")
    }

    if len(initialState) < 1 || len(initialState[0]) < 1 {
        return board{}, errors.New("initialState must contain at least 1 row and 1 column")
    }

    colSize := len(initialState[0])
    for i := 0; i < len(initialState); i++ {
        if colSize != len(initialState[i]) {
            return board{}, errors.New("initialState is a jagged 2D array, initialState cannot be jagged")
        }
        for j := 0; j < len(initialState[i]); j++ {
            cellValue := initialState[i][j]
            if cellValue < 0 || cellValue > 1 {
                return board{}, errors.New("initialState may only contain values 0 or 1")
            }
        }
    }

    return board{state:initialState, rows: len(initialState), columns: len(initialState[0])}, nil
}

func (b *board) Evolve() {
    newState := make([][]int, b.rows)
    for i := range newState {
        newState[i] = make([]int, b.columns)
        for j := range newState[i] {
            newState[i][j] = nextStateForCell(b,i,j)
        }
    }
    b.state = newState
}

func (b *board) State() [][]int {
    return b.state
}

func (b *board) Rows() int {
    return b.rows
}

func (b *board) Columns() int {
    return b.columns
}

func (b *board) PrettyPrint() {
    for i := range b.state {
        for j := range b.state[i] {
            print(" " + strconv.Itoa(b.state[i][j]) + "")
        }
        println()
    }
}

func nextStateForCell(b *board, i,j int) int {

    neighborsAlive := 0
    cellValue := b.state[i][j]
    for x := -1; x <= 1; x++ {
        for y := -1; y <= 1; y++ {
            if i + x < 0 || i + x > (b.rows - 1) || y + j < 0 || y + j > (b.columns - 1) {
                continue
            }
            neighborsAlive += b.state[i + x][y + j]
        }
    }
    neighborsAlive -= cellValue

    if cellValue == CellDead && neighborsAlive == 3 {
        return CellAlive
    } else if cellValue == CellAlive && (neighborsAlive < 2 || neighborsAlive > 3) {
        return CellDead
    } else {
        return cellValue
    }
}

The main file

package main

import (
    "io.jkratz/katas/life/board"
)

func main() {

    myBoard, err := board.NewRandomBoard(10, 10)
    if err != nil {
        panic("Failed to instantiate board")
    }

    myBoard.PrettyPrint()
    println()
    myBoard.Evolve()
    myBoard.PrettyPrint()
}
\$\endgroup\$
1
  • \$\begingroup\$ package is called board, and there's a function called NewBoard. The call stutters. Writing board.New() communicates the exact same thing as board.NewBoard() without the repetition. check the golang code review wiki for more conventions that are widely adopted. \$\endgroup\$ Jan 2, 2019 at 17:00

1 Answer 1

4
\$\begingroup\$

Is there anything else that stands out as not being Go like?

While reading through your code, I only noticed a few things that weren't idiomatic Go, so congrats!

Avoid print() and println() in favor of fmt

for i := range b.state {
    for j := range b.state[i] {
        print(" " + strconv.Itoa(b.state[i][j]) + "")
    }
    println()
}

Becomes:

for i := range b.state {
    for j := range b.state[i] {
        fmt.Printf(" %d ", b.state[i][j])
    }

    fmt.Println()
}

Notice that you also avoid strconv.Itoa().

rand.Intn((1 -0 + 1) + 0)

You mean rand.Intn(2)?

log.Fatal() instead of panic()

In your example usage, you use panic() to raise an error. Unless you plan to recover() from that, you can use log.Fatal() to write to standard error and exit the program.

panic("Failed to instantiate board")

Would be more commonly done as:

log.Fatalf("Failed to instantiate board: %s", err)

Or whatever error grammar you prefer. If you have an error value, you might as well use it to indicate the potential problem to the user.

Inconsistent formatting

For example:

func nextStateForCell(b *board, i,j int) int {

Would be consistently spaced as such:

func nextStateForCell(b *board, i, j int) int {

It's a nitpick. If you run go fmt on the source code, it should fix these kinds of things. If you use vim, there's also vim-go, which I find very helpful.


I have a colleague who is big into Go and favors immutability and no side effects. I can see how that would be ideal for concurrency, but does the general Go community prefer avoiding mutations?

I'm not sure if there's a consensus. Normally the answer is: "It depends." I think the way you've done it is fine. By mutating the state directly, you avoid potentially-costly memory allocations. So, for example, if you want to see the state after a trillion generations, it would likely be faster than if you constantly reassign based on return values.

Since you're new to Go, I recommend experimenting with concurrency. Here, you can concurrently read and determine the next state of the board.


Here is another example of a Go implementation of Conway's Game of Life, straight from the Go website.

Notice that they use a boolean field, rather than an integer one. This is more common (and performant), given that living is boolean.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I wish I could upvote this multiple times, thanks for the thorough answer and explanation! \$\endgroup\$
    – jkratz55
    Dec 31, 2018 at 15:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.