Golang 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的一些注意事项

已标记关键词 清除标记
<div class="post-text" itemprop="text"> <p>I am not an Go unsafe package expert - Neither am I a seasoned C programmer. I am trying to read a huge file > 1G using mmap syscall in go. There are a number of reasons I do mmap and munmap as opposed to read, write I/O. That is beside the point - I can write to the file in a test, when I read from the file, I can ascertain that bytes length matches up, but I cannot read contents of this string file :( Can someone suggest some reading? I need to do to go a little further, here's some code I cooked up for sample test:</p> <pre><code>filename := "/tmp/dd_file.db" f, err := os.OpenFile(filename, os.O_RDWR, 0666) defer f.Close() if err != nil { fmt.Printf("error opening file: %v", err) } stat, _ := f.Stat() size := stat.Size() fmt.Printf("[READ-ONLY] : size was : %+v ", size) got := make([]byte, size) if _, err := f.ReadAt(got, 0); err != nil && err != io.EOF { panic(err) } want, err := ioutil.ReadFile(filename) if err != nil { fmt.Printf("[READ-ONLY] : ioutil.ReadFile: %v", err) } // going to change the file size now, punch in a few things t := unsafe.Sizeof("") if err != nil { fmt.Println(err) os.Exit(1) } _, err = f.Seek(int64(t-1), 0) if err != nil { fmt.Println(err) os.Exit(1) } _, err = f.Write([]byte(" ")) if err != nil { fmt.Println(err) os.Exit(1) } mmap, err := syscall.Mmap(int(f.Fd()), 0, int(t), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) if err != nil { fmt.Println(err) os.Exit(1) } // not too sure on reading data on string - doesnt work as expected. map_array := (*[10000]string)(unsafe.Pointer(&mmap[0])) map_array[0] = "yellow!" err = syscall.Munmap(mmap) if err != nil { fmt.Println(err) os.Exit(1) } newStat, _ := f.Stat() newSize := newStat.Size() fmt.Printf("[mmap( ) RW] : size was : %+v ", newSize) got = make([]byte, newSize) if _, err := f.ReadAt(got, 0); err != nil && err != io.EOF { panic(err) } if len(got) == len(want) { fmt.Println("well the lengths are equal atleast??!") } if !bytes.Equal(got, want) { fmt.Printf(" [mmap( ) RW] : works! got %d want %d", len(got), len(want)) } </code></pre> <p>This obviously works as expected - but what if I wanted to read via mmap( ) on an mmapped file, how do I read string out of these bytes (I have a sense there is an encoding package somewhere that I might have to put to use perhaps but then StringHeader on unsafe documentation confused me).</p> <p>Suggestions.</p> </div>
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页