map中的key可以是任何的类型,只要它的值能比较是否相等,Go的语言规范已精确定义,Key的类型可以是:
但不能是:
Key类型只要能支持和!=操作符,即可以做为Key,当两个值时,则认为是同一个Key。
我们先看一下样例代码:
**package** main
**import** "fmt"
**type** _key **struct** {
}
**type** point **struct** {
x **int**
y **int**
}
**type** pair **struct** {
x **int**
y **int**
}
**type** Sumer **interface** {
Sum() **int**
}
**type** Suber **interface** {
Sub() **int**
}
**func** (p *pair) Sum() **int** {
**return** p.x + p.y
}
**func** (p *point) Sum() **int** {
**return** p.x + p.y
}
**func** (p pair) Sub() **int** {
**return** p.x - p.y
}
**func** (p point) Sub() **int** {
**return** p.x - p.y
}
**func** main() {
fmt.Println("_key{} == _key{}: ", _key{} == _key{}) // output: true
fmt.Println("point{} == point{}: ", point{x: 1, y: 2} == point{x: 1, y: 2}) // output: true
fmt.Println("&point{} == &point{}: ", &point{x: 1, y: 2} == &point{x: 1, y: 2}) // output: false
fmt.Println("[2]point{} == [2]point{}: ",
[2]point{point{x: 1, y: 2}, point{x: 2, y: 3}} == [2]point{point{x: 1, y: 2}, point{x: 2, y: 3}}) //output: true
**var** a Sumer = &pair{x: 1, y: 2}
**var** a1 Sumer = &pair{x: 1, y: 2}
**var** b Sumer = &point{x: 1, y: 2}
fmt.Println("Sumer.byptr == Sumer.byptr: ", a == b) // output: false
fmt.Println("Sumer.sametype == Sumer.sametype: ", a == a1) // output: false
**var** c Suber = pair{x: 1, y: 2}
**var** d Suber = point{x: 1, y: 2}
**var** d1 point = point{x: 1, y: 2}
fmt.Println("Suber.byvalue == Suber.byvalue: ", c == d) // output: false
fmt.Println("Suber.byvalue == point.byvalue: ", d == d1) // output: true
ci1 := make(**chan** **int**, 1)
ci2 := ci1
ci3 := make(**chan** **int**, 1)
fmt.Println("chan int == chan int: ", ci1 == ci2) // output: true
fmt.Println("chan int == chan int: ", ci1 == ci3) // output: false
}
上面的例子让我们较直观地了解结构体,数组,指针,chan在什么场景下是相等。我们再来看Go语言规范中是怎么说的:
注意:Go语言里是无法重载操作符的,struct是递归操作每个成员变量,struct也可以称为map的key,但如果struct的成员变量里有不能进行==操作的,例如slice,那么就不能作为map的key。
判断两个变量是否相等,首先是要判断变量的动态类型是否相同,在runtime中,_type结构是描述最为基础的类型(runtime/type.go),而map, slice, array等内置的复杂类型也都有对应的类型描述(如maptype,slicetype,arraytype)。
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
...
type chantype struct {
typ _type
elem *_type
dir uintptr
}
...
type slicetype struct {
typ _type
elem *_type
}
...
其中对于类型的值是否相等,需要使用到成员alg *typeAlg(runtime/alg.go),它则持有此类型值的hash与equal的算法,它也是一个结构体:
type typeAlg struct {
// function for hashing objects of this type
// (ptr to object, seed) -> hash
hash func(unsafe.Pointer, uintptr) uintptr
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
}
runtime/alg.go中提供了各种基础的hash func与 equal func,例如:
func strhash(a unsafe.Pointer, h uintptr) uintptr {
x := (*stringStruct)(a)
return memhash(x.str, h, uintptr(x.len))
}
func strequal(p, q unsafe.Pointer) bool {
return *(*string)(p) == *(*string)(q)
}
有了这些基础的hash func与 equal func,再复杂的结构体也可以按字段递归计算hash与相等比较了。那我们再来看一下,当访问map[key]时,其实现对应在runtime/hashmap.go中的mapaccess1函数:
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...
alg := t.key.alg
hash := alg.hash(key, uintptr(h.hash0)) // 1
m := uintptr(1)<<h.B - 1
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize))) // 2
...
top := uint8(hash >> (sys.PtrSize*8 - 8))
if top < minTopHash {
top += minTopHash
}
for {
for i := uintptr(0); i < bucketCnt; i++ {
...
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if alg.equal(key, k) { // 3
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
...
return v
}
}
...
}
}
mapaccess1的代码还是比较多的,简化逻辑如下(参考注释上序列):
到现在我们也就有了初步了解,map中的key访问时同时需要使用该类型的hash func与 equal func,只要key值相等,当结构体即使不是同一对象,也可从map中获取相同的值,例如:
m := make(map[interface{}]interface{})
m[_key{}] = "value"
if v, ok := m[_key{}];ok {
fmt.Println("%v", v) // output: value
}
比较两个map实例需要使用reflect包的DeepEqual()方法。如果相比较的两个map满足以下条件,方法返回true:
Map values are deeply equal when all of the following are true: they are both nil or both non-nil, they have the same length, and either they are the same map object or their corresponding keys (matched using Go equality) map to deeply equal values.
1.两个map都为nil或者都不为nil,并且长度要相等 they are both nil or both non-nil, they have the same length 2.相同的map对象或者所有key要对应相同 either they are the same map object or their corresponding keys 3.map对应的value也要深度相等 map to deeply equal values
凭什么 Go 官方还不支持,难不成太复杂了,性能太差了,到底是为什么?
官方答复原因如下(via @go faq):
核心来讲就是:Go 团队在经过了长时间的讨论后,认为原生 map 更应适配典型使用场景。
如果为了小部分情况,将会导致大部分程序付出性能代价,决定了不支持原生的并发 map 读写。且在 Go1.6 起,增加了检测机制,并发的话会导致异常。
前面有提到一点,在 Go1.6 起会进行原生 map 的并发检测,这是一些人的 “噩梦”。
在此有人吐槽到:“明明给我抛个错就好了,凭什么要让我的 Go 进程直接崩溃掉,分分钟给我背个 P0”。
这里我们假设一下,如果并发读写 map 是以下两种场景:
你会选择哪一种方案呢?Go 官方在两者的风险衡量中选择了第二种。
无论是编程,还是人生。如何在随机性中掌握确定性的部分,也是一门极大的哲学了。
Go 官方团队选择的方式是业内经典的 “let it crash” 行为,很多编程语言中,都会将其奉行为设计哲学。
let it crash 是指工程师不必过分担心未知的错误,而去进行面面俱到的防御性编码。
这块理念最经典的就是 erlang 了。