标签 goroutine 下的文章

通过实例深入理解sync.Map的工作原理

img{512x368}

注:本文首发于笔者的个人微信公众号”iamtonybai”,是公号付费文章(价格1元)。首发于2020.10.9日,经过一个月收费期,我觉得将其免费分享出来。如果你觉得文章质量不错,欢迎到首发地址付费支持:https://mp.weixin.qq.com/s/rsDC-6paC5zN4sepWd5LqQ

近期在项目考虑在内存中保存从数据库加载的配置数据的方案,初步考虑采用map来保存。Go语言中有两个map,一个是Go语言原生的map类型,而另外一种则是在Go 1.9版本新增到标准库中的sync.Map

一. 原生map的“先天不足”

对于已经初始化了的原生map,我们可以尽情地对其进行并发读:

// github.com/bigwhite/experiments/inside-syncmap/concurrent_builtin_map_read.go

package main

import (
    "fmt"
    "math/rand"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    var m = make(map[int]int, 100)

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

    wg.Add(10)
    for i := 0; i < 10; i++ {
        // 并发读
        go func(i int) {
            for j := 0; j < 100; j++ {
                n := rand.Intn(100)
                fmt.Printf("goroutine[%d] read m[%d]: %d\n", i, n, m[n])
            }
            wg.Done()
        }(i)
    }
    wg.Wait()
}

但原生map一个最大的问题就是不支持多goroutine并发写。Go runtime内置对原生map并发写的检测,一旦检测到就会以panic的形式阻止程序继续运行,比如下面这个例子:

// github.com/bigwhite/experiments/inside-syncmap/concurrent_builtin_map_write.go

package main

import (
        "math/rand"
        "sync"
)

func main() {
        var wg sync.WaitGroup
        var m = make(map[int]int, 100)

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

        wg.Add(10)
        for i := 0; i < 10; i++ {
                // 并发写
                go func(i int) {
                        for n := 0; n < 100; n++ {
                                n := rand.Intn(100)
                                m[n] = n
                        }
                        wg.Done()
                }(i)
        }
        wg.Wait()
}

运行上面这个并发写的例子,我们很大可能会得到下面panic:

$go run concurrent_builtin_map_write.go
fatal error: concurrent map writes

... ...

原生map的“先天不足”让其无法直接胜任某些场合的要求,于是gopher们便寻求其他路径。一种路径无非是基于原生map包装出一个支持并发读写的自定义map类型,比如,最简单的方式就是用一把互斥锁(sync.Mutex)同步各个goroutine对map内数据的访问;如果读多写少,还可以利用读写锁(sync.RWMutex)来保护map内数据,减少锁竞争,提高并发读的性能。很多第三方map的实现原理也大体如此。

另外一种路径就是使用sync.Map

二. sync.Map的原理简述

按照官方文档,sync.Map是goroutine-safe的,即多个goroutine同时对其读写都是ok的。和第一种路径的最大区别在于,sync.Map对特定场景做了性能优化,一种是读多写少的场景,另外一种多个goroutine读/写/修改的key集合没有交集。

下面是两种技术路径的性能基准测试结果对比(macOS(4核8线程) go 1.14):

// 对应的源码在https://github.com/bigwhite/experiments/tree/master/go19-examples/benchmark-for-map下面

$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/go19-examples/benchmark-for-map
BenchmarkBuiltinMapStoreParalell-8           7945152           179 ns/op
BenchmarkSyncMapStoreParalell-8              3523468           387 ns/op
BenchmarkBuiltinRwMapStoreParalell-8         7622342           190 ns/op
BenchmarkBuiltinMapLookupParalell-8          7319148           163 ns/op
BenchmarkBuiltinRwMapLookupParalell-8       21800383            55.2 ns/op
BenchmarkSyncMapLookupParalell-8            70512406            18.5 ns/op
BenchmarkBuiltinMapDeleteParalell-8          8773206           174 ns/op
BenchmarkBuiltinRwMapDeleteParalell-8        5424912           214 ns/op
BenchmarkSyncMapDeleteParalell-8            49899008            23.7 ns/op
PASS
ok      github.com/bigwhite/experiments/go19-examples/benchmark-for-map    15.727s

