Go Lang 实战

GO数据-字典(四)

2019-08-02  本文已影响0人  草莓君_

字典(哈希表)是一种使用频率极高的数据结构。将其作为语言内置类型,从运行时层面进行优化,可获得更高效的性能。
作为无序键值对集合,字典要求key必须是支持相等运算符(== ,!=)的数据类型,比如。数字、字符串、指针、数组、结构体、以及对应接口类型。

使用make创建字典

func main() {
    m := make(map[string] int)
    m["a"] = 1
    m["b"] = 2
    fmt.Println(m)
}

使用初始化表达式语句来创建

func main() {
    m2 := map[int]struct {
        x int
    }{
        1: {x: 100},
        2: {x: 200},
    }
    fmt.Println(m2)
}

修改字典

func main() {
    m := map[string]int {
        "a": 1,
        "b": 2,
    }

    m["a"] = 10     //修改
    m["c"] = 30     //新增

    if v, ok := m["d"];ok {     //使用ok-idiom判断key是否存在,返回值
        println(v)
    }

    delete(m, "d")          //删除键值对。不存在时,不会报错
}

函数len返回当前键值对数量,cap不接受字典类型。另外,因内存访问安全和哈希算法等缘故,字典被设计成“not addressable”,故不能直接修改value成员 (结构或数组)。

func main() {
    type user struct {
        name string
        age int
    }

    m := map[int]user {
        1: {"Tom", 19},
    }

    m[1].age += 1 //编译报错cannot assign to struct field m[1].age in map
}

正确的做法是返回整个value,待修改后在设置字典键值,或直接用指针类型。

func main() {
    type user struct {
        name string
        age int
    }

    m := map[int]user {
        1: {"Tom", 19},
    }
    u := m[1]
    u.age += 1
    m[1] = u        //设置整个value

    m2 := map[int]*user {   //value是指针
        1: &user{"Jack", 20},
    }

    m2[1].age++     //m2[1]返回的是指针,可透过指针修改目标对象
    fmt.Println(*m2[1])
}

迭代字典

访问不存在的键值,默认返回零,不会引发错误。但推荐使用ok-idiom模式,毕竟通过零值无法判断键值是否存在,或许存储的value本就是零。

对字典进行迭代每次返回的键值次序都不相同

func main() {
    m := make(map[string]int)
    for i := 0; i< 8;i ++ {
        m[string('a'+i)] = i
    }

    for i := 0;i < 4;i ++ {
        for k,v := range m {
            print(k, ":", v, " ")
        }
        println()
    }
}

输出:

g:6 h:7 a:0 b:1 c:2 d:3 e:4 f:5 
a:0 b:1 c:2 d:3 e:4 f:5 g:6 h:7 
a:0 b:1 c:2 d:3 e:4 f:5 g:6 h:7 
c:2 d:3 e:4 f:5 g:6 h:7 a:0 b:1 

安全

在迭代期间删除或新增键值是安全的。

func main() {
    m := make(map[int]int)

    for i := 0; i< 10; i++ {
        m[i] = i + 10
    }

    for k := range m {
        if k == 5 {
            m[100] = 100
        }
        delete(m, k)
        fmt.Println(k, m)
    }
}

输出:

1 map[0:10 2:12 3:13 4:14 5:15 6:16 7:17 8:18 9:19]
3 map[0:10 2:12 4:14 5:15 6:16 7:17 8:18 9:19]
4 map[0:10 2:12 5:15 6:16 7:17 8:18 9:19]
5 map[0:10 2:12 6:16 7:17 8:18 9:19 100:100]
6 map[0:10 2:12 7:17 8:18 9:19 100:100]
9 map[0:10 2:12 7:17 8:18 100:100]
0 map[2:12 7:17 8:18 100:100]
2 map[7:17 8:18 100:100]
7 map[8:18 100:100]
8 map[100:100]
100 map[]

运行时会字典并发操作作出检测。如果某个任务正在对字典进行写操作,那么其他任务就不能对该字典执行并发操作(读、写、删除),否则会导致进程崩溃。

func main() {
    m := make(map[string]int)

    go func() {
        for {
            m["a"] += 1         //写操作
            time.Sleep(time.Microsecond)
        }
    }()

    go func() {
        for {
            _ = m["b"]          //读操作
            time.Sleep(time.Microsecond)
        }
    }()

    select{}  //阻止进程退出
}

输出

fatal error: concurrent map read and map write

可是使用sync.RWMutex读写锁实现同步,避免读写操作同时进行。

func main() {
    var lock sync.RWMutex       //使用读写锁
    m := make(map[string]int)

    go func() {
        for {
            lock.Lock()         //注意锁的粒度
            m["a"] += 1         //写操作
            lock.Unlock()       //不能使用defer
            time.Sleep(time.Microsecond)
        }
    }()

    go func() {
        for {
            lock.RLock()
            _ = m["b"]          //读操作
            lock.RUnlock()
            time.Sleep(time.Microsecond)
        }
    }()

    select{}  //阻止进程退出
}

性能

字典对象本身就是指针包装,传参时无须再次取地址。

func test(x map[string]int)  {
    fmt.Printf("x: %p\n", x)
}

func main() {
    m := make(map[string]int)
    test(m)
    fmt.Printf("m: %p, %d\n", m, unsafe.Sizeof(m))

    m2 := map[string]int{}
    test(m2)
    fmt.Printf("m: %p, %d\n", m2, unsafe.Sizeof(m2))
}

输出:

x: 0xc000070180
m: 0xc000070180, 8
x: 0xc000096000
m: 0xc000096000, 8

在创建是预先准备足够空间有助于提升性能,减少扩张时内存分配和重新哈希操作。
使用基准测试:

func test() map[int]int {
    m := make(map[int]int)
    for i := 0; i < 1000; i++ {
        m[i] = i
    }
    return m
}

func testCap() map[int]int {
    m := make(map[int]int, 1000)  //预先准备充足的空间
    for i := 0; i < 1000; i++ {
        m[i] = i
    }
    return m
}

//基准测试 是测量一个程序在固定工作负载下的性能
func BenchmarkTest(b *testing.B) {
    for i := 0; i < b.N; i++ {
        test()
    }
}

func BenchmarkTestCap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        testCap()
    }
}

输出:

//BenchmarkTest
goos: darwin
goarch: amd64
pkg: hello/models
BenchmarkTest-12           20000         68202 ns/op
PASS
//BenchmarkTestCap
goos: darwin
goarch: amd64
pkg: hello/models
BenchmarkTestCap-12        50000         27326 ns/op
PASS

对于海量小对象,应直接用字典存储键值数据拷贝,而非指针。这有助于减少需要扫描的对象数量,大概缩短垃圾回收时间。另外,字典不会收缩内存,所以适当替换成新对象是必要的。

上一篇 下一篇

猜你喜欢

热点阅读