go 语言中 map 的相关知识

简介: go 语言中 map 的相关知识

Go map key类型分析

1 map的key类型

map中的key可以是任何的类型,只要它的值能比较是否相等,Go的语言规范已精确定义,Key的类型可以是:

  • 布尔值
  • 数字
  • 字符串
  • 指针
  • 通道
  • 接口类型
  • 结构体
  • 只包含上述类型的数组

但不能是:

  • slice
  • map
  • function

Key类型只要能支持==!=操作符,即可以做为Key,当两个值==时,则认为是同一个Key。

2 比较相等

我们先看一下样例代码:

**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语言规范中是怎么说的:

  • Pointer values are comparable. Two pointer values are equal if they point to the same variable or if both have value nil. Pointers to distinct zero-size variables may or may not be equal.当指针指向同一变量,或同为nil时指针相等,但指针指向不同的零值时可能不相等。
  • Channel values are comparable. Two channel values are equal if they were created by the same call to make or if both have value nil.Channel当指向同一个make创建的或同为nil时才相等
  • Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.从上面的例子我们可以看出,当接口有相同的动态类型并且有相同的动态值,或者值为都为nil时相等。要注意的是:参考理解Go Interface
  • A value x of non-interface type X and a value t of interface type T are comparable when values of type X are comparable and X implements T. They are equal if t’s dynamic type is identical to X and t’s dynamic value is equal to x.如果一个是非接口类型X的变量x,也实现了接口T,与另一个接口T的变量t,只t的动态类型也是类型X,并且他们的动态值相同,则他们相等。
  • Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.结构体当所有字段的值相同,并且没有 相应的非空白字段时,则他们相等,
  • Array values are comparable if values of the array element type are comparable. Two array values are equal if their corresponding elements are equal.两个数组只要他们包括的元素,每个元素的值相同,则他们相等。

注意:Go语言里是无法重载操作符的,struct是递归操作每个成员变量,struct也可以称为map的key,但如果struct的成员变量里有不能进行==操作的,例如slice,那么就不能作为map的key。

3 类型判断

判断两个变量是否相等,首先是要判断变量的动态类型是否相同,在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 funcequal 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 funcequal 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的代码还是比较多的,简化逻辑如下(参考注释上序列):

  1. 调用key类型的hash方法,计算出keyhash
  2. 根据hash值找到对应的桶bucket
  3. 在桶中找到key值相等的map的value。判断相等需调用key类型的equal方法

到现在我们也就有了初步了解,map中的key访问时同时需要使用该类型的hash funcequal func,只要key值相等,当结构体即使不是同一对象,也可从map中获取相同的值,例如:

m := make(map[interface{}]interface{})
m[_key{}] = "value"
if v, ok := m[_key{}];ok {
  fmt.Println("%v", v) // output: value
}

判断两个 map 是否相等

比较两个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 为什么不在语言层面支持 map 并发?

为什么原生不支持

凭什么 Go 官方还不支持,难不成太复杂了,性能太差了,到底是为什么?


官方答复原因如下(via @go faq):

  • 典型使用场景:map 的典型使用场景是不需要从多个 goroutine 中进行安全访问。

  • 非典型场景(需要原子操作):map 可能是一些更大的数据结构或已经同步的计算的一部分。

  • 性能场景考虑:若是只是为少数程序增加安全性,导致 map 所有的操作都要处理 mutex,将会降低大多数程序的性能。

核心来讲就是:Go 团队在经过了长时间的讨论后,认为原生 map 更应适配典型使用场景。


如果为了小部分情况,将会导致大部分程序付出性能代价,决定了不支持原生的并发 map 读写。且在 Go1.6 起,增加了检测机制,并发的话会导致异常。

为什么要崩溃

前面有提到一点,在 Go1.6 起会进行原生 map 的并发检测,这是一些人的 “噩梦”。


在此有人吐槽到:“明明给我抛个错就好了,凭什么要让我的 Go 进程直接崩溃掉,分分钟给我背个 P0”。

场景枚举