我们看到:sync.Map在读和删除两项性能基准测试上的数据都大幅领先使用sync.Mutex或RWMutex包装的原生map,仅在写入一项上存在一倍的差距。sync.Map是如何实现如此高的读取性能的呢?简单说:空间换时间+读写分离+原子操作(快路径)。

sync.Map底层使用了两个原生map,一个叫read,仅用于读;一个叫dirty,用于在特定情况下存储最新写入的key-value数据:

img{512x368}

图:sync.Map内置两个原生map

read(这个map)好比整个sync.Map的一个“高速缓存”,当goroutine从sync.Map中读取数据时,sync.Map会首先查看read这个缓存层是否有用户需要的数据(key是否命中),如果有(命中),则通过原子操作将数据读取并返回,这是sync.Map推荐的快路径(fast path),也是为何上面基准测试结果中读操作性能极高的原因。

三. 通过实例深入理解sync.Map的原理

sync.Map源码(Go 1.14版本)不到400行,应该算是比较简单的了。但对于那些有着“阅读源码恐惧症”的gopher来说,我们可以通过另外一种研究方法:实例法,并结合些许源码来从“黑盒”角度理解sync.Map的工作原理。这种方法十分适合那些相对独立、可以从标准库中“单独”取出来的包,而sync.Map就是这样的包。

首先,我们将sync.Map从标准库源码目录中拷贝一份,放入本地~/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap/sync下面,得益于go module的引入,我们在~/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap目录下面建立go.mod文件:

module github.com/bigwhite/go

go 1.14

这样我们就可以通过github.com/bigwhite/go/sync包路径导入module:github.com/bigwhite/go下面的sync包了。

接下来,我们给位于~/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap/sync下面的map.go中(sync.Map包的副本)添加一个Map类型的新方法Dump

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/sync/map.go

func (m *Map) Dump() {
        fmt.Printf("=====> sync.Map:\n")
        // dump read
        read, ok := m.read.Load().(readOnly)
        fmt.Printf("\t read(amended=%v):\n", read.amended)
        if ok {
                // dump readOnly's map
                for k, v := range read.m {
                        fmt.Printf("\t\t %#v:%#v\n", k, v)
                }
        }

        // dump dirty
        fmt.Printf("\t dirty:\n")
        for k, v := range m.dirty {
                fmt.Printf("\t\t %#v:%#v\n", k, v)
        }

        // dump miss
        fmt.Printf("\t misses:%d\n", m.misses)

        // dump expunged
        fmt.Printf("\t expunged:%#v\n", expunged)
        fmt.Printf("<===== sync.Map\n")
}

这个方法将打印Map的内部状态以及read、dirty两个原生map中的所有key-value对,这样我们在初始状态、store key-value后、load key以及delete key后通过Dump方法输出sync.Map状态便可以看到不同操作后sync.Map内部的状态变化,从而间接了解sync.Map的工作原理。下面我们就分情况剖析sync.Map的行为特征。

1. 初始状态

sync.Map是零值可用的,我们可以像下面这样定义一个sync.Map类型变量,并无需做显式初始化(关于零值可用,在我的Go专栏《改善Go语言编程质量的50个有效实践》中有专门的一节详述,有兴趣的gopher可以订阅学习^_^)。

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go

var m sync.Map

我们通过Dump输出初始状态下的sync.Map的内部状态:

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go

func main() {
        var m sync.Map
        fmt.Println("sync.Map init status:")
        m.Dump()

        ... ...

}

运行后,输出如下:

sync.Map init status:
=====> sync.Map:
     read(amended=false):
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc0001101e0)
<===== sync.Map

在初始状态下,dirty和read两个内置map内都无数据。expunged是一个哨兵变量(也是一个包内的非导出变量),它在sync.Map包初始化时就有了一个固定的值。该变量在后续用于元素删除场景(删除的key并不立即从map中删除,而是将其value置为expunged)以及load场景。如果哪个key值对应的value值与explunged一致,说明该key已经被map删除了(即便该key所占用的内存资源尚未释放)。

// map.go

var expunged = unsafe.Pointer(new(interface{}))

2. 写入数据(store)

