教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 Golang map 三板斧第二式:注意事项

Golang map 三板斧第二式:注意事项

发布时间:2021-04-26   编辑:jiaochengji.com
教程集为您提供Golang map 三板斧第二式:注意事项等资源,欢迎您收藏本站,我们将为您提供最新的Golang map 三板斧第二式:注意事项资源


文章目录

  • 1.map 默认初始值为 nil
  • 2.map range 顺序的随机性
  • 3.map 值传递表现出引用传递的效果
    • 4.map 元素不可取址
  • 5.map 并发读写问题
  • 参考文献

map 使用起来非常方便,但也有些必须要注意的地方,否则可能会导致程序异常甚至 panic。

1.map 默认初始值为 nil

map 未初始化的情况下值为 nil,此时进行取值,返回的是对应类型的零值,不会引发 panic。所以取值时如果不关心取的是否是零值,那么可以直接取而不用使用 comma-ok 式,这样会使代码变得简洁许多。

var mapName map[string]string

// 使用 comma-ok 式
if v, ok := mapName["dable"]; ok {
	name = v
}

// 直接取(不关心是否存在)
name = mapName["dable"]

向 map 写入要非常小心,因为向未初始化的 map(值为 nil)写入会引发 panic,所以向 map 写入时需先进行判空操作。

var m map[string]string
m["dable"] = "male"

上面的代码将产生 panic: assignment to entry in nil map 的运行时错误。

2.map range 顺序的随机性

map 在没有被修改的情况下,使用 range 多次遍历 map 时输出的 key 和 value 的顺序可能不同。这是 Go 语言的设计者们有意为之,在每次 range 时的顺序被随机化,旨在提示开发者们,Go 底层实现并不保证 map 遍历顺序稳定,请大家不要依赖 range 遍历结果顺序。Golang 官方博文对此有详细说明:Go maps in action。

参考如下程序:

package main

import (
	"fmt"
)

func main() {
	fmt.Println("first range:")
	for i, v := range m {
		fmt.Printf("m[%v]=%v ", i, v)
	}
	fmt.Println("\nsecond range:")
	for i, v := range m {
		fmt.Printf("m[%v]=%v ", i, v)
	}
}

运行输出的结果可能是:

first range:
m[3]=c m[4]=d m[1]=a m[2]=b
second range:
m[1]=a m[2]=b m[3]=c m[4]=d

map 本身是无序的,且遍历时顺序还会被随机化,如果想顺序遍历 map,需要对 map key 先排序,再按照 key 的顺序遍历 map。

import "sort"

var tmpSl []string
// 把 key 单独取出放到切片
for k := range m {
    tmpSl = append(tmpSl, k)
}
// 排序切片
sort.Strings(tmpSl)
// 以切片中的 key 顺序遍历 map 就是有序的了
for _, k := range tmpSl {
    fmt.Println(k, m[k])
}

3.map 值传递表现出引用传递的效果

Golang 中没有引用传递,只有值和指针传递。所以 map 作为函数实参传递时本质上也是值传递,只不过因为 map 底层数据结构是通过指针指向实际的元素存储空间,在被调函数中修改 map,对调用者同样可见,所以 map 作为函数实参传递时表现出了引用传递的效果。因此,传递 map 时,函数形参无需使用指针。

考察如下程序:

package main

import (
	"fmt"
)

func main() {
	m := map[int32]string{
		1: "a",
		2: "b",
		3: "c",
		4: "d",
	}
	modifyMapV0(m)
	fmt.Println("after modifyMapV0 pass by value:")
	for i, v := range m {
		fmt.Printf("m[%v]=%v ", i, v)
	}
	modifyMapV1(&m)
	fmt.Println("\nafter modifyMapV1 pass by pointer:")
	for i, v := range m {
		fmt.Printf("m[%v]=%v ", i, v)
	}
}

// modifyMapV0 删除所有 key 为偶数的元素
// 使用值传递 map
func modifyMapV0(m map[int32]string) {
	for i := range m {
		if i%2 == 0 {
			delete(m, i)
		}
	}
}

// modifyMapV1 删除所有 key 为大于 1 的元素
// 使用指针传递 map
func modifyMapV1(m *map[int32]string) {
	for i := range *m {
		if i > 1 {
			delete(*m, i)
		}
	}
}

运行输出:

after modifyMapV0 pass by value:
m[1]=a m[3]=c
after modifyMapV1 pass by pointer:
m[1]=a

可见值传递同样可以修改 map 的内容,达到了指针传递的效果。所以如果想修改 map 的内容而不是 map 变量本身,那么请使用值传递,而不是指针传递,这样会使代码更加简洁可读。

4.map 元素不可取址

map 中的元素并不是一个变量,而是一个值。因此,我们不能对 map 的元素进行取址操作。

var m = map[int]int {
	0 : 0,
	1: 1,
}

