go语言-数组、字符串和切片

Go语言中数组、字符串和切片三者是密切相关的数据结构。这3种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。
Go语言的数据是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。
Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导师底层数据的复制。
切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数的传参时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。
一、数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。因为数组长度是数组的一部分,不同长度或不同类型的数据组成的都是不同的类型,所以在Go语言中很少直接使用数组。和数组对应的类型是切片,切片是可以动态增长和收缩的序列,切片的功能也更灵活。
数组定义方式:

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

第一种方式是定义一个数组变量的最基本的方式,数组长度明确指定,数组中的每个元素都以0值初始化
第二种方式是定义数组,可以在定义的时候顺序指定全部元素的初始化值,数组的长度根据初始化元素的数目自动计算
第三种方式是以索引的方式来初始化数组的元素,因此元素的初始化值出现顺序比较随意。这种初始化方式和map[int]Type类型的初始化语法类似。数组的长度以出现的最大索引为准,没有明确初始化的元素依然用0值初始化
第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三个和第四个元素以0值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。
Go语言数组是值定义。一个数组变量即表示整个数组,它并不隐式的指向第一个元素的指针,而是一个完整的值。

var a = [...]int{1, 2, 3}    //a是一个数组
var b = &a                   //b是指向数组的指针
fmt.Println(a[0], a[1])      //打印数组的前两个元素
fmt.Println(b[0], b[1])      //打印数组指针访问数组元素的方式和通过数组类似

for i, v := range b {        //通过数组指针迭代数组的元素
    fmt.Println(i, v)
}

其中b是指向数组a的指针,但是通过b访问数组中元素的写法和a是类似的。还可以通过for range 来迭代数组指针指向的数组元素。
可以将数组看做一个特殊的机构体,结构的字段名对应数组的索引,同时结构体成员的数目是固定的。内置函数len()可以用于计算数组的长度,cap()函数用于计算数组的容量。
数组不仅可以定义数值数组,还可以定义字符串数组、结构体数组、函数数组、接口数组、通道数组等
二、字符串
一个字符串是一个不可改变的字节序列,和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。字符串的结构有两个信息组成:第一个是字符串指向的底层字节数组;第二个是字符串的字节长度。以下是go语言源码中对string类型的说明:

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string

字符串是8位字节字符的集合,默认以utf-8编码,可以是空的但是不能是nil,字符串是只读的。
字符串虽然不是切片,但是支持切片操作,不同位置的切片底层访问的是同一块内存数据

s := "hello, world"
hello := s[:5]
world := s[7:]

字符串和数组类似,内置的len()函数返回字符串的长度
三、切片
切片(slice)是一种简化版的动态数组。
切片的定义方式:
var (
a [] int                 //nil切片,和nil相等,一般用来表示不存在的切片
b = []int{}            //空切片,和nil不相等,一般用来表示一个空的集合
c = []int{1, 2, 3}  //有3个元素的切片,len和cap都为3
d = c[:2]             //有2个元素的切片,len为2,cap为3
e = c[0:2:cap(c)] //有2个元素的切片,len为2,cap为3
f = make([]int,3)  //有3个元素的切片,len和cap都为3
)
和数组一样,内置的len()函数返回切片中有效元素的长度,内置的cap()函数返回切片容量的大小,容量必须大于或等于切片的长度。

  1. 添加切片元素
    内置的泛型函数append()可以在切片的尾部追加N个元素:
var a []int        
a = append(a, 1)     //追加一个元素
a = append(a, 1, 2, 3) //追加多个元素,手写解包方式
a = append(a, []int{1, 2, 3}...) //追加一个切片,切片需要解包

注意,在容量不足的情况下,append()操作会导致重新分配内幕才能,可能导致巨大的内存分配和复制数据的代价。及时容量足够,依然需要用append()函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。
除了在切片尾部追加,还可以在切片的开头添加元素:

var a = []int{1, 2, 3}
a = append([]int{0}, a...)
a = append([]int{-3, -2, -1}, a...)

在开头一般会导致内存的重新分配,而且会导致已有的元素全部复制一次,因此,从切片开头添加元素的性能一般要比从尾部追加元素的性能差很多。
由于append()函数返回新的切片,也就是它支持链式操作,因此我们可以将多个append()操作组合起来,实现在切片中间插入元素:

var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...)   //在第i个位置插入x
a = append(a[:i], append([]int{1, 2, 3}, a[i:]...)...)  //在第一个位置插入切片

用copy()和append()组合可以避免创建中间的临时切片,同样完成添加元素的操作:

a = append(a, 0)        //切片扩展一个空间
copy(a[i+1], a[i:])     //a[i:]向后移动一个位置
a[i] = x                //设置新添加的元素

操作语句虽然冗余了一些,但是相比之前的方法可以减少中间创建的临时切片。
2.删除切片元素

根据要删除的元素的位置,有从开头位置删除、从中间位置删除和从尾部删除3种情况,其中删除切片尾部的元素最快:

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

删除开头的元素可以直接移动数据指针:

a = []int{1, 2, 3}
a = a[1:]            //删除开头1个元素
a = a[N:]            //删除开头N个元素

删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append()或copy()原地完成

a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) //删除中间1个元素
a = append(a[:i], a[i+N:]...) //删除中间N个元素

a = a[:i+copy(a[i:], a[i+1:])] //删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] //删除中间N个元素