数组

数组是固定长度的特定类型元素组成的一个序列,在 Go 中对于数组的定义方式

  1. var a [3]int // 长度为3的 int 类型数组,元素全部是 0
  2. var b = [...]int{1,2,3} // 长度为 3 的 int 型数组,元素为 1,2,3
  3. var c = [...]int{2:3,1:2} // 长度为 3 的int 型数组,元素为 0,2,3
  4. var d = [...]int{1,2,4:5,6} // 长度为 6 的 int 型数组,元素为 1,2,0,0,0,6

第一种是基本定义方式,也是最常用的一种,长度明确,数组中每个元素以零值初始化
第二种是定义数组,在定义的时候顺序指定全部元素的初始值,数组的长度根据初始化元素的数目自动计算
第三种是以索引的方式来初始化数组的元素,因此元素的初始化值出现的位置就相对随意,数组长度以出现的最大索引值为准,也就是 2,2:3 表示索引为 2 的值是 3
第四种其实能理解第三种就可以理解第四种了,效果是一样的,混合了第二种和第三种的姿势初始化,表示索引下标是 4 的值为 5,然后后面还有一个 6 ,所以长度一共是 6 个长度
Go 中的数组是一个值语义的存在,一个数组变量就是整个数组,并不是像 C 一样隐式的指向第一个元素,而是一个完整的值,所以当一个数组被赋值或者被传递的时候,实际是复制整个数组的
在 Go 中数组类型是字符串和切片等结构的基础。对于数组的很多操作,比如 for…range 都可以应用到字符串或者切片中

字符串

一个字符串是一个不可改变的字节序列,字符串通常用来包含人类可读的文本数据,和数组不一样的是字符串的元素不可以被修改,是一个只读的字节序列。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。由于 Go 的源代码要求是 UTF8 编码,导致 Go 源代码中出现的字符串常量一般也是 UTF8 编码的。源代码中的文本字符串通常被解释为采用 UTF8 编码的 Unicode 码点序列,因为字节序列对应的只是只读的字节序列,所以字符串可以包含任意的数据,包括字节值 0,也可以用字符串表示 GBK 等非 UTF8 编码的数据,不过这个时候把字符串看作是一个只读的二进制数组更加准确,因为 for range 等语法并不能支持非 UTF8 编码的字符串遍历
Go 中字符串的地层结构在 reflect.StringHeader 中定义

  1. type StringHeader struct {
  2. Data uintptr
  3. Len int
  4. }

字符串由两个信息组成,一个是指向的底层字节数组;第二个是字符串的字节的长度。字符串其实是一个结构体,因此字符串的赋值操作也就是 reflect.StringHeader 结构体的复制过程,并不会涉及到底层字节数组的复制,本身 string 类型定义的字符串数组和 reflect.StringHeader 定义的字符串数组对应的底层结构是一样的,所以可以将字符串数组看作一个结构体数组
一个字符串的底层内存结构如下
hello,world

  1. data -> hello, world
  2. len -> 12

可以发现,这个字符串底层数据和以下的数组是完全一致的

  1. var data =[...]byte {
  2. 'h','e','l','l','o',',',' ','w','o', 'r', 'l', 'd'
  3. }

字符串虽然不是切片,但是支持切片操作,不同位置的切片底层访问的是同一块内存数据(因为字符串是只读的,所以相同的字符串面值常量通常对应同一个字符串常量)

  1. s := "hello, world"

字符串和数组相似,内置的 len() 函数返回字符串的长度,也可以通过 reflect.StringHeader 结构访问字符串的长度,所以可以直接用哪个下面这种姿势去访问字符串的长度

  1. fmt.Println("len(s): ",(*reflect.StringHeader)(unsafe.Pointer(&s)).Len) //12

