map介绍和简单使用
map是一种无序的key/value对的集合,在Go语言中,一个map就是一个hash表的引用。map中的key数据类型必须是可以比较的,不然key的相等性无法校验,也就无法hash到哪个链表了。
map的删除key可以用过builtin内置delete方法:
func delete(s map[Type]Type1 key Type)
map要注意的一点,我在写RBAC构建多叉树的时候遇到过,map中的value元素并不是一个变量,我们不能对value元素做取地址操作,这样做的话,编译直接报错。禁止对map的value元素去地址操作原因在于:
类似于slice动态数组,底层数组可能随时会变化。map的hash链表增长也是动态的,所以随着map中元素的增加,链表上存放的value元素存储的空间也会发生动态的变化,导致之前的value元素存放地址是无效的
所以,map也是不能用来直接比较的。
总结,slice动态数组和map哈希表即取即用,但是在大并发时,map的读写需要手动互斥的,不过再Go1.9版本中引入了sync.map标准库对map的访问时并发安全的。
map的迭代顺序是不确定的,因为开始就提及了map是一种无序的key/value对的集合。我们如果要保证输出的时候要是有序的,可以这样做:
var smap map[Type]Value
var keys = make([]Type, 0, len(smap))
for key := range smap {
keys = append(keys, key)
}
sort.Sort(keys)
for _, key := range keys {
fmt.Println(smap[key])
}
首先收集map中的所有key元素,然后对这些key进行排序,然后再遍历key,并获取map中key对应的value元素。保证其输出有序。
这里提到了一个sort标准库:
type Interface interface{
Len() int // 数据长度
Less(i, j int) bool // 大小比较
Swap(i, j int) // 元素交换
}
数据的排序通过获取数据的长度、数据的比较和数据的交换, 就可以实现集合排序。所以我们可以对要排序的数据类型添加这三种行为,就可以使用sort.Sort排序了。这里要注意的地方是,我们可以不直接在Type类型上添加这三个方法。可以定义其他类型,然后把Type数据类型转成定义的类型,就ok。这里举例说明一下:
var keys []Type // 对keys进行排序
type TypeSlice []Type
func (p TypeSlice) Len() int {
xxx
}
func (p TypeSlice) Less(i, j int) bool {
xxx
}
func (p TypeSlice) Swap(i, j int) {
xxx
}
sort.Sort(TypeSlice(keys)) // 这里的sort原型不是func Sort(v []T) []T的原因:v不会动态增长,所以是安全的
明白会用就ok了。
Set集合
Go语言中没有set集合数据类型,它可以使用map来实现:var s map[Type]bool
, 保证集合的数据不重复性。
map中key类型是slice,请绕道走
因为map中的key类型必须是可以比较的,如果想要的key是slice类型,则需要这样绕道走两步:
- 定义一个辅助函数K,将slice转化为map中对应string类型的key,确保x和y相等时,K(x)==K(y)。
- 当每次对map操作时,先用辅助函数K将slice转化为string类型的数据,再操作。
这里有一个关键点:x=y时, K(x)==K(y)为true;当x!=y时,K(x)==K(y)为false。用序列化为byte流再加密取前N个字符,作为string类型的数据就可以了,N个字符大小由一般比较熟悉的算法决定。
结构体
结构体 是一种聚合的数据类型,是由零个或者多个任意类型的值聚合成的实体。
由函数返回结构体参数引出的讨论:
func EmployeeById(id int) *Employee {
...
}
func print() {
fmt.Println(EmployeeById(1234).Position) // "管理员"
EmployeeById(1234).Position = "销售员" // 这个是合理的。虽然是给返回的参数赋值,它确实可以对原实体有修改的影响
}
上面的DEMO,如果把函数原型改为func EmployeeById(id int) Employee
,则编译无法通过。错误在最后一个语句。如果EmployeeById返回参数是一个拷贝的变量,则返回后没有变量名,也就没有任何意义,它会作为垃圾回收处理,我们在《Go圣经-学习笔记之程序结构》有说过,当一个内存地址不再被任何别名引用时,它会被垃圾回收掉。这里你也无法引用它。
由此知道,当返回的参数没有引用其他变量时,则这个新的内存地址没有被任何变量引用,则会被垃圾回收掉, 做赋值操作也就没有任何意义, 所以编译会直接报错。而指针值类型引用了变量,则暂时不会被垃圾回收,修改内存值是由意义的。
结构体嵌入和匿名成员
我们首先看一个圆和一个轮子的结构体定义:
type Point struct {
X, Y int
}
type Circle struct { // 圆的定义
Center Point
Radius int // 半径
}
type Wheel struct { // 轮子的定义
Circle Circle // 这里成员变量名称也可以和类型名相同
Spokes int // 辐条的数量
}
var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
上面Wheel变量赋值是很麻烦的。为了简便赋值操作,Go语言提供了一个特性:声明一个成员对应的数据类型而不指明成员的名字,这类成员就叫匿名成员。匿名成员的数据类型可以是命名的数据类型,也可以是命名的数据类型指针。下面改造下:
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 4
w.Spokes = 20
这样赋值方便简单,代码不冗余, 当我们用字面声明时,需要这样做:
var w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
要注意的一点是,如果在结构体采用匿名成员形式,其中不能有相同的两个匿名成员类型,因为它会有隐式的成员名字,会出现名字冲突。那么为什么我们要在结构体中采用匿名成员呢?
Go语言面向编程思想的核心有一个是组合特性,它通过简单的把匿名成员放在一起,就可以吸纳匿名成员类型所有的方法集,这样就可以实现想要的复杂对象行为,同时也可以修改和管理对象的属性。