为什么这个 goroutine 的行为就像是通过引用调用?

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

我正在尝试学习 Go 的基础知识,但在我测试的代码片段中,我对按值调用和按引用调用之间的区别有点困惑。

我试图解决一个coding game puzzle,其中要计算一个井字游戏字段的解决方案。


我正在使用的代码

因为在学围棋,所以想用一个goroutine来测试井字棋的每一个字段,看看这个字段是不是解,然后把指向这个字段的指针放到一个channel里面获得结果的主要方法。我使用的代码如下所示:

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
}

您可以 运行 它在 go playground

问题

由于代码段中的最后一个函数被声明为 func tryField(b board, field [2]int, result chan *board) 我假设板 b 是一个独立的副本,每次我调用该方法时,因为它是按值调用的。所以改变这个板不应该影响其他 goroutines 中的其他板。但不幸的是,改变一个 goroutine 中的板确实会影响其他 goroutine 中的板,因为程序的输出如下:

input board:
OO.
...
...

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

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

OO.
O..
.O.

如您所见,初始字段在第一行的第一列和第二列有两个 O。向 [1 0] 位置添加一个 O 就像预期的那样工作,但是当向字段 [2 1] 添加一个 O 时,在 [1 0] 处也有一个 O,这是在之前的 goroutine 中添加的,不应该因为它是按值调用的。


问题

为什么我的代码片段中的代码表现得像通过引用调用,尽管该函数不使用指针?

提前致谢!

切片是对数组的引用。在不复制切片的情况下修改切片时,将修改底层数组。因此,指向同一底层数组的所有切片都将看到此更改。