字符串
字符串是不可变字节(byte)序列,其本身是一个复合结构。
type stringStruct struct{
str unsafe.Pointer
len int
}
头部指针指向字节数组,但没有NULL结尾。默认以utf-8编码存储Unicode字符,字面量里允许使用十六进制,八进制和UFT编码格式。字符串默认值不是nil是“”。
-
使用“ ‘ ” 定义不做转义处理的原始字符串,支持跨行(\n)。
-
支持“!= ,==,< ,>,=,+=”操作符
-
允许以索引访问字节数组(非符号),但不能获取元素地址。
-
以切片语法(起始和结束索引号)返回子串时,其内部依旧指向原字节数组。
转换
要修改字符串,需将其转换为可变类型([]rune或[]byte),待完成后在转换回来。不管怎样转换,都需要重新分配内存,并复制数据。
用append函数,可将string直接追加到[]byte内。
考虑到字符串只读特征,转换是复制数据到新分配内存是可以理解的。
编译器会为某些场合进行专门优化,避免额外分配内存和复制操作:
- 将[]byte 转换为string key,去map[string]查询的时候。
- 将string转换为[]byte,进行for range 迭代时,直接取字节赋值给局部变量。
性能
除类型转化外,动态构建字符串也容易造成性能问题。
用加法操作符拼接字符串时,每次都须重新分配内存。造成性能的浪费。
改进思路就是预先分配足够的内存空间。常用方式是用string.Join函数,会统计所有参数长度,并一次完成内存分配操作。
Unicode
类型rune 专门用来存储Unicode码点(code point),是int32的别名,相当于USC-4/UTF-32编码格式。使用单引号的字面量,其默认类型就是rune。
除了[]rune外,还可以直接在rune, byte,sting间进行转换。
标准库unicode里提供了丰富的操作函数。验证函数外,还可以用RuneCountInString代替len返回准确的Unicode字符数量。
数组
定义数组类型时,数组长度必须时非负整形常量表达式,长度是类型组成部分。元素类型相同,长度不同的数组不属于同一类型。
初始化方式
func main() {
var a [5]int
b := [3]int{12,5}
c := [4]int{1 , 3: 10}
d := [...]int{1,2,3}
e := [...]int{1, 5: 1}
fmt.Println(a,b,c,d,e)
}
对于结构等复合类型,可省略元素初始化类型标签。
func main() {
type user struct {
name string
age string
}
d := [...]user{
{"zz","ll"},
{"jj","14"},
}
fmt.Println(d)
}
在定义多维数组时,仅第一维允许使用[…]
内置函数len()和cap()都返回第一维度长度。
如元素类型支持“== !=”操作符,那么数组也支持该操作。
指针
要分清指针数组和数组指针的区别。指针数组时指元素为指针类型的数组,数组指针式获取数组变量的地址。
func main() {
x , y := 110 , 120
a := [...]*int{&x,&y}
p := &a
fmt.Printf("%T,%v\n",a,a)
fmt.Printf("%T,%v\n",p,p)
}
可获取任意元素地址。数组指针可直接用来操作元素。
复制
Go数组是值类型,复制和传参操作都会复制整个数组数据。
如果需要,可改用指针或切片,以避免数据复制。
切片
切片(slice)本身并非动态数组或数组指针。切片内部通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内。
type slice struyct {
array unsafe.Pointer
len int
cap int
}
切片本身是个只读对象,其工作机制类似于数组指针的一种包装。
初始化
可基于数组或数组指针创建切片,以开始和结束索引位置确定引用的数组片段。不支持反向索引,实际范围是一个右半开区间。
切片名 := 数组名[起始索引:结束索引:最大索引]
// 起始索引省略,就是从数组起始位置开始
// 结束索引省略,就是到数组最后索引结束
// 最大索引省略,就是默认是数组的最大索引。
结构体中的 len = 结束索引-起始索引
cap = 最大索引-起始索引
**属性cap表示切片所引用数组片段的真实长度,len用于限定可读的写元素数量。**数组必须是addressable(可寻址的),否则会引发错误。
和数组一样,切片同样使用索引号访问元素内容,起始索引为0,而非对应的底层数组的真实索引位置。
可直接创建切片对象,无须预先准备数组。因为是引用类型,须使用make函数或显示初始化语句,它会自动完成底层数组内存分配。
func main() {
s1 := make([]int,3,5) //指定len,cap底层数组初始化为零值
s2 := make([]int,3) //省略cap 和len相等
s3 := []int{10,20,5:90}
fmt.Println(s1,len(s1),cap(s1))
fmt.Println(s2,len(s2),cap(s2))
fmt.Println(s3,len(s3),cap(s3))
}
切片类型不支持比较操作,就算元素类型支持也不行,仅能判断是否为nil.
切片可获取元素地址,但不能向数组那样直接用指针访问元素内容。
如果元素类型也是切片可实现类似交错数组功能。
func main() {
x := [][]int{
{1,2},
{10,20,30},
{100},
}
fmt.Println(x[1])
x[2] = append(x[2], 110,120)
fmt.Println(x[2])
}
切片只是很小的结构体对象,用来代替数组传参可避免复制开销。make函数允许在运行期动态指定数组长度,绕开了数组类型必须使用编译器
并非所有时候都适合切片代替数组,因为切片底层数组可能会在堆上分配内存。而且小数组在栈上拷贝的消耗未必就比make大。
reslice
将切片视作[cap]slice数据源,根据此创建新切片对象。不能超出cap,但不受len限制。
新建切片对象依旧指向原底层数组,也就是说修改堆所有关联的切片可见。
可借助replace实现栈数据结构。
func main() {
stack := make([]int, 0, 5)
//入栈
push := func(x int) error {
n := len(stack)
if n == cap(stack) {
return errors.New("stack is full")
}
stack = stack[:n+1]
stack[n] = x
return nil
}
//出栈
pop := func() (int, error) {
n := len(stack)
if n == 0 {
return 0, errors.New("stack is empty")
}
x := stack[n-1]
stack = stack[:n-1]
return x, nil
}
for i := 0; i < 7; i++ {
fmt.Printf("push %d: %v , %v \n", i, push(i), stack)
}
for i := 0; i < 7; i++ {
x, err := pop()
fmt.Printf("pop: %d,%v %v\n", x, err, stack)
}
}
append
向切片尾部(slice[len])添加数据,返回新的切片对象。
数据被追加到原底层数组。如超出cap限制,则为新的切片对象重新分配数组。
注意:
- 是超出底层cap限制,而非底层数组长度限制,应为cap可小于数组长度。
- 新分配数组长度是原cap的2倍,而非原数组的2倍。
- 并非总是2倍,对于较大的切片,会尝试扩容1/4,节约内存。
- 向nil切片追加数据时,会为其分配底层数组内存。
应为存在重新分配底层数组的缘故,在某些场合建议预留足够多的空间,避免中途内存分配和数据复制开销。
copy
在两个切片对象间复制数据,允许指向同一底层数组,允许目标区间重叠。最终复制长度以较短的切片长度(len)为准。
copy函数以第一字符串为拷贝目标,返回值是拷贝了几个字符。
如果切片长时间内引用大数组中很小的片段,那么建议新建切片,复制出所需要数据,以便原数组内存可被及时回收。
字典
字典(哈希表)是一种使用频率极高的数据结构。Go语言将其作为语言内置类型,从运行时层面进行优化,以便获取更高效的性能。
作为无序键值对集合,字典要求key必须时支持相等运算符(==,!=)数据类型。
字典时引用类型,使用make函数或初始化表达语句来创建。
m := make(map[key类型]value类型)
m[key] = value
m2 := map[key类型]value类型{
key : value
}
访问不存在的键值,默认返回零值,不会引发错误。
对字典进行迭代,每次返回的键值次序都不相同。
函数len返回当前键值对数量,cap不支持字典类型。因内存访问安全和哈希算法等原因,字典被设计成“not addressable”(不可访问),故不能直接修改value成员。
正确做法是返回整个value,待修改后在设置字典键值值,或直接用指针类型。
func main() {
m := map[int]user{
1: {"zxt", 18},
}
//设置整个value
u := m[1]
u.age += 1
m[1] = u
fmt.Println(m)
m2 := map[int]*user{
2: &user{"zz", 10},
}
m2[2].age++
fmt.Println(m2[2])
}
不能对nil字典进行写操作,但却内读。内容为空的字典,于nil是不同的。
安全
在迭代期间删除或新增键值是安全的。运行时会对字典并发操作作出检测。如果某个任务正在对字典进行写操作,那么其他任务就不能对该字典执行兵法操作(读,写,删除),否则会导致进程崩溃。
可用sync.RWMutex实现同步,避免读写操作同时进行。
性能
字典对象本身就是指针包装,传参时无须再次取地址。
在创建时预先准备足够空间有助于提升性能,减少扩张时内存分配和重新哈希操作。
m := make(map[int]int,1024) // 预先准备足够的空间
对于海量小对象,应直接用字典存储键值数据拷贝,而非指针。着有助于减少需要扫描的对象数量,大幅度缩短垃圾回收时间。字典不会收缩内存,所以适当替换成新对象时必要的。
结构
结构体(struct)将多个不同类型命名字段(field)序列打包成一个复合类型。
字段名必须唯一,可用“—”补位,支持使用自身指针类型成员。字段名,排列顺序属性类型组成部分。除堆起处理外,编译器不会优化,调整内存布局。
初始化
可按顺序序列化全部字段,或使用命名方式初始化指定字段。
type zxr struct{
name string
age int
}
z := zxt{"ll",18} //按照顺讯序列化全部字段
// 按照命名方式初始化指定指定字段
x := zxt{
name: "ss"
age: 8
}
推荐用命名初始化。这样在扩充结构字段或调整字段顺序是,不会导致初始化错误。
可直接定义匿名结构类型变量。或用作字段类型。但因缺少类型标识,在作为字段类型是无法直接初始化,稍显麻烦。
只有在所有字段类型全部支持是,才可做相等操作。
可以使用指针直接操作结构体字段,但不能是多级指针。
空结构体
空结构体(struct{})是指没有字段的结构体类型。无论是其自身,还是作为数组元素类型,长度都是零。
尽管没有分配数组内存,但依然可以操作元素,对应切片len,cap属性也正常。
这类“长度”为零的对象通常都指向runtime.zerobase。
空结构可作为通道元素类型,用于事件通知。
匿名字段
所谓匿名字段(anonymous field),是指没有名字,仅有类型的字段,也被称为嵌入类型。
从编译器角度看,这只是隐式地一类型名作为字段名字。可直接引用匿名字段的成员,但初始化时必须当作独立字段。
如嵌入其他包中的类型,则隐式字段名字不包括包名。
不仅仅是结构体,除接口指针和多级指针以外的任何命名类型都可作为匿名字段。
未命名类型没有名字标识,自然无法做为匿名字段。
虽然可以想普通字段以牙膏访问匿名字段成员,但会存在重名问题。默认情况下,编译器从当前现实命名字段开始,逐步向内查找匿名字段成语昂。如果匿名字段成员被外层相同名字段遮蔽,那么必须使用显示字段名。
如果多个相同层次的匿名字段成员,就只能泗洪显式字段名访问,因为编译器无法确定目标。
Go语言不是传统面向对象编程语言,或者仅实现了最小面向对象机制。匿名嵌入不是继承,无法实现多态。
字段标签
字段标签(tag)并不是注释,而是用来对字段进行描述的元数据。字段标签不属于数据成员,但却是类型的组成部分。
在运行期,可以用反射获取标签信息。通常被用作格式校验,数据库关系映射。
type user struct{
name string '名字'
sex byte '性别'
}
内存布局
不管结构体包含多少字段,其内存总是一次性分配的,各字段在相邻的地址空间按定义顺序排列。
对于引用类型,字符串和指针,结构内存中只包含其基本(头部)数据。
在分配内存是,字段须做对齐处理,通常以所有字段中最长的基础类型宽度为标准。
参考资料:
《Go语言学习笔记》 雨痕