爱上开源之golang入门至实战第四章-映射(Map)

发布于:2023-01-22 ⋅ 阅读:(8) ⋅ 点赞:(0) ⋅ 评论:(0)

前言

键值对的集合是很多语言都支持的基础数据结构,GO语言里也有同样的键值对集合的数据类型,那就是Map。

Map 是一种无序的键值对的集合。Map可以通过 key 来快速检索数据,key类似于索引,指向数据的值。我们可以像迭代数组和切片那样迭代它。但是因为 Map 是使用 hash 表来实现的;Map 是无序的,无法决定它的返回顺序。Map和切片一样都是Go语言编程里使用非常广泛的数据结构,我们也要熟悉的掌握Map数据类型的使用。

 

今天就来谈谈go语言里的Map

4.3.1 声明

map 是引用类型,可以使用如下声明:

var variable_name map[keytype]valuetype 
​
例如
var students map[string]Student

keytype定义的数据类型可以是任意可以用 == 或者 != 操作符比较的类型,k1 == k2 时,可认为 k1 和 k2 是同一个 key。Go 语言中只要是可比较的类型都可以作为 key。除开 slice,map,functions 这几种类型,其他类型都是作为key的对象的。具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。结构体的比较需要结构体定义的字段类型都支持可比较。如果是结构体,只有 hash 后的值相等以及字面值相等,才被认为是相同的 key。很多字面值相等的,hash出来的值不一定相等,比如引用。

valuetype可以是任何数据类型,任何类型都可以作为value;通过使用空接口类型, 比如 any, interface{} 我们可以存储任意值,也可以使用函数类型来定义成值类型;这个经常在一些复杂的命令控制程序代码里出现;

在声明的时候不需要指定map 的长度(创建和初始化时,可以指定初始长度),map 是可以动态增长的。

如果不初始化 map,那么就会创建一个值为nil的Map对象。nil Map对象不能用来存放键值对。程序运行时,对nil Map对象进行访问时,就会报panic错误。 所以如果要使用一个Map对象,一定要进行初始化或者赋值。这一点上和Slice稍微有点区别。

4.3.2 初始化

map 可以用 {key1: val1, key2: val2} 的描述方法来初始化,就像数组和结构体一样。

var m map[string]string
m = map[string]string{"Go": "Google", "Java": "Oracle"}
fmt.Println(m)
​
==== OUTPUT ====
map[Go:Google Java:Oracle]

或者直接写成

m := map[string]string{"Go": "Google", "Java": "Oracle"}
fmt.Println(m)
​
==== OUTPUT ====
map[Go:Google Java:Oracle]

下面是一个结构体作为Value的初始化


students := map[int]struct {
    name string
    sex  int
    age  int
}{
    1: {"user1", 1, 20},
    2: {"user2", 2, 20},
    3: {"user2", 2, 21},
}
​
fmt.Println(students)
​
==== OUTPUT ====
map[1:{user1 1 20} 2:{user2 2 20} 3:{user2 2 21}]

map 是 引用类型 的: 内存用 make 方法来分配。可以通过make方法来对map进行初始化;


c := make(map[string]string)
c["Go"] = "Google"
c["Java"] = "Oracle"
fmt.Println(c)
​
==== OUTPUT ====
map[Go:Google Java:Oracle]

 

s := make(map[int]any)
s[1] = struct {
    name string
    sex  int
    age  int
}{"user1", 1, 20}
s[2] = struct {
    name string
    sex  int
    age  int
}{"user2", 2, 20}
s[3] = struct {
    name string
    sex  int
    age  int
}{"user2", 2, 21}
​
fmt.Println(s)
​
==== OUTPUT ====
map[1:{user1 1 20} 2:{user2 2 20} 3:{user2 2 21}]

当 map 到容量上限的时候,如果再增加新的键值对,map 的大小会自动加 1。出于对性能的考虑,对于大的 map或者会快速扩张的 map,即使不是非常明确具体的容量大小,也最好先根据初略的计算,大概的标明Map的大小。 在使用make初始化Map的时,可以预先给Map指定⼀个合理元素数量,在初始化时先申请⼀⼤块内存,而可以避免后续操作时频繁扩张。

例如

map2 := make(map[string]float32, 100) 

4.3.3 赋值及访问

map在中的对象可以通过key进行赋值或者访问

s := make(map[int]any)
s[1] = struct {
    name string
    sex  int
    age  int
}{"user1", 1, 20}
s[2] = struct {
    name string
    sex  int
    age  int
}{"user2", 2, 20}
s[3] = struct {
    name string
    sex  int
    age  int
}{"user2", 2, 21}
​
fmt.Println(s[0])
fmt.Println(s[1])
fmt.Println(s)
​
==== OUTPUT ====
<nil>
{user1 1 20}
map[1:{user1 1 20} 2:{user2 2 20} 3:{user2 2 21}]

如上个例子所示,s为Map对象,其中key值分别有1,2,3;访问key值为0的时候,key不存在;返回nil;

访问Map里不存在的Key返回nil对象; 由于Map的value可以是任何类型的值,也包括nil,所以我们可以将一个nil对象作为value放入Map对象, 时候返回值也为nil,但是key是存在的; 我们可以通过下面的方式来进行key的判断;一般判断是否某个key存在,不使用值判断,