下面,我们向Map写入一条数据:

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go

type val struct {
        s string
}

func main() {
        ... ...
        val1 := &val{"val1"}
        m.Store("key1", val1)
        fmt.Println("\nafter store key1:")
        m.Dump()

        ... ...

}

我们看一下存入新数据后,Map内部的状态:

after store key1:
=====> sync.Map:
     read(amended=true):
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000108080)}
     misses:0
     expunged:(unsafe.Pointer)(0xc000108040)
<===== sync.Map

我们看到写入(key1,value1)后,Map中有两处变化,一处是dirty map,新写入的数据存储在dirty map中;第二处是read中的amended值由false变为了true,表示dirty map中存在某些read map还没有的key

3. dirty提升(promoted)为read

此时,如果我们调用一次sync.Map的Load方法,无论传给Load的key值是否为”key1″还是其他,sync.Map内部都会发生较大变化,我们来看一下:

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go

        m.Load("key2") //这里我们尝试load key="key2"
        fmt.Println("\nafter load key2:")
        m.Dump()

下面是Load方法调用后Dump方法输出的内容:

after load key2:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

我们看到:原dirty map中的数据被提升(promoted)到read map中了,提升后amended值重新变回false

结合sync.Map中Load方法的源码,我们得出如下sync.Map的工作原理:当Load方法在read map中没有命中(miss)传入的key时,该方法会再次尝试在dirty中继续匹配key;无论是否匹配到,Load方法都会在锁保护下调用missLocked方法增加misses的计数(+1);如果增加完计数的misses值大于等于dirty map中的元素个数,则会将dirty中的元素整体提升到read:

// $GOROOT/src/sync/map.go

func (m *Map) missLocked() {
        m.misses++  //计数+1
        if m.misses < len(m.dirty) {
                return
        }
        m.read.Store(readOnly{m: m.dirty})  // dirty提升到read
        m.dirty = nil  // dirty置为nil
        m.misses = 0 // misses计数器清零
}

为了验证上述promoted的条件,我们再来做一组实验:

        val2 := &val{"val2"}
        m.Store("key2", val2)
        fmt.Println("\nafter store key2:")
        m.Dump()

        val3 := &val{"val3"}
        m.Store("key3", val3)
        fmt.Println("\nafter store key3:")
        m.Dump()

        m.Load("key1")
        fmt.Println("\nafter load key1:")
        m.Dump()

        m.Load("key2")
        fmt.Println("\nafter load key2:")
        m.Dump()

        m.Load("key2")
        fmt.Println("\nafter load key2 2nd:")
        m.Dump()

        m.Load("key2")
        fmt.Println("\nafter load key2 3rd:")
        m.Dump()

在完成一次promoted动作之后,我们又向sync.Map中写入两个key:key2和key3,并在后续Load一次key1并连续三次Load key2,下面是Dump方法的输出结果:

after store key2:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after store key3:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key1:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key2:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     misses:1
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key2 2nd:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     misses:2
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key2 3rd:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

我们看到在写入key2这条数据后,dirty中不仅存储了key2这条数据,原read中的key1数据也被复制了一份存入到dirty中。这个操作是由sync.Map的dirtyLocked方法完成的:

// $GOROOT/src/sync/map.go

func (m *Map) dirtyLocked() {
        if m.dirty != nil {
                return
        }

        read, _ := m.read.Load().(readOnly)
        m.dirty = make(map[interface{}]*entry, len(read.m))
        for k, e := range read.m {
                if !e.tryExpungeLocked() {
                        m.dirty[k] = e
                }
        }
}

前面我们提到过,promoted(dirty -> read)是一个整体的指针交换操作,promoted时,sync.Map直接将原dirty指针store给read并将自身置为nil,因此sync.Map要保证amended=true时,dirty中拥有整个Map的全量数据,这样在下一次promoted(dirty -> read)时才不会丢失数据。不过dirtyLocked是通过一个迭代实现的元素从read到dirty的复制,如果Map中元素规模很大,这个过程付出的损耗将很大,并且这个过程是在锁保护下的。

在存入key3后,我们调用Load方法先load了key1,由于key1在read中有记录,因此此次load命中了,走的是快路径,对Map状态没有任何影响。