又由于 Go 中的源文件都是用 UTF8 的编码,所以 Go 中源文件出现的字符串面值常量一般也是 UTF8 编码的。
编译期间存在的字符串会被直接分配到只读的内存空间并且这段内存不会被更改,但是在运行时我们其实还是可以将这段内存拷贝到其他的堆或者栈上,同时将变量的类型修改成 []byte 在修改之后再通过类型转换变成 string,不过如果想要直接修改 string 类型变量的内存空间,Go 语言是不支持这种操作的。
所以 go 的字符串是只读的,但是修改的时候会转换成 []byte 数组的形式去做修改,然后再转换成 string 返回

切片

切片是一个简化版的动态数组,因为动态数组的长度不固定,所以切片的长度自然就不能是类型的组成部分了。数组虽然有适用的地方,但是不够灵活,切片相比于数组用得更多,看看切片的数据结构

  1. type SliceHeader struct{
  2. Data uintptr
  3. Len int
  4. Cap int
  5. }

相比于字符串的结构,新增了 Cap 属性来表示切片指向的内存空间的最大容量(对应的是元素个数,不是字节数)
定义姿势

  1. var (
  2. a []int // 空切片,和 你来相等
  3. b = []int{} // 空切片,和 nil 不想等
  4. c = []int{1,2,3}
  5. d = c[:2]
  6. e = c[0:2:cap(c)] //有 2 个元素的切片,len 为 2,cap 为 3
  7. g = make([]int,3) // 三个元素的切片,len 和 cap 都是 3
  8. )

和数组一样,内置的 len() 函数返回的切片中有效元素的长度,内置的 cap() 函数返回的切片容量大小,容量必须大于或者等于切片的长度
对于切片的访问,当切片作为一个参数或者对切片本身进行赋值的时候,和数组指针的姿势比较类似,是一个复制切片头信息的操作,不会复制底层的数据

删除切片元素

当删除切片中元素的时候,为了节省空间,防止再次分配新的内存空间用于切片删除,所以可以直接原地删除,使用 append 方法实现

  1. a = []int{1,2,3}
  2. a = a[:len(a)-1] // 删除尾部 1 个元素
  3. a = a[:len(a) - N] //删除尾部 N 个元素

上述是通过挪动数据指针的方式实现,也可以像这样

  1. a = []int{1,2,3}
  2. a = append(a[:0],a[1:]...) //删除开头 1 个元素

上述的方法就是使用 append 在原地直接完成,不需要新分配空间,也不需要做整体挪动
切片的高效操作只要是要降低内存分配的次数,尽量保证 append() 操作不会超出 cap 的容量,降低触发内存分配的次数和每次分配的内存大小

避免切片内存泄漏

切片的操作不会复制底层的数据每层的数据会被保存在内存中,直到它不再被引用。但是有时候会因为一个小的内存引用导致底层整个数组被处于使用的状态,这会延迟垃圾回收器对底层数组的回收
比如下面的这个姿势,这个函数加载整个文件到内存,然后只是匹配出现的电话号码,然后以切片的方式返回

  1. func FindPhone(filename string)[]byte{
  2. b,_ := ioutil.ReadFile(filename)
  3. return regexp.MustCompile("[0-9]+",Find(b))
  4. }

这个操作会倒是整个文件被加载到内存很长一段时间不会被释放,解决这个问题应该将其与原数据的关系切断,将它复制到一个新的切片中返回

  1. func FindPhone(filename string)[]byte{
  2. b,_ := ioutil.ReadFile(filename)
  3. b= regexp.MustCompile("[0-9]+",Find(b))
  4. return append([]byte{},b...)
  5. }

在切片类型是指针的时候也会遇到这个问题

  1. var a []*int{...}
  2. a = a[:len(a) -1] // 删除最后一个元素,但是最后一个元素还是在被引用,导致没有办法很快释放

正确的做法是,在删除前将需要删除的元素设置成 nil

  1. a[:len(a) -1] = nil
  2. a = a[:len(a) -1]

然后再删除
Go 中实现的非 0 大小数组的长度不能超过 2GB,因此需要针对数组的元素的类型大小计算数组的最大长度范围([] int8 最大是 2GB,[]uint16 最大是 1GB,但是 []struct{} 数组的长度可以超过 2GB)