数组和切片

type Array struct {
    len  int64
    elem Type
}

type Slice struct {
    elem Type
}

// Type表示Go的类型
// 所有类型都实现Type接口。
type Type interface {
    // Underlying 返回类型的基础类型
    Underlying() Type

    // String 返回类型的字符串表示形式
    String() string
}
  • 共同点:都属于集合类的类型,可以存储某一种类型的值;应用索引表达式得到值,应用切片表达式得到新切片

  • 不同点:数组长度固定,切片长度可变

数组的长度必须在声明时给定,且不可变,它是数组类型的一部分

切片的类型字面量中,只有元素类型,长度随着元素数量增长而增长,但不会减小

切片可以看成是对数组的一层简单封装,在每个切片的底层数据结构中,一定会包含一个数组。

  • 数组:是切片的底层数组

  • 切片:是对数组的某个连续片段的引用,切片的容量代表了其可见底层数组的长度,切片的底层数组长度不可变

关于数组和切片,golang官方博客有文章详细说明,点击这里arrow-up-right。其实这里说的已经很清楚了,论好好阅读官方说明的重要性。

引用类型与值类型

引用类型:

  1. 切片类型(切片是对数组某个连续片段的引用)【相对于slice底层的数组而言,其实slice是一个结构体类型(也就是值类型)】

  2. 字典类型

  3. 通道类型

  4. 函数类型

值类型:

  1. 基础数据类型

  2. 结构体类型

  3. 数组类型

  4. 切片类型【相对于slice底层的数组而言,其实slice是一个结构体类型(也就是值类型)】

Go语言中不存在“传值或传引用”的问题,在Go语言中只要看被传递的值的类型,如果被传递的值是引用类型,那就是“传引用”,如果被传递的值是值类型,那就是“传值”。从传递成本的角度,引用类型的值比值类型的值成本低很多。

切片注意点

从上面代码和输出结果(注释部分)可以看出:

  1. changeSlice()函数对外部slice生效了

  2. appendSlcie()函数对外部没有生效

Go中只有值传递,所有的引用传递都是直接把对应的指针拷贝过去了,所以修改能直接在原对象生效。

很多地方都说slice是引用类型(这是相对于slice底层的数组而言的),其实slice是一个结构体类型(也就是值类型)。

因为slice其实是一个结构体而不是一个引用,要让appendSlice生效,只要传入引用就可以,代码修改如下:

内建函数

  • len():得到数组或切片的长度

  • cap():得到数组或切片的容量

  • make(): 创建切片、字典、通道等

  • append(): 切片中追加值

数组容量永远和长度相等,且不可变

len

官方标准库中的描述:

For some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification's "Length and capacity" section for details.

当参数是字符串字面量和简单 array 表达式,len 函数返回值是常量,这很重要。后半句更重要。

内置函数 len 和 cap 获取各种类型的实参并返回一个 int 类型结果。实现会保证结果总是一个 int 值。

  • 如果 s 是一个字符串常量,那么 len(s) 是一个常量 。

  • 如果 s 类型是一个数组或到数组的指针且表达式 s 不包含通道接收或(非常量的)函数调用的话,那么表达式 len(s)cap(s) 是常量;这种情况下,s 是不求值的。否则的话, len 和 cap 的调用结果不是常量且 s 会被求值。

切片与底层数组

切片表达式是一个开区间,得到的新切片的容量和长度为区间的差值。新切片的起始值为原切片或数组对应的索引值。切片无法向左扩展,但是可以向右扩展。

当切片无法容纳更多元素时,Go语言会进行扩容,不会改变原切片,而是创建一个容量更大的新切片,将原来的元素和新的元素一起拷贝到新的切片中。一般情况扩容为原来的2倍,当原切片的长度大于等于1024后,一次增长1.25倍的方式逐渐扩容。

切片在扩容时,创建了新的切片和新的底层数组,原来的切片和底层数组没有任何改动。

  1. 在底层数组容量(即切片容量)足够的情况下append()函数返回的是指向原底层数组的切片

  2. 在底层数组容量(即切片容量)不够的情况下append()函数返回的是指向新底层数组的新切片

可寻址与不可寻址

看一下crypto/sha1库:

sha1.Sum()返回一个长度是20的数组,而不是切片(如果是切片不会报错)。

Go是返回数值的,所以这里是20字节的数组,而不是指向它的指针。

不可寻址

大多数匿名值都不可寻址(复合字面值是一个例外)。

在上面的代码中,sha1.Sum() 的返回值是匿名的,因为我们立即对其进行了切片操作。如果我们将它存在变量中,并因此使其变为非匿名,就是可寻址的,则该代码不会报错,如下所示。

因为对数组进行切片操作要求该数组是可寻址的sha1.Sum() 返回的匿名数组是不可寻址的,因此对其进行切片会被编译器拒绝。

如果在这里允许对不可寻址的匿名值进行切片操作,那么 Go 要默默地实现堆存储以容纳 sha1.Sum() 的返回值(然后将该值复制到另一个值),该返回值将一直存在直到那个切片被回收。

注意:Go 语言规范中的许多内容要求或仅对可寻址的值适用。例如,大多数赋值操作需要可寻址性。

方法调用

假设有一个类型 T,并且在 *T 上定义了一些方法,例如 *T.Op()。就像 Go 允许在不取消引用指针的情况下进行字段引用一样,可以在非指针值上调用指针方法:

并发访问切片

由于 slice或者map 是引用类型,golang 函数是传值调用,所用参数副本依然是原来的 slice或者map, 并发访问同一个资源会导致竞态条件。

真实的输出并没有达到我们的预期,len(slice) < n

slice是对数组一个连续片段的引用,当 slice 长度增加的时候,底层的数组会被换掉。当在换底层数组之前,切片同时被多个 goroutine 拿到,并执行 append 操作。那么很多 goroutine 的 append 结果会被覆盖,导致 n 个 gouroutine append 后,长度小于n。

go 1.9 增加sync.map实现并发安全,slice咋整?

使用互斥锁

优点是比较简单,适合对性能要求不高的场景。

通道串行化

实现相对复杂,优点是性能很好,利用了channel的优势。

最后更新于