之后,我们又Load了key2,key2不在read中,因此产生了一次miss。misses增加计数后的值为1,而此时dirty中的元素数量为3,不满足promote的条件,于是没有执行promote操作。后续我们又连续进行了两次key2的Load操作,产生了两次miss事件后,misses的计数值等于了dirty中的元素数量,于是promote操作被执行,dirty map整体被置换给read,自己则变成了nil。

4. 更新已存在的key

我们再来看一下更新已存在的key的值的情况。首先是该key仅存在于read中(刚刚promote完毕),而不在dirty中。我们更新这时仅在read中存在的key2的值:

        val2_1 := &val{"val2_1"}
        m.Store("key2", val2_1)
        fmt.Println("\nafter update key2(in read, not in dirty):")
        m.Dump()

下面是Dump输出的结果:

after update key2(in read, not in dirty):
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc00008e1e0)
<===== sync.Map

我们看到sync.Map直接更新了位于read中的key2的值(entry.storeLocked方法实现的),dirty和其他字段没有受到影响。

第二种情况是该key刚store到dirty中,尚未promote,不在read中。我们新增一个key4,并更新其值:

        val4 := &val{"val4"}
        m.Store("key4", val4)
        fmt.Println("\nafter store key4:")
        m.Dump()

        val4_1 := &val{"val4_1"}
        m.Store("key4", val4_1)
        fmt.Println("\nafter update key4(not in read, in dirty):")
        m.Dump()

dump方法的输出结果如下:

after store key4:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
         "key4":&smap.entry{p:(unsafe.Pointer)(0xc00008e310)}
     misses:0
     expunged:(unsafe.Pointer)(0xc00008e1e0)
<===== sync.Map

after update key4(not in read, in dirty):
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
         "key4":&smap.entry{p:(unsafe.Pointer)(0xc00008e330)}
     misses:0
     expunged:(unsafe.Pointer)(0xc00008e1e0)
<===== sync.Map

我们看到,sync.Map同样是直接将key4对应的value重新设置为新值(val4_1)。

5. 删除key

为了方便查看,我们将上述Map状态回滚到刚刚promote(dirty -> read)完的时刻,即:

after load key2 3rd:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e270)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc00008e1e0)
<===== sync.Map

删除key也有几种情况,我们分别来看一下:

  • 删除的key仅存在于read中

我们删除上面Map中仅存在于read中的key2:

        m.Delete("key2")
        fmt.Println("\nafter delete key2:")
        m.Dump()

删除后的Dump结果如下:

after delete key2:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(nil)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

我们看到sync.Map并没有删除key2,而是将其value置为nil。

  • 删除的key仅存在于dirty中

为了构造初仅存在于dirty中的key,我们向sync.Map写入新数据key4,然后再立刻删除它

        val4 := &val{"val4"}
        m.Store("key4", val4)
        fmt.Println("\nafter store key4:")
        m.Dump()

        m.Delete("key4")
        fmt.Println("\nafter delete key4:")
        m.Dump()

上述代码的Dump结果如下:

after store key4:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000104220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc0001041e0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000104220)}
         "key4":&smap.entry{p:(unsafe.Pointer)(0xc0001042f0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
     misses:0
     expunged:(unsafe.Pointer)(0xc0001041e0)
<===== sync.Map

after delete key4:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000104220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc0001041e0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
     dirty:
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000104220)}
     misses:0
     expunged:(unsafe.Pointer)(0xc0001041e0)
<===== sync.Map

我们看到:和仅在read中的情况不同(仅将value设置为nil),仅存在于dirty中的key被删除后,该key就不再存在了。这里还有一点值得注意的是:当向dirty写入key4时,dirty会复制read中的未被删除的元素,由于key2已经被删除,因此顺带将read中的key2对应的value设置为哨兵(expunged),并且该key不会被加入到dirty中。直到下一次promote,该key才会被回收(因为read被交换指向新的dirty,原read指向的内存将被GC)。

  • 删除的key既存在于read,也存在于dirty中

目前上述sync.Map实例中既存在于read,也存在于dirty中的key有key1和key3(key2已经被删除),我们这里以删除key1为例:

