4.1 数组

当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。
因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。在这个方面,Go语言对待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数

4.2 Slice

数组与Slice的差异

  • 外观:方括号里面有没有东西
    1. 数组必须定长,必须告诉编译器有多大,或者让编译器自己去数
      比如:[3]int{1,2,3},[...]int{1,2,3,4}

    只要方括号里面有‘东西’,就是数组

    1. 方括号里面是空的,就是切片
  • 切片:隐式创建数组
    当创建一个切片时,s := []int{1, 2, 3},编译器做了两件事
    1. 在内存里创建一个长度为3的匿名数组,里面存了[1,2,3]
    2. 创建一个切片结构体,让s的指针指向那个匿名数组的第一个元素

切片不能用==比较

  1. 递归引用
    一个Slice甚至可以包含自身,切片是一个引用类型,比如切片A里面有个元素是切片B,切片B里面又有一个元素是切片A
    如果GO允许使用==的话,程序就会无限的递归循环
  2. 安全性
    • 如果Slice支持 == (深度比较)
      底层的元素可能被修改,比如把切片s当作key存入一个map,修改s一个元素,此时s的hash值也会发生变化,再也找不到对应的key,map的内部结构崩坏
    • 如果Slice支持 == (浅比较,只比较指针地址)
      这会有不一致性问题,因为数组的 == 是比内容,而切片的 == 却是比地址,所以go直接禁止了切片的==操作

怎么解决?

  • 标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较,自己写一个 equal 函数来循环比较,或者使用 bytes.Equal(仅限字节切片)。 注:在 Go 1.21+ 版本中,官方新增了 slices.Equal 函数,终于不用自己手写了。

  • Slice唯一合法的比较-和nil比较
    nil slice和 empty slice的区别
    声明: nil: var s []int || s = nil
    s := []int{} || make([]int, 0)
    底层:nil slice没有底层数组,而empty有底层数组,尽管为空

    1
    2
    nil slice == nil //true
    empty slice == nil //false
  • 如果要判断一个切片是不是空,不要用if s == nil,而要用if len(s) == 0,这样可以覆盖nil切片和空切片两种情况

Map

K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在

虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法

  • 在数学中,0.1+0.20.1 + 0.2 肯定等于 0.30.3
    但在计算机二进制中,浮点数往往是对小数的近似表示。如果有一点的误差,存进去的数据就取不出来了
  • NaN 不等于任何数,包括它自己,把 NaN 当作 Key 存进 Map 时,哈希表找到了对应的key,但是比对会失败,这个数据会永远的失联

struct

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员

结构体嵌入与匿名成员

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。

1
2
3
4
5
6
7
8
9
type Circle struct {
Point
Radius int
}

type Wheel struct {
Circle
Spokes int
}

我们可以直接访问叶子属性而不需要给出完整的路径:

1
2
3
4
5
var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20

结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:

w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}

w.X = 42

fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
  • 匿名成员也有一个隐式的名字,所以不能同时包含两个相同类型的匿名成员,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员,但是像注释中的那种长形式(w.circle.point.X)会在外部包中被禁止,因为你无法访问到circle和point
1
w.X = 8 // equivalent to w.circle.point.X = 8