func main() {
	fmt.Println(&m[0])
}

运行报错:

cannot take the address of m[0]

因此,当 map 的元素为结构体类型的值,那么无法直接修改结构体中的字段值。考察如下示例:

package main

import (
        "fmt"
)

type person struct {
    name   string
    age    byte
    isDead bool
}

func whoIsDead(personMap map[string]person) {
    for name, _ := range personMap {
        if personMap[name].age < 50 {
            personMap[name].isDead = true
        }   
    }   
}

func main() {
    p1 := person{name: "zzy", age: 100}
    p2 := person{name: "dj", age: 99} 
    p3 := person{name: "px", age: 20} 
    personMap := map[string]person{
        p1.name: p1, 
        p2.name: p2, 
        p3.name: p3, 
    }   
    whoIsDead(personMap)
    
    for _, v :=range personMap {
        if v.isDead {
            fmt.Printf("%s is dead\n", v.name)
        }    
    }   
}

编译报错:

cannot assign to struct field personMap[name].isDead in map

原因是 map 元素是无法取址的,也就说可以得到 personMap[name],但是无法对其进行修改。解决办法有二,一是 map 的 value用 struct 的指针类型,二是使用临时变量,每次取出来后再设置回去。

(1)将 map 中的元素改为 struct 的指针。

package main

import (
        "fmt"
)

type person struct {
    name   string
    age    byte
    isDead bool
}

func whoIsDead(people map[string]*person) {
    for name, _ := range people {
        if people[name].age < 50 {
            people[name].isDead = true
        }   
    }   
}

func main() {
    p1 := &person{name: "zzy", age: 100}
    p2 := &person{name: "dj", age: 99} 
    p3 := &person{name: "px", age: 20} 
    personMap := map[string]*person {
        p1.name: p1, 
        p2.name: p2, 
        p3.name: p3, 
    }   
    whoIsDead(personMap)
    
        for _, v :=range personMap {
                if v.isDead {
                        fmt.Printf("%s is dead\n", v.name)
                }    
        }   
}

输出结果:

px is dead

(2)使用临时变量覆盖原来的元素。

package main

import (
        "fmt"
)

type person struct {
    name   string
    age    byte
    isDead bool
}

func whoIsDead(people map[string]person) {
    for name, _ := range people {
        if people[name].age < 50 {
            tmp := people[name]
            tmp.isDead = true
            people[name] = tmp 
        }   
    }   
}

func main() {
    p1 := person{name: "zzy", age: 100}
    p2 := person{name: "dj", age: 99} 
    p3 := person{name: "px", age: 20} 
    personMap := map[string]person {
        p1.name: p1, 
        p2.name: p2, 
        p3.name: p3, 
    }   
    whoIsDead(personMap)
    
        for _, v :=range personMap {
                if v.isDead {
                        fmt.Printf("%s is dead\n", v.name)
                }    
        }   
}

输出结果:

px is dead

5.map 并发读写问题

共享 map 在并发读写时需要加锁。先看错误示例:

package main

import (
        "fmt"
        "time"
)

var m = make(map[int]int)

func main() {
        //一个go程写map 
        go func(){
                for i := 0; i < 10000; i   {
                        m[i] = i    
                }   
        }() 

        //一个go程读map 
        go func(){
                for i := 0; i < 10000; i   { 
                        fmt.Println(m[i])    
                }   
        }() 
        time.Sleep(time.Second*20)
}

运行报错:

fatal error: concurrent map read and map write

可以使用读写锁(sync.RWMutex)实现互斥访问。

package main

import (
        "fmt"
        "time"
        "sync"
)

var m = make(map[int]int)
var rwMutex sync.RWMutex

func main() {
        //一个go程写map 
        go func(){
                rwMutex.Lock()
                for i := 0; i < 10000; i   {
                        m[i] = i    
                }   
                rwMutex.Unlock()
        }() 

        //一个go程读map
        go func(){
                rwMutex.RLock()
                for i := 0; i < 10000; i   { 
                        fmt.Println(m[i])    
                }   
                rwMutex.RUnlock()
        }() 
        time.Sleep(time.Second*20)
}

正常运行输出:

0
1
...
9999

参考文献

[1] 腾讯云 社区.golang新手容易犯的3个错误
[2] CSDN.golang map中结构体元素是无法取地址的
[3] CSDN.golang中map的一些注意事项

到此这篇关于“Golang map 三板斧第二式:注意事项”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Golang map 三板斧第二式:注意事项
go 开了多少个goroutine 怎么看_Golang 协程Goroutine到底是怎么回事?(一)
Golang 中使用多维 map
Django之小白必会的三板斧
Golang 空map和未初始化map注意事项
golang中map的一些注意事项
golang map key 正则表达_Golang中的Map
Golang make多种使用方法详解
golang遍历时修改被遍历对象
Go语言发展历史、核心、特性及学习路线

[关闭]
~ ~