after delete key1:
=====> sync.Map:
     read(amended=true):
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc0001041e0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
     dirty:
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
     misses:0
     expunged:(unsafe.Pointer)(0xc0001041e0)
<===== sync.Map

我们看到删除key1后,read和dirty两个map中的key1均没有真正删除,而是将其value设置为nil。

我们再触发一次promote:连续调用两次导致read miss的LOAD:

        m.Load("key5")
        fmt.Println("\nafter load key5:")
        m.Dump()

        m.Load("key5")
        fmt.Println("\nafter load key5 2nd:")
        m.Dump()

调用后的Dump输出如下:

after load key5:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010200)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     dirty:
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
     misses:1
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key5 2nd:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

我们看到虽然dirty中的key1已经处于被删除状态,但它仍算作dirty元素的个数,因此第二次miss才会触发promote。promote后,dirty被赋值给read,因此原dirty中的key1元素就顺带进入到read中,只能等下次写入一个不存在的新key时才能被置为哨兵值,并在下一次promote时才能被真正删除释放。

四. 小结

通过实例法,我们大致得到了sync.Map的工作原理和行为特征,从这些结果来看sync.Map并非是一个可应用于所有场合的goroutine-safe的map实现,但在读多写少的情况下,sync.Map才能发挥出最大的效能。

本文涉及代码可以在这里 https://github.com/bigwhite/experiments/tree/master/inside-syncmap 下载。


我的Go技术专栏:“改善Go语⾔编程质量的50个有效实践”上线了,欢迎大家订阅学习!

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

系统学习Go语言,有这几本书就够了!

img{512x368}

1. Go语言的发展现状

如果从2007年9月20日那个下午三个“程序员大佬”在谷歌总部的一间办公室里进行的一次有关设计一门新编程语言的讨论算起,那么Go语言已经度过了自己的13个年头了。

img{512x368}

Robert Griesemer、Rob Pike和Ken Thompson

如果从2009年11月10日Go语言正式开源发布算起,Go语言也即将迎来自己的第11个生日

img{512x368}

2020年,Go联合创始人Rob Pike在专访中也认可了Go确实已成为云基础架构的语言。在Go即将迎来自己的11个生日的时候,Hacker News有人发起了“Go已超过10岁了,你觉得这门语言如何?”的提问,收到了广泛的关注和回答。国内媒体将这些问答整理后得到的结论是:“人生苦短,我要换Go”

Stackoverflow官博11月2日发表的《Go语言有哪些优点?探讨导致Go语言日益流行的特征 》一文对Go语言的发展趋势描述的贴切:Go语言就像爬行的藤蔓,虽缓慢,但却逐渐占据了开发世界。它正以一种郁郁葱葱的并且在许多方面都很优越的编程能力覆盖着在它之前出现的所有事物
img{512x368}

不管你是否承认,Go在IT就业市场已经成为事实上的“香饽饽”之一,就像一贯不激进的慕课网也在今年双11打出了下面的专题:

img{512x368}

上车,任何时间都不晚! 那么怎么才能踏上Go这一强大且稳健前行的车呢?和其他主流编程语言一样,上车的必经之路:看书

2. 市面上的Go书籍为何这么少

和C、C++、Java、Python等编程语言在市面上的书籍数量相比,Go流行于市面(大陆)上的图书似乎少了很多。其原因笔者觉得有如下几点:

1) 年轻

我们来看看上述几门主流编程语言的诞生时间:

  • java 1995

  • c 1972

  • c++ 1983

  • python 1991

对于很多IT从业者来说,这些语言诞生的时候他们还没出生呢。而2009年末才正式发布的Go和“最年轻”的java之间还有14年的“年龄差”。

Go在国内真正开始快速流行起来大致在2015年第一届GopherChina大会(2015.4月)之后,当时的Go是1.4版本)。同一年下半年发布的Go 1.5实现自举并让GC延迟大幅下降,这引爆了Go在国内的流行。一批又一批程序员成为Gopher,在大厂、初创实践着Go语言。但知识和技能的沉淀和总结需要时间,相信再有5年,国内作者出版的Go语言相关书籍会像雨后春笋版出现在大家的书架上。

