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()
}
1 Answer 1
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.
-
1\$\begingroup\$ I wish I could upvote this multiple times, thanks for the thorough answer and explanation! \$\endgroup\$jkratz55– jkratz552018年12月31日 15:20:51 +00:00Commented Dec 31, 2018 at 15:20
board
, and there's a function calledNewBoard
. The call stutters. Writingboard.New()
communicates the exact same thing asboard.NewBoard()
without the repetition. check the golang code review wiki for more conventions that are widely adopted. \$\endgroup\$