s := make(map[int]any)
s[1] = struct {
    name string
    sex  int
    age  int
}{"user1", 1, 20}
s[2] = struct {
    name string
    sex  int
    age  int
}{"user2", 2, 20}
s[3] = struct {
    name string
    sex  int
    age  int
}{"user2", 2, 21}
s[4] = nil
​
fmt.Println(s)
fmt.Println(s[1])
fmt.Println(s[0])
fmt.Println(s[4])
​
if v, ok := s[0]; !ok {
    fmt.Println("不存在")
} else {
    fmt.Printf("存在 %+v \n", v)
}
​
if v, ok := s[4]; !ok {
    fmt.Println("不存在")
} else {
    fmt.Printf("存在 值=%+v \n", v)
}
​
if v, ok := s[1]; !ok {
    fmt.Println("不存在")
} else {
    fmt.Printf("存在 值= %+v \n", v)
}
​
==== OUTPUT ====
map[1:{user1 1 20} 2:{user2 2 20} 3:{user2 2 21} 4:<nil>]
{user1 1 20}
<nil>
<nil>
不存在
存在 值=<nil> 
存在 值= {name:user1 sex:1 age:20} 

4.3.4 迭代Map

非常多的时候,我们需要遍历整个Map所有的Key,并根据获取到的Key和对应的Value对象进行操作;此时我们的可以使用range方式来实现这个目的, 例如


    s := make(map[int]any)
    s[1] = struct {
        name string
        sex  int
        age  int
    }{"user1", 1, 20}
    s[2] = struct {
        name string
        sex  int
        age  int
    }{"user2", 2, 20}
    s[3] = struct {
        name string
        sex  int
        age  int
    }{"user2", 2, 21}
    s[4] = nil
​
    fmt.Println(s)
    for k, o := range s {
        fmt.Printf("%d=%+v \n", k, o)
    }
​
==== OUTPUT ====
map[1:{user1 1 20} 2:{user2 2 20} 3:{user2 2 21} 4:<nil>]
1={name:user1 sex:1 age:20} 
2={name:user2 sex:2 age:20} 
3={name:user2 sex:2 age:21} 
4=<nil> 

由于Map是无序的,迭代的过程不能保证返回次序,返回的顺序;通常是和具体环境和版本实现有关。

4.3.5 值传递和引用传递

Map对象本身,在作为参数和返回值的时候,都是使用的引用传递;如下面的示例


setName := func(a map[string]string, name string) map[string]string {
    a["name"] = name
    return a
}
​
sample := map[string]string{"name": "Go"}
fmt.Println(sample) // map[name:Go]
setName(sample, "Java")
fmt.Println(sample) // map[name:Java]
​
two := setName(sample, "C#")
fmt.Println(two) // map[name:C#]
two["name"] = "Python"
fmt.Println(sample) // map[name:Python]
​
==== OUTPUT ====
map[name:Go]
map[name:Java]
map[name:C#]
map[name:Python]

与切片一样,切片有 copy函数来进行值的复制;map 却没有,目前只能通过遍历然后重新赋值来实现。

通过Map的Key访问获取的值对象,不是引用传递,是指传递

如下列所示

type student struct {
    name string
    sex  int
    age  int
}
​
s1 := make(map[int]student)
s1[1] = student{"user1", 1, 20}
s1[1].name = "User01"   //Cannot assign to s1[1].name
​
==== Complie ERROR====
.\build_test.go:134:2: cannot assign to struct field s1[1].name in map

通过range的方式,进行迭代,或者值对象也是值传递

type student struct {
    name string
    sex  int
    age  int
}
​
s1 := make(map[int]student)
s1[1] = student{"user1", 1, 20}
​
setName2 := func(a map[int]student, id int, newName string) map[int]student {
    if v, ok := a[id]; ok {
        v.name = newName
    }
​
    return a
}
​
fmt.Println(s1) // map[1:{user1 1 20}]
setName2(s1, 1, "User01")
fmt.Println(s1) // map[1:{user1 1 20}]
s2 := setName2(s1, 1, "User02")
fmt.Println(s2) // map[1:{user1 1 20}]
​
==== OUTPUT ====
map[1:{user1 1 20}]
map[1:{user1 1 20}]
map[1:{user1 1 20}]

如果需要进行原值的覆盖可以使用完整替换 value 或使⽤指针

type student struct {
    name string
    sex  int
    age  int
}
​
s1 := make(map[int]student)
s1[1] = student{"user1", 1, 20}
s1[1] = student{"User01", 1, 20}
fmt.Println(s1) // map[1:{User01 1 20}] 
​
==== OUTPUT ====
map[1:{User01 1 20}]


type student struct {
    name string
    sex  int
    age  int
}
​
ss := make(map[int]*student)
ss[1] = &student{"user1", 1, 20}
setName3 := func(a map[int]*student, id int, newName string) {
    if v, ok := a[id]; ok {
        v.name = newName
    }
}
fmt.Println(ss[1]) //{user1 1 20}
setName3(ss, 1, "User01")
fmt.Println(ss[1]) // {User01 1 20}
​
==== OUTPUT ====
&{user1 1 20}
&{User01 1 20}

4.3.6 Delete

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。从 map1 中删除 key1:

直接 delete(map1, key1) 就可以。如果 key1 不存在,该操作不会产生错误。

实例如下:

Delete(map4, "a")

4.3.7 map的GC回收机制

map是只增不减的一种数组结构,那些被 delete 的 key 会被打上标记,但是不会被GC回收。对于大的map对象。如果不再使用了最好使用赋值 nil 的方式使其整个的可以被GC。

4.3.8 并发缺陷

Go语言原生的Map非并发安全的, 在多并发的情况下,如果有写的操作,会出现Panic,提示concurrent map writes的错误

mm := map[int]int{}
​
for i := 0; i < 21; i++ {
    go func() {
        mm[1] = 1
    }()
}
​
====Panic====
fatal error: concurrent map writes

另外如果多线程同时 read 和 write ,或者删除 key,还会出现 fatal error: concurrent map read and map write,这都是 map 存在的并发问题。

解决问题,可以引入sync.Mutex或者sync.RWMutex控制并发访问;或是是通过第三方的并发控制进行解决。