数据结构进阶(Go语言)

字符串

字符串是不可变字节(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语言学习笔记》 雨痕

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值