2)以品类代名词的身份占据的“领域”还少

提到Web,人们想到的是Java spring;提到深度学习、机器学习、人工智能,人们想到的是python和tensorflow;提到比特币,嵌入式,人们想到的是C;提到游戏,人们想到的是C++;提到前端,人们想到的是Javascript。这些语言在这些垂直领域早早以杀手级框架入场,使得它们成为了这一领域的“品类代名词”,因此与该垂直领域相关的技术书籍都会采用作为该领域“品类代名词”的编程语言编写书中示例等,这样的书也就会被归类为这类语言方面的书籍。

Go语言诞生晚,入场也较晚。Go虽然通过缓慢的“爬行”,覆盖了一些领域并占据优势地位,但还不能说已经成为了该领域的“品类代名词”,比如:云原生、API、微服务、区块链等,因此被垂直领域书籍关联的机会也不像上面那几门语言多。

同时,由于Go“自带电池”,基于Go标准库我们可以实现大部分功能特性,无需依赖过多框架。即便依赖框架,框架本身也不复杂,很少以“某某框架”为主题编写一本技术书籍,这方面远远无法媲美Java和Spring这对“黄金组合”。

3) 引进国外优秀作品需要时间

相对于国内,国外关于Go语言的作品要多不少,但引进国外图书资料需要时机以及时间(找译者翻译)。

3. 系统学习Go语言的书籍列表TOP 5

笔者接触Go语言较早,Go语言相关的中外文书籍几乎都通读过一遍(经典好书读过可不止一遍哦)。Go语言比较简单,如果单单从系统掌握这门语言的角度来看,阅读下面基本书籍就足够了。如果你要学习某些垂直领域的Go应用和技巧,那么期待我后续对垂直领域Go书籍/资料的推荐吧^_^。

这里参考“天下足球”TOP10栏目的方式推荐我心目中掌握Go语言必读的五大好书(每项满分为5分)!

第五名:《The Way To Go》 – Go语言百科全书

img{512x368}

《The Way To Go》是我早期学习Go语言时最喜欢翻看的一本书。该书成书于2012年3月,恰逢Go 1.0版本刚刚发布,作者承诺书中代码均可在Go 1.0版本上编译通过并运行。该书分为4个部分:

  • 为什么学习Go以及Go环境安装入门

  • Go语言核心语法

  • Go高级用法(读写、错误处理、单元测试、并发编程、socket与web编程等)

  • Go应用(常见陷阱、语言应用模式、从性能考量的代码编写建议、现实中的Go应用等)

每部分的每个章节都很精彩,这本书也是目前见到的最全面详实的讲解Go语言的书籍了,我称之为Gopher们的第一本“Go百科全书”

该书作者Ivo Balbaert想必大多数人都不曾耳闻。为了写本文,我特地研究了一下他的作品以及出版时间,发现这个技术作者是很会“抢先机”并且眼光独到。他总是能发现市面刚出现不久但却很有潜力的编程语言并在其他人了解该门语言之前,就编写出类似“The way to Go”这样的为早期语言接纳者提供的详实资料,包括JuliaRust等。在很多人还不知道这些语言名字的时候,他就已经开始学习这些语言,并为这些语言编写出质量很高的“百科全书”式的书籍。

很遗憾,这本书没有中文版。这可能是由于本书出版太早,等国内出版社意识到要引进Go语言方面的书籍时,这本书使用的Go版本又太老了,虽然本书中绝大部分例子依然可以在今天最新的Go编译器下通过编译并运行起来。不过无闻在github上发起了这本书的中译版项目:https://github.com/Unknwon/the-way-to-go_ZH_CN,感兴趣的gopher可以去在线或下载阅读。
此书虽棒,但毕竟年头“久远”,我只能委屈它一下了,将它列在第五位,下面是其各个指数的评分:

  • 作者名气指数:3

  • 关注度指数:3

  • 内容实用指数:4

  • 经典指数:4

总分:14

第四名:《Go 101》 – Go语言规范全方位解读

img{512x368}

这是一本在国外人气和关注度比在国内高的中国人编写的英文书,当然也是有中文版的。

