关于 go 切片与数组的三个结论

67 阅读3分钟

关于 go 切片与数组的三个结论

众所周知,在方法中改变字段的值是否对外界生效取决于该方法是值类型还是指针类型

func (s Struct)setX() {……}  // 值方法      无效
func (s *Struct)setY() {……} // 指针方法    生效

同样的,在函数中改变参数是否对外界生效也取决于参数是值类型还是引用类型

i := 1
func setI(I int) { I = 2 }   // 参数是值类型     无效
setI(i)                      // i: 1

j := &i
func setJ(J *int) { *J = 2 } // 参数是引用类型   生效
setJ(j)                      // j: 2

对于 *int 类型的 j 来说,对其重新赋值也会对外界生效。
但对于数组和切片类型来说,事情就不是那样了。

先说说我的结论

  1. 函数中对切片的部分更改会对外界生效
  2. go 数组是值类型,部分更改数组不会反映到外界
  3. 切片底层维护了一个指向数组的指针

然后用代码证明我的结论

结论一:函数中部分更改引用类型值会对外界生效

// 切片 部分更改 生效
s := []int{2: 0}                   // []int [0 0 0]
func fa(S []int) { S[0] = 1 }
fa(s)                              // []int [1 0 0]

但若在函数内部对变量重新赋值就不会对外界生效了
这也是其他语言中经常出现的 “传值” 与 “传引用” 问题

// 切片 重新赋值 无效
s1 := []int{2: 0}                  // []int [0 0 0]
func fb(S []int) { S = []int{} }
fb(s1)                             // []int [0 0 0]

结论一:方法与函数的行为一致,也不会对外界生效
实例中本不应该生效的更改(因为是值方法)却生效了
原因就是在方法中部分更改了引用类型的值

// 数据准备
type s struct{ x []int }
ss := s{[]int{2: 0}}        // main.s {𒀸x:[0 0 0]}

// 值方法 部分更改 但生效了
func (ss s) setX() { ss.x[1] = 1 }
ss.setX()                   // main.s {𒀸x:[0 1 0]}

// 值方法 重新赋值 无效
func (ss s) setY() { ss.x = []int{1} }
ss.setY()                   // main.s {𒀸x:[0 1 0]}

结论二: go 数组是值类型,部分更改数组不会反应到外界

事实上,代码 var arr [3]int = nil 会报错就已经证实了数组是值类型了。因为只有引用类型的空值才能为 nil

但部分更改数组不会反应到外界就有点违反直觉

// 数组 部分更改 无效
arr := [3]int{}                    // [3]int [0 0 0]
func fc(Arr [3]int) { Arr[0] = 1 }
fc(arr)                            // [3]int [0 0 0]

其实想一想,在函数中更改结构体的值也是无法传递到外界的

type s struct{ x int }
func fd(S s) { S.x = 1 }

var ss s                    // main.s {𒀸x:0}
fd(ss)                      // main.s {𒀸x:0}

正是因为数组和结构体的这两条“死规矩”,才能将值类型与指针类型真正的区分开。这样方法的意图更明显,调用者也更省心

顺带一提,在 java 的方法内部部分更改数组也会生效。因为数组也属于复杂类型

import static java.lang.System.out;

public class a {
    public static void main(String[] args) {
        // 创建数组 -> 0 0 0
        var arr = new int[3];
        for (int i : arr) out.print(i + " ");

        out.println();

        // 部分更改后 -> 1 0 0
        setArr(arr);
        for (int i : arr) out.print(i + " ");
    }

    public static void setArr(int[] arr) {
        arr[0] = 1;
    }
}

结论三:切片底层维护了一个指向数组的指针
创建了一个“指向”数组的切片。部分更改数组后切片的值也变了

a2 := [3]int{}
s2 := a2[:]

a2; s2              // [3]int [0 0 0]; []int [0 0 0]
a2[0] = 1; 
a2; s2              // [3]int [1 0 0]; []int [1 0 0]

可以试试 append 这个切片(会因为数据超长而扩容)

扩容后两者之间就没有关联了,对任意一方的更改就不会同步到另一方了