Go_读书笔记_01
数组、字符串和切片
go语言数组、切片和字符串三者的关系
# 1. 在底层原始数据有着相同的内存结构;在上层,因为语法的限制而有着不同的行为表现。
# 2. 数组:一种值类型;
# 虽然数组的元素可以被修改,但数组本身的赋值和函数传参都是以整体复制的方式处理的。
# 3. 字符串:
# 底层数据也是对应的字节数组,但字符串的只读属性禁止了在程序中对底层字节数组元素的修改。
# 字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。
# 4. slice(切片)
# 切片的结构和字符串结构类似;但解除了只读限制。
# 切片的底层数据虽然也是对应数据类型的数组,但每个切片还有独立的长度和容量信息,切片赋值和函数传参时也是将切片透信息
# 部分按值传递处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。
其实go语言的赋值和函数传参规则很简单,除闭包函数以引用的方式对外部变量访问之外,其它赋值和函数传参都以传值的方式处理。
数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的一部分,不同长度或不同类型组成的数组都是不同的类型。
// 数组的定义方式
var a [3]int // 定义长度为3的int数组,元素全部为0
var b =[...]int{1,2,3} // 定义长度为3int数组
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}
go语言中数组是值语义。一个数组变量即表示整个数组。
可以将数组看作一个特殊的结构体,结构体的字段名对应数组的索引,同时结构体成员的数目是固定的(即len())。内置函数len()
可用于计算数组的长度,cap()
可用于计算数组的容量。
// 常见的遍历数组的方式
for i:=rang a {
fmt.Printf("a[%d] -->%d \n",i,a[i])
}
for i,v:=range b {
fmt.Printf("b[%d] -->%d \n",i,v)
}
for i:=0;i<len(c);i++ {
fmt.Printf("c[%d] -->%d \n",i,c[i])
}
用for- range
方式遍历数组,性能可能会好一些,因为这种遍历保证不会出现数组越界的情况,每一轮迭代对数组元素的访问时可以省去对下标越界的判断。
字符串
一个字符串是一个不可改变的字节序列,字符串通常用来表示人类可读的文本数据。
字符串的元素不可修改,是一个只读的字节数组。go语言的源代码中文本字符串通常被解释为采用了UTF8
编码的Unicode码点(rune)
序列。
go语言字符串的底层结构在reflect.StringHeader
中定义:
type StringHeader struct{
Data uintptr // 指向字符串的底层字节数组
Len int // 字符串的字节的长度
}
字符串其实是一个结构体,因此字符串的赋值操作也就是refect.Stringheader
结构体的复制过程,并不会涉及底层字节数组的复制。
字符串虽然不是切片,但是支持切片操作,不同位置的切片底层访问的是同一块内存数据(因为字符串是只读的,所以相同的字符串面值常量通常对应同一个字符串常量)
s :="hello,world"
hello :=s[:5]
world :=s[7:]
切片
**切片(slice)**就是一种简化版的动态数组。
切片的结构定义,reflect.SliceHeader
type SliceHeader struct {
Data uintptr // 切片指向的底层数组
Len int // 表示切片的长度, len<=cap
Cap int // 表示切片指向的内存空间的最大容量
}
切片的定义方式:
var (
a []int // nil切片, 和nil相等,一般用来表示一个不存在的切片
b =[]int{} // 空切片,和nil不相等,一般用来表示一个空的集合
c =[]int{1,2,3} // 有三个元素的切片, len = cap =3
d =c[:2] // 有两个元素的切片, len=2 cap =3
e =c[0:2:cap(c)] // 有两个元素的切片, len=2 cap =3
f =c[:0] // 有0个元素的切片,len=0 cap=3
g =make([]int,3) // 有3个元素的切片,len = cap = 3
h =make([]int,2,3) // 有2个元素的切片,len = 2 cap = 3
i =make([]int,0,3) // 有0个元素的切片,len = 0 cap = 3
)
添加切片元素
内置的泛型函数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...) // 在开头添加一个切片
在开头一般都会导致内存的重新分配,而且会导致已有的元素全部重新复制一次。因此在头追加元素的性能要比尾部追加性能差很多。
删除切片的元素
根据删除元素的位置,分为头删除、中间删除、尾部删除等情况。其中尾部删除性能最佳。
a =[]int{1,2,3}
a =a[:len(a)-1] // 删除尾部1个元素
a =a[:len(a)-N] // 删除尾部n个元素
删除开头元素,可以采用直接移动数据指针的方式:
a =[]int{1,2,3,4}
a =a[1:] // 删除开头1个元素
a =a[N:] // 删除开头N个元素
删除开头的元素也可以不移动数据指针,而将后面的数据向开头移动,可以用append()
原地完成,(指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)
a =[]int{1,2,3,4}
a =append(a[:0],a[1:]...) // 删除开头1个元素
a =append(a[:0],a[N:]...) // 删除开头N个元素
也可以用copy()
完成删除开头的元素:
a =[]int{1,2,3,4}
a =a[:copy(a,a[1:])] // 删除开头1个元素
a =a[:copy(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个元素
切片高效操作的要点:要降低内存分配的次数,尽量保证append()
操作不会超过cap
的容量,降低触发内存分配的次数和每次分配内存的大小。