如果仅从书名中的101去判断,你很大可能会认为这仅仅是一本讲解Go入门基础的书,但这本书的内容可远远不止入门这么简单。这本书可大致分为三个部分:

  • Go语法基础

  • Go类型系统与运行时实现

  • 以专题(topic)形式阐述的Go特性、技巧与实践模式

除了第一部分算是101范畴,其余两个部分都是Go语言的高级话题,也是要精通Go必须要掌握的“知识点”。并且,结合Go语言规范,作者对每个知识点的阐述都细致入微并结合大量示例辅助说明。我们知道有关C和C++语言,市面上有一些由语言作者或标准规范委员会成员编写的annotated或rationale书籍(语言参考手册或标准解读),Go 101这本书也可以理解为Go语言的标准解读或参考手册

Go 101这本书是开源电子书,其作者也在国外一些支持自出版的服务商那里做了付费数字出版。这使得这本书相对于其他纸板书有着另外一个优势:与时俱进。在作者的不断努力下,该书的知识点更新基本保持与Go的演化同步,目前其内容已经覆盖了最新的Go 1.15版本

该书作者为国内资深工程师老貘,他花费三年时间“呕心沥血”完成此书并免费奉献给Go社区,值得大家为其大大的点赞!
下面是本书推荐指数的评分:

  • 作者名气指数:3

  • 关注度指数:4

  • 内容实用指数:4

  • 经典指数:4

总分:15

第三名:《Go语言学习笔记》 – Go源码剖析与实现原理探索

img{512x368}

这是一本在国内影响力很大和关注度较高的作品。一来其作者雨痕老师是国内资深工程师,也是2015年第一届GopherChina大会讲师;二来,该作品的前期版本是以开源电子书的形式风险给国内Go社区的;三来,作者在Go源码剖析方便可谓之条理清晰,细致入微。

2016年《Go语言学习笔记》纸版书出版,该书覆盖了当时最新的Go 1.5版本,Go 1.5版本在Go语言演化历史中的分量极高,它不仅实现了Go自举,还让Go GC的延迟下降到绝大多数应用可以将其应用到生产的程度。本书整体上分为两大部分:

  • Go语言详解:以短平快、捞干的来的风格对Go语言语法做了说明,能用示例说明的,绝不用文字做过多修饰。

  • Go源码剖析:这是本书精华,也是最受Gopher关注的部分。这部分对Go运行时神秘的内存分配、垃圾回收、并发调度、channel和defer的实现原理、syn.Pool的实现原理做了细致的源码剖析与原理总结。

随着Go语言演化,其语言和运行时实现一直在变化,但Go 1.5版本的实现是后续版本的基础,因此这本书的剖析非常值得每位Gopher阅读。从雨痕老师的github上最新消息来看,他似乎在编写新版Go语言学习笔记,基于Go 1.12版本,剖析源码是枯燥繁琐的,期待新版Go学习笔记早日与Gopher们见面。
下面是本书各个指数的评分:

  • 作者名气指数:4

  • 关注度指数:4

  • 内容实用指数:4

  • 经典指数:4

总分:16

第二名:《Go语言实战》 – 实战系列(in action)经典之作,紧扣Go语言的精华

img{512x368}

Manning出版社出版的“实战系列(xx in action)”一直是程序员心中高质量和经典的代名词。在出版Go语言实战方面,该出版社也是丝毫不敢怠慢,邀请了Go社区知名的三名明星级作者联合撰写了该书的内容。这三位作者分别是:

  • 威廉·肯尼迪 (William Kennedy) – 知名Go培训师,培训机构Ardan Labs的联合创始人,”Ultimate Go”培训的策划实施者。

  • 布赖恩·克特森 (Brian Ketelsen) – 世界上最知名的Go技术大会 – GopherCon大会的联合发起人和组织者,GopherAcademy创立者,现微软Azure工程师

  • 埃里克·圣马丁 (Erik St.Martin) – 世界上最知名的Go技术大会 – GopherCon大会的联合发起人和组织者