这里我们假设一下,如果并发读写 map 是以下两种场景:

  1. 产生 panic:程序 panic -> 默认走进 recover -> 没有对并发 map 进行处理 -> map 存在脏数据 -> 程序使用脏数据 -> 产生**未知((影响。
  2. 产生 crash:程序 crash -> 直接崩溃 -> 保全数据(数据正常)-> 产生**明确((风险。

你会选择哪一种方案呢?Go 官方在两者的风险衡量中选择了第二种。



无论是编程,还是人生。如何在随机性中掌握确定性的部分,也是一门极大的哲学了。

let it crash

Go 官方团队选择的方式是业内经典的 “let it crash” 行为,很多编程语言中,都会将其奉行为设计哲学。


let it crash 是指工程师不必过分担心未知的错误,而去进行面面俱到的防御性编码


这块理念最经典的就是 erlang 了。

总结

在今天这篇文章中,我们介绍了 Go 语言为什么不支持原生支持 map 并发,核心原因是大部分场景都不需要,从性能考虑上做的考虑。


直接让并发读写 map 的原因,是从 “let it crash” 去考虑。这块如果你想在自己的工程中避免这个情况,可以在 linter 等工具链加入竞态检测(-race),也可以避免这类风险。

相关文章
|
1天前
|
程序员 Go PHP
为什么大部分的 PHP 程序员转不了 Go 语言?
【9月更文挑战第8天】大部分 PHP 程序员难以转向 Go 语言,主要因为:一、编程习惯与思维方式差异,如语法风格和编程范式;二、学习成本高,需掌握新知识体系且面临项目压力;三、职业发展考量,现有技能价值及市场需求不确定性。学习新语言虽有挑战,但对拓宽职业道路至关重要。
22 10
|
1天前
|
算法 程序员 Go
PHP 程序员学会了 Go 语言就能唬住面试官吗?
【9月更文挑战第8天】学会Go语言可提升PHP程序员的面试印象,但不足以 solely “唬住” 面试官。学习新语言能展现学习能力、拓宽技术视野,并增加就业机会。然而,实际项目经验、深入理解语言特性和综合能力更为关键。全面展示这些方面才能真正提升面试成功率。
20 10
|
1天前
|
编译器 Go
go语言学习记录(关于一些奇怪的疑问)有别于其他编程语言
本文探讨了Go语言中的常量概念,特别是特殊常量iota的使用方法及其自动递增特性。同时,文中还提到了在声明常量时,后续常量可沿用前一个值的特点,以及在遍历map时可能遇到的非顺序打印问题。
|
5天前
|
安全 大数据 Go
深入探索Go语言并发编程:Goroutines与Channels的实战应用
在当今高性能、高并发的应用需求下,Go语言以其独特的并发模型——Goroutines和Channels,成为了众多开发者眼中的璀璨明星。本文不仅阐述了Goroutines作为轻量级线程的优势,还深入剖析了Channels作为Goroutines间通信的桥梁,如何优雅地解决并发编程中的复杂问题。通过实战案例,我们将展示如何利用这些特性构建高效、可扩展的并发系统,同时探讨并发编程中常见的陷阱与最佳实践,为读者打开Go语言并发编程的广阔视野。
|
3天前
|
存储 Shell Go
Go语言结构体和元组全面解析
Go语言结构体和元组全面解析
|
8天前
|
Go
golang语言之go常用命令
这篇文章列出了常用的Go语言命令,如`go run`、`go install`、`go build`、`go help`、`go get`、`go mod`、`go test`、`go tool`、`go vet`、`go fmt`、`go doc`、`go version`和`go env`,以及它们的基本用法和功能。
20 6
|
7天前
|
Go
Golang语言之映射(map)快速入门篇
这篇文章是关于Go语言中映射(map)的快速入门教程,涵盖了map的定义、创建方式、基本操作如增删改查、遍历、嵌套map的使用以及相关练习题。
20 5
|
8天前
|
存储 Go
Golang语言基于go module方式管理包(package)
这篇文章详细介绍了Golang语言中基于go module方式管理包(package)的方法,包括Go Modules的发展历史、go module的介绍、常用命令和操作步骤,并通过代码示例展示了如何初始化项目、引入第三方包、组织代码结构以及运行测试。
16 3
|
10天前
|
缓存 安全 Java
如何利用Go语言提升微服务架构的性能
在当今的软件开发中,微服务架构逐渐成为主流选择,它通过将应用程序拆分为多个小服务来提升灵活性和可维护性。然而,如何确保这些微服务高效且稳定地运行是一个关键问题。Go语言,以其高效的并发处理能力和简洁的语法,成为解决这一问题的理想工具。本文将探讨如何通过Go语言优化微服务架构的性能,包括高效的并发编程、内存管理技巧以及如何利用Go生态系统中的工具来提升服务的响应速度和资源利用率。
|
10天前
|
Rust Linux Go
Rust/Go语言学习
Rust/Go语言学习