Search code examples
gogoroutine

Why does this goroutine behave like it's call by reference?


I'm trying to learn the basics of Go and I'm a bit confused about the difference between call by value and call by reference in a code snippet I tested.

I tried to solve a coding game puzzle in which a solution for a tic-tac-toe field is to be calculated.


The code I'm using

Because I'm learning Go, I wanted to use a goroutine to test every field of the tic-tac-toe board, check whether this field is the solution and then put a pointer to this field in a channel for the main method to have the result. The code I used looks like this:

package main

import "fmt"
import "os"

var player int = int('O')
var opponent int = int('X')
var empty int = int('.')

type board struct {
    fields [][]int
}

func main() {
    lines := [3]string {"OO.", "...", "..."}

    var b board
    b.fillBoard(lines)
    fmt.Fprintln(os.Stderr, "input board:")
    b.printBoard(true)

    resultChannel := make(chan *board)
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            go tryField(b, [2]int{i, j}, resultChannel) // goroutine call that isn't working as expected
        }
    }

    fmt.Fprintln(os.Stderr, "\nresult:")
    for i := 0; i < 9; i++ {
        resultBoard := <- resultChannel
        if (resultBoard != nil) {
            resultBoard.printBoard(false)
            return
        }
    }
    
    // fmt.Fprintln(os.Stderr, "Debug messages...")
    fmt.Println("false")// Write answer to stdout
}

func tryField(b board, field [2]int, result chan *board) {
    b.printBoard(true)
    fmt.Fprintln(os.Stderr, "add O to field: ", field)
    fmt.Fprint(os.Stderr, "\n")
    if (b.fields[field[0]][field[1]] != empty) {
        result <- nil
    }

    b.fields[field[0]][field[1]] = player
    if (b.isWon()) {
        result <- &b
    } else {
        result <- nil
    }
}


func (b *board) fillBoard(lines [3]string) {
    b.fields = make([][]int, 3)
    for i := 0; i < 3; i++ {
        b.fields[i] = make([]int, 3)
    }

    for i, line := range lines {
        for j, char := range line {
            b.fields[i][j] = int(char)
        }
    }
}

func (b *board) printBoard(debug bool) {
    var stream *os.File
    if (debug) {
        stream = os.Stderr
    } else {
        stream = os.Stdout
    }
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            fmt.Fprint(stream, string(b.fields[i][j]))
        }
        fmt.Fprint(stream, "\n")
    }
}

func (b *board) isWon() bool {
    for i := 0; i < 3; i++ {
        rowFull := true
        colFull := true
        for j := 0; j < 3; j++ {
            rowFull = rowFull && b.fields[i][j] == player
            colFull = rowFull && b.fields[j][i] == player
        }
        if (rowFull || colFull) {
            return true
        }
    }

    diagonal1Full := true
    diagonal2Full := true
    for i := 0; i < 3; i++ {
       diagonal1Full = diagonal1Full && b.fields[i][i] == player
       diagonal2Full = diagonal2Full && b.fields[i][2-i] == player
    }

    if (diagonal1Full ||diagonal2Full) {
        return true
    }

    return false
}

You can run it in the go playground.

The problem

Since the last function in the snippet is declared as func tryField(b board, field [2]int, result chan *board) I assume the board b to be an indipendent copy, each time I call the method, because it's call by value. So changing this board should not affect the other boards in the other goroutines. But unfortunately changing the board in one goroutine does affect the boards in the other goroutines as the output of the programm is the following:

input board:
OO.
...
...

result:
OO.
...
...
add O to field: [1 0]

OO.
O..
...
add O to field: [2 1]

OO.
O..
.O.

As you can see the initial field has two O's at the first and the second col in the first line. Adding an O to the position [1 0] works like expected, but when adding an O to the field [2 1] the there is also an O at [1 0], which was added in the previous goroutine and shouldn't be there since it's call by value.


The question

Why does the code in my snippet behave like it's call by reference although the function doesn't use a pointer?

Thanks in advance !


Solution

  • Slices are references to arrays. When modifying a slice without copying it, the underlaying array will be modified. Therefore, all slices that point to the same underlaying array will see this change.