Dawn's Blogs

分享技术 记录成长

0%

GO语言圣经学习笔记 (3) map和结构体

Map

GO语言中,map是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value

一个map就是一个哈希表的引用,所以map作为函数的参数时,是引用传递的。map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。

内置的delete函数可以删除元素(元素不在map中也没关系):

1
delete(hashMap, "dawn")	// 从map中移除key为dawn的元素

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作。禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效:

1
p := &hashMap["dawn"]	// error

和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。

遍历顺序随机

map遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序

1
2
3
4
5
6
7
8
9
10
11
var names []string

for name := range ages {
names = append(names, name)
}
// 显式的对key排序
sort.Strings(names)
// 依据顺序的key对map顺序遍历
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}

判断元素是否存在

通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值。如果需要区分已存在的零值和不存在的零值,可以进行以下操作,第二个布尔值ok用于报告元素是否真的存在

1
2
3
if age, ok := ages["dawn"]; !ok {
// ...
}

绕过key必须是可比较的限制

有时候需要map的key是slice类型,但是map的key必须是可比较的,可以通过两步来绕过此限制:

  • 定义一个函数k,将slice转为string类型,作为map的key来使用
  • 创建一个key为string的map,在每次对map操作时先用k辅助函数将slice转化为string类型
1
2
3
4
5
6
7
func k(list []string) string {	// 将slice转为string
return fmt.Sprintf("%q", list)
}

var hashMap = make(map[string]int)
keySlice := []string{/* ... */}
hashMap[k(keySlice)]++ // 对map操作时先将slice转为string

使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。

结构体

结构体是一种聚合数据类型,结构体类型的零值是每个成员都是零值。

同时结构体在作为函数参数时默认是值传递的,也就是说函数中使用的是结构体的拷贝

结构体的比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。在相等比较时,将比较两个结构体的每个成员

所以,可比较的结构体可以作为map的key类型

1
2
3
4
5
6
type address struct {
hostname string
port int
}

counts := make(map[address]int) // address结构体作为key

结构体嵌入和匿名成员

一个结构体可以嵌入另一个结构体中,使得结构体的类型变得清晰了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 坐标点
type Point struct {
X, Y int
}
// 圆
type Circle struct {
Center Point // 圆心
Radius int // 半径
}
// 轮子
type Wheel struct {
Circle Circle
Spokes int // 径向辐条的数量
}

但是这会使得访问每个成员变得繁琐:

1
2
3
4
5
var w Wheel
w.Circle.Center.X = 1
w.Circle.Center.Y = 1
w.Circle.Radius = 2
w.Spokes = 10

所以可以使用匿名成员:

1
2
3
4
5
6
7
8
type Circle struct {
Point // 匿名结构体
Radius int
}
type Wheel struct {
Circle // 匿名结构体
Spokes int
}

得意于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

1
2
3
4
5
var w Wheel
w.X = 1
w.Y = 1
w.Radius = 2
w.Spokes = 10d

简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法