本书并不是大部头,而是薄薄的一本(中文版才200多页),因此你不要期望从本书得到百科全书一样的阅读感。本书的作者们显然也没有想将其写成面面俱到的作品,而是直击要点,即挑出Go语言和其他语言相比与众不同的特点进行着重讲解,这些特点构成了本书的结构框架:

  • 入门:快速上手搭建、编写、运行一个go程序

  • 语法:数组(作为一个类型而存在)、切片和map

  • Go类型系统的与众不同:方法、接口、嵌入类型

  • Go的拿手好戏:并发及并发模式

  • 标准库常用包:log、marshal/unmarshal、io(Reader和Writer)

  • 原生支持的测试

读完这本书,你就掌握了Go语言的精髓之处,这迎合了多数gopher的内心需求。本书中文版译者Googol Lee也是Go圈子里的资深gopher,翻译质量上乘。

下面对本书各个指数的评分:

  • 作者名气指数:5

  • 关注度指数:5

  • 内容实用指数:4

  • 经典指数:4

总分:18

第一名:《Go程序设计语言》 – 人手一本的Go语言“圣经”

如果说由Brian W. KernighanDennis M. Ritchie联合编写的《The C Programming Language》(也称K&R C)是C程序员(甚至是所有程序员)心目中的“圣经”的话,

img{512x368}

那么同样由Brian W. Kernighan(K)参与编写的《The Go Programming Language》(也称tgpl)就是Go程序员心目中的“圣经”。

img{512x368}

本书模仿并致敬“The C Programming Language”的经典结构,从一个”hello, world”示例开始带领大家开启Go语言之旅。第二章程序结构是Go语言这个“游乐园”的向导图,了解它之后,我们就会迫不及待地奔向各个“景点”细致参观。Go语言规范中的所有“景点”在本书中都被覆盖到了,并且由浅入深,循序渐进:从基础数据类型到复合数据类型、从函数、方法到接口、从创新的并发goroutine到传统的基于共享变量的并发,从包、工具链到测试,从反射到低级编程(unsafe包)。作者行文十分精炼,字字珠玑,这与《The C Programming Language》的风格保持了高度的一致。书中的示例在浅显易懂的同时,又极具实用性并突出Go语言的特点(比如:并发web爬虫、并发非阻塞缓存等)。

读完本书后,你会有一种爱不释手,马上还要从头再读一遍的感觉,也许这就是“圣经”的魅力!

本书出版于2015年10月26日,也是既当年中旬Go 1.5这个里程碑版本发布后,Go社区的又一重大历史事件!并且Brian W. Kernighan老爷子的影响力让更多程序员加入到Go阵营,这也或多或少促成了Go成为下一个年度,即2016年年度TIOBE最佳编程语言。能得到Brian W. Kernighan老爷子青睐的编程语言只有C和Go,这也是Go的幸运。当然了如果老爷子是被Rob Pike或Ken Thompson通过私人关系邀请写书的,那就另当别论了,当然这纯属臆测,别当真^_^。

这本书的另一名作者Alan A. A. Donovan也并非等闲之辈,他是Go核心开发团队的成员,专注于Go工具链方面的开发。

现在唯一遗憾的是Brian W. Kernighan老爷子年事已高,不知道Go加入泛型后老爷子是否还有精力更新这本圣经。

该书中文版由七牛团队翻译,总体质量是不错的。建议Gopher们人手购置一本圣经“供奉”起来!^_^

下面对本书各个指数的评分:

  • 作者名气指数:5

  • 关注度指数:5

  • 内容实用指数:5

  • 经典指数:5

总分:20

4. 小结

Go书籍绝非“汗牛充栋”,预计Go增加泛型表达力增强后,市面上会有更多的技术书籍出炉。上面的某些经典也许还会出新版。而市面上Go书籍不多从另外一角度也可以理解成Go语言在国内还有巨大的发展空间与潜力。

努力吧,Gopher们!


只有写书者,才能体会到写者的艰辛!Go专栏:《改善Go语言编程质量的50个有效实践》也是我努力了一年多才打磨雕琢出来的心血之作。自从上线后,收到大家的热烈关注和好评!现在恰逢双11慕课大促,欢迎有意愿在Go这条技术路线上进阶的朋友们订阅,在学习过程中欢迎随时反馈和交流!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中!欢迎小伙伴们学习支持!双十一慕课网优惠空前!别错过机会哦!

img{512x368}
微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats