go语言sync.map源码阅读-基于go1.20

更方便的在微信公众号阅读文章可以关注公众号:海生的go花园
图片

一、什么是 sync.Map

sync.Map,是一种可以像Go语言中的Map那样以Key/Value格式将值存储在内存中。
sync通用Mutex,可以在多个goroutine并发执行上也可以安全使用。
我们可以把它当做和gocache或者Redis一样的缓存来使用。
适用的场景为 写少,读多的地方。
我们在命令行中输入:go doc sync.map
基于go1.20版本,可以使用的功能如下。

type Map struct {}
// 常用
func (m *Map) Store(key, value any)
func (m *Map) Delete(key any)
func (m *Map) Load(key any) (value any, ok bool)
func (m *Map) Range(f func(key, value any) bool)
// 其他
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
func (m *Map) Swap(key, value any) (previous any, loaded bool)
func (m *Map) CompareAndDelete(key, old any) (deleted bool)
func (m *Map) CompareAndSwap(key, old, new any) bool

现在我们来一边使用,一边学习。

1.1 Store存储/Range遍历

我们顺序的往sync.map存储多个值,然后遍历sync.map,输出刚才的key/value

func TestStoreAndRange(t *testing.T) {
    // 初始化
    m := sync.Map{}

    // 使用Store,增加 key/value
    m.Store("Key 1", "Value 1")
    m.Store("Key 2", "Value 2")
    m.Store("Key 3", "Value 3")
    m.Store("Key 4", "Value 4")
    m.Store("Key 5", "Value 5")


    // 遍历sync.map 来获取刚才存储的 key/value
    m.Range(func(key, value any) bool {
        fmt.Printf("Key: %v(Type: %T) -> Value: %v(Type: %T)\n", key, key, value, value)
        return true
    })
}

输出的结果,每一次都不同:

Key: Key 3 -> Value: Value 3
Key: Key 4 -> Value: Value 4
Key: Key 5 -> Value: Value 5
Key: Key 1 -> Value: Value 1
Key: Key 2 -> Value: Value 2

遍历输出的特征为:无序
因为sync.map这个结构体,存储的时候还是go map。

1.2 Delete 删除

我们在上面的代码基础上,删除一些key,然后再遍历出整个sync.map。

func TestDelete(t *testing.T) {
    // 初始化
    m := sync.Map{}

    // 使用Store,增加 key/value
    m.Store("Key 1", "Value 1")
    m.Store("Key 2", "Value 2")
    m.Store("Key 3", "Value 3")
    m.Store("Key 4", "Value 4")
    m.Store("Key 5", "Value 5")

    // 遍历sync.map 来获取刚才存储的 key/value
    m.Range(func(key, value any) bool {
        fmt.Printf("Key: %v -> Value: %v\n", key, value)
        return true
    })

    // 删除
    m.Delete("Key 2")
    m.Delete("Key 3")
    m.Delete("Key5")
    m.Delete("Key 10")

    // 遍历sync.map 来获取刚才存储的 key/value
    fmt.Printf("删除后数据:\n")
    m.Range(func(key, value any) bool {
        fmt.Printf("Key: %v -> Value: %v\n", key, value)
        return true
    })
}

1.3 Load 读取数据

我们读取数据有三种 load 读取数据,根据我们的需要来使用。

func (m *Map) Load(key any) (value any, ok bool)
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)

我们还是准备才开的数据:

// 初始化
m := sync.Map{}

// 使用Store,增加 key/value
m.Store("Key 1", "Value 1")
m.Store("Key 2", "Value 2")
m.Store("Key 3", "Value 3")
m.Store("Key 4", "Value 4")
m.Store("Key 5", "Value 5")

// 遍历sync.map 来获取刚才存储的 key/value
m.Range(func(key, value any) bool {
fmt.Printf("Key: %v -> Value: %v\n", key, value)
return true
})

1.3.1 使用load()方法:

func (m *Map) Load(key any) (value any, ok bool)

Load方法 返回 map中key 存储的值,或者返回nil
ok返回值 表示 这个值是否存在map中。

t.Log(m.Load("Key 1"))  // key 存在
t.Log(m.Load("Key 11")) // key 不存在

输出:

Value 1 true
 false

1.3.2 使用LoadOrStore()方法:

func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)

LoadOrStore 返回指定key现在存在的值
loaded返回值为 如果是load读取返回true,如果是store返回false

t.Log(m.LoadOrStore("Key 1", "New Value 1")) // key 存在
t.Log(m.LoadOrStore("Key 2", nil))           // key 存在
t.Log(m.LoadOrStore("Key 11", "Value 11"))   // key 不存在
t.Log(m.LoadOrStore("Key 12", nil))          // key 不存在

输出:

Value 1 true
Value 2 true
Value 11 false
 false

1.3.3 使用LoadAndDelete()方法:

func (m *Map) LoadAndDelete(key any) (value any, loaded bool)

LoadAndDelete 删除指定的key,返回先前的值。
loaded 返回值,是key是否存在。

t.Log(m.LoadAndDelete("Key 1"))  // key 存在
t.Log(m.LoadAndDelete("Key 11")) // key 不存在

输出:

Value 1 true
 false

在这里可以休息一下

留白互动时间:
在这里可以休息一下,消化一下上面的使用。

1.4 多个goroutine并发

在多个goroutine并发的场景下,可以安全使用。

func TestGoroutineSyncMap(t *testing.T) {
    m := sync.Map{}

    for i := 0; i < 1000; i++ {
        go func(i int) {
            m.Store(i, i)
        }(i)
    }

    <-time.After(1000 * time.Millisecond)
    sum := 0
    m.Range(func(key interface{}, value interface{}) bool {
        fmt.Printf("Key: %v -> Value: %v\n", key, value)
        sum++
        return true
    })

    t.Logf("m的大小为 %d", sum)
}

我们可以在并发的情况下查看一下 go map的使用

func TestGoroutineMap(t *testing.T) {
    m := make(map[int]int, 0)

    for i := 0; i < 1000; i++ {
        go func(i int) {
            m[i] = i
        }(i)
    }

    <-time.After(1000 * time.Millisecond)

    for key, value := range m {
        fmt.Printf("Key: %v -> Value: %v\n", key, value)
    }

    t.Logf("m的大小为 %d", len(m))
}

输出报错:

fatal error: concurrent map writes

goroutine 404 [running]:
command-line-arguments.TestGoroutineMap.func1(0x181)
    /Users/staff/study/sync-map/goroutine_test.go:35 +0x3

因为go map 不是并发安全的,我们如要在并发的情况下使用,需要结合sync.Mutex互斥锁

func TestGoroutineMapMutex(t *testing.T) {
    m := make(map[int]int, 0)

    var mu sync.Mutex
    for i := 0; i < 1000; i++ {
        go func(i int) {
            mu.Lock()
            m[i] = i
            mu.Unlock()
        }(i)
    }

    <-time.After(1000 * time.Millisecond)

    for key, value := range m {
        fmt.Printf("Key: %v -> Value: %v\n", key, value)
    }

    t.Logf("m的大小为 %d", len(m))
}

而sync map,其实本质上,就是 go map + sync.Mutex 的这个版本的,升格版本。
这个版本的代码,都读写同一个 map,sync.map 进行了读写分离,通过两个map,来提高效率。
对比一下:

两种方案对比 go map + sync.Mutex sync.Map
占用内存map 1个map 2个map,2倍的内存占用
写锁
读锁

sync.Map的方案主要是对锁的优化,在只读的情况下,无锁。
通过增加 一个只读 read map 的方式来实现。
用增加一倍的内存,来实现,读效率的提升,适用于写一次,读n次的情况。

1.5 go map + sync.Mutex方案

在go源码:src/sync/map_reference_test.go文件里,官方实现了和sync.map同样的接口。

// RWMutexMap is an implementation of mapInterface using a sync.RWMutex.
type RWMutexMap struct {
    mu    sync.RWMutex
    dirty map[any]any
}

func (m *RWMutexMap) Load(key any) (value any, ok bool) {
    m.mu.RLock()
    value, ok = m.dirty[key]
    m.mu.RUnlock()
    return
}

func (m *RWMutexMap) Store(key, value any) {
    m.mu.Lock()
    if m.dirty == nil {
        m.dirty = make(map[any]any)
    }
    m.dirty[key] = value
    m.mu.Unlock()
}
....这里我截取一部分,详细的可以看源码。

1.6 和 go map + sync.Mutex方案对比

在go源码:src/sync/map_bench_test.go文件里,官方有benchmark基准测试。我们可以看一下结果

BenchmarkLoadMostlyHits/*sync_test.RWMutexMap
BenchmarkLoadMostlyHits/*sync_test.RWMutexMap-8              15851244            80.08 ns/op
BenchmarkLoadMostlyHits/*sync.Map
BenchmarkLoadMostlyHits/*sync.Map-8                          210334969             6.167 ns/op

sync.Map比go map 加锁,速度快10倍。

在这里可以休息一下

留白互动时间:
在这里可以休息一下,消化一下上面的使用。

二、sync.map实现原理

2.1 读写分离

sync.Map的架构是实时读写分离,如下图。
go语言sync.map源码阅读-基于go1.20_第1张图片
dirty里面是全量数据,read里面的数据是dirty某个节点,同步过去的上个时间段的全量数据

因为读写分离,我们写都是到 dirty 里面,而读在 read里面。那么才是开始,是下面的样子。 dirty 有值,read位空。
所有的 读,写都在 dirty。相当于只有一个 map。
go语言sync.map源码阅读-基于go1.20_第2张图片
这个就是我们 使用 go map + sync.mutex 互斥锁 架构。

2.2 什么时候把dirty数据复制到read中?

从上面的图,我们可以看出来,如果要提高效率,就是看read的击中率。
如果更多的数据,击中read,就不用走到dirty中。
我们在read中的操作是没有锁的,dirty的操作,都需要加锁。

互动环节:
在这里,有很多种场景的应用,这里大家可以发言环节,说一下自己的方案?

2.3 sync.Map把dirty同步到read的时机。

sync.Map{}这个结构体,在设计的时候,增加了一个字段misses,来记录未命中read,击中dirty的次数。当次数达到dirty的长度时候,会同步。
map结构体源码如下:

type Map struct {
    mu Mutex // sync.Mutex 互斥锁
    read atomic.Pointer[readOnly] //read数据库
    dirty map[any]*entry  //dirty 数据库
    misses int // 未命中read,击中dirty的次数。
}

dirty同步到read的源码如下:

// missLocked 当m.misses未击中 read map,走到 dirty map 中的 次数小于 len(m.dirty),
// 会一直走到这里,m.misses增加1次
func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    // 当 m.misses 达到 len(m.dirty)次数。
    // 把 dirty map 增加到 read map 中。(此时 read map中的m值为原来的 read + dirty; amended 重置为false )
    // 然后把 dirty map置空,m.misses归0,重新开始。
    m.read.Store(&readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

同步后,读写分离的状况如下:
go语言sync.map源码阅读-基于go1.20_第3张图片

2.4 思考:此时如果一个 store写入操作,我们应该怎么做?

在这里可以休息一下

留白互动时间:
在这里可以休息一下,消化一下上面的使用。


2.5 sync.Map当dirty刚同步完read为空的时候,store写入操作

我们可以看源码Store方法。

// Store sets the value for a key.
// Store 存储指定key的值
func (m *Map) Store(key, value any) {
    _, _ = m.Swap(key, value)
}

具体到当dirty刚同步到read为空的时候,这段代码为:

// dirtyLocked 初始化 dirty map 初始值 为 read map
func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }
    // 初始化 dirty map ,把 read map 里面的未删除值,全部同步到 dirty map中
    read := m.loadReadOnly()
    m.dirty = make(map[any]*entry, len(read.m))
    for k, e := range read.m {
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

主要的作用就是,把read里面的有效数据 同步到dirty里面。这样保持dirty要么为nil无数据,有数据的时候,就是 全量数据
此时我们回到读写分离的最原始的那张图。
go语言sync.map源码阅读-基于go1.20_第4张图片

三、sync.map源码阅读

3.1 map 和 readOnly ,entry 结构体

type Map struct {
    mu Mutex // sync.Mutex 互斥锁
    read atomic.Pointer[readOnly] // 存储read数据的readOnly指针类型
    dirty map[any]*entry  // 全量数据 dirty map
    misses int  //确认dirty同步到read时机用的,记录未命中read,击中dirty的次数
}

sync.map 主要的思想,读写分离。所以这里有两个map,
一个read,为了提高效率这里用了readOnly结构体原子化操作。
mu,主要是为了并发安全。
misses字段用来确认dirty同步到read时机用的,记录未命中read,击中dirty的次数

// readOnly is an immutable struct stored atomically in the Map.read field.
// readOnly 是一个 不可变的结构体,原子化存储在 sync.Map的read 字段中。
type readOnly struct {
    m       map[any]*entry // 其中map[key],key为泛型,任意类型。
    amended bool           // true if the dirty map contains some key not in m.
    // amended (简单说,就是记录一种状态的,标记dirty和read有无差异)
    // 1 :有差异,返回true。
    // 2 :没有差异。返回false
}

amended 字段来标记 dirty 和 read 有无差异。我们可以总结一下几种情况。

状况 amended 说明
新建sync.Map{} false
第1次Store true 第一次Store后,dirty有1个值,read为0
第1次Load读 false 第一次读,m.misses为1,此时len(dirty)为1,len(dirty)>=misses时候,会进行第一次同步,同步完,dirty和read数据一致,此时amended改成false
第2次Store true 第2次Store后,dirty有2个值,read为1,此时amended改成true,标记他们有差异
第n次 ----- -----
type entry struct {
    p atomic.Pointer[any] // p 泛型指针
}

这里的sync.Map里面存储的entry,有三种值的情况。

  1. nil 空值
  2. expunged 标记空泛型new(any),代表被删除的泛型值。
  3. any 任意类型的非nil有效值。

当我们从dirty复制到read的时候,他们两个map,都指向同一个&entry,这样在后面我们就可以通过cas修改值,修改后,无论从read还是dirty读取,都会是新的值
go语言sync.map源码阅读-基于go1.20_第5张图片

type Pointer[T any] struct {
    // Mention *T in a field to disallow conversion between Pointer types.
    // See go.dev/issue/56603 for more details.
    // Use *T, not T, to avoid spurious recursive type definition errors.
    _ [0]*T

    _ noCopy
    v unsafe.Pointer
}

atomic.Pointer常用的方法有:

// 原子化加载 指针的值
func (x *Pointer[T]) Load() *T { return (*T)(LoadPointer(&x.v)) }

// 原子化存储,指针的值
func (x *Pointer[T]) Store(val *T) { StorePointer(&x.v, unsafe.Pointer(val)) }

// 原子化,交换指针的值
func (x *Pointer[T]) Swap(new *T) (old *T) { return (*T)(SwapPointer(&x.v, unsafe.Pointer(new))) }

// cas
func (x *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) {
    return CompareAndSwapPointer(&x.v, unsafe.Pointer(old), unsafe.Pointer(new))
}
什么是CAS?

 CAS全称compare and swap——比较并替换,它是并发条件下实现原子操作,修改数据的一种机制,包含三个操作数:

  • 需要修改的数据的内存地址(V);
  • 对这个数据的旧预期值(A);
  • 需要将它修改为的值(B);

3.2 Store存储 key/value数据

语法如下:

func (m *Map) Store(key, value any)

在Store的时候,其实有关于key有2种情况需要我们考虑。

Store情况 处理方式
未存在的key m.dirty[key] = newEntry(value)
已经存在的key 具体分析如下表格

已经存在的key,我们需要考虑他的值指针的情况,有2种情况。

已经存在的key 处理方式
只在dirty中,还没同步到read 修改对应key的值,e.swapLocked(&value)
同时存在dirty和read中 备注他们的是同一个&entry地址,cas修改,方法e.trySwap(&value)

我们先看一下Store的逻辑
go语言sync.map源码阅读-基于go1.20_第6张图片
接着我们看Store的源码实现

// Store sets the value for a key.
// Store 存储指定key的值
func (m *Map) Store(key, value any) {
    _, _ = m.Swap(key, value)
}

// Swap swaps the value for a key and returns the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
    read := m.loadReadOnly()
    // 1、key在 read map中, 更换key对应的值指针地址。
    if e, ok := read.m[key]; ok {
        if v, ok := e.trySwap(&value); ok {
            if v == nil {
                return nil, false
            }
            return *v, true
        }
    }

    // 走到这里的情况为,1 不在read中  2在read中,不过被删除了。
    m.mu.Lock()
    read = m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        // 1、如果key在 read map 存在,并且被删除了
        // read map 中 这个key 对应的值为 nil
        if e.unexpungeLocked() {
            // The entry was previously expunged, which implies that there is a
            // 该 entry实体记录 先前已被删除,
            // non-nil dirty map and this entry is not in it.
            // 意味着  dirty map 不为nil,且 这个 entry 不在里面。
            m.dirty[key] = e
        }
        if v := e.swapLocked(&value); v != nil {
            loaded = true
            previous = *v
        }
    } else if e, ok := m.dirty[key]; ok {
        // 2、如果key在 dirty map 存在
        if v := e.swapLocked(&value); v != nil {
            loaded = true
            previous = *v
        }
    } else {
        // 3、key不在read和dirty map中 ,一个新key
        if !read.amended {
            // 如果 dirty中没有新key
            // We're adding the first new key to the dirty map.
            // 第一次 增加新key 到 dirty map
            // Make sure it is allocated and mark the read-only map as incomplete.
            // 重要! dirtyLocked 这个方法会初始化 dirty map
            m.dirtyLocked()
            // 把 amended 改为 true
            m.read.Store(&readOnly{m: read.m, amended: true})
        }
        // 写入到 dirty map 中
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
    return previous, loaded
}

3.3 Delete

语法为

func (m *Map) Delete(key any) 

基本逻辑为:
go语言sync.map源码阅读-基于go1.20_第7张图片

源码请看演示:

// Delete deletes the value for a key.
// Delete 删除 指定key的值
func (m *Map) Delete(key any) {
    m.LoadAndDelete(key)
}

// LoadAndDelete deletes the value for a key, returning the previous value if any.
// LoadAndDelete 删除指定的key,返回先前的值。
// The loaded result reports whether the key was present.
// loaded 返回值,是key是否存在。
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
    read := m.loadReadOnly()
    e, ok := read.m[key]
    // read中没有,且dirty map中的key后来新增过key
    if !ok && read.amended {
        m.mu.Lock()
        read = m.loadReadOnly()
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // 删除dirty中的key
            delete(m.dirty, key)
            // Regardless of whether the entry was present, record a miss: this key
            // will take the slow path until the dirty map is promoted to the read
            // map.
            // miss增加1,直到 dirty map 下一次同步数据 到 read map
            m.missLocked()
        }
        m.mu.Unlock()
    }
    // read 或 dirty map 存在的时候,把entry记录的指针 置为nil
    if ok {
        return e.delete()
    }
    return nil, false
}

3.4 range

语法:

func (m *Map) Range(f func(key, value any) bool) {

逻辑图:
go语言sync.map源码阅读-基于go1.20_第8张图片

源码演示:

func (m *Map) Range(f func(key, value any) bool) {
    // We need to be able to iterate over all of the keys that were already
    // present at the start of the call to Range.
    // If read.amended is false, then read.m satisfies that property without
    // requiring us to hold m.mu for a long time.
    read := m.loadReadOnly()
    if read.amended {
        // m.dirty contains keys not in read.m. Fortunately, Range is already O(N)
        // (assuming the caller does not break out early), so a call to Range
        // amortizes an entire copy of the map: we can promote the dirty copy
        // immediately!
        m.mu.Lock()
        read = m.loadReadOnly()
        if read.amended {
            read = readOnly{m: m.dirty}
            m.read.Store(&read)
            m.dirty = nil
            m.misses = 0
        }
        m.mu.Unlock()
    }

    for k, e := range read.m {
        v, ok := e.load()
        if !ok {
            continue
        }
        if !f(k, v) {
            break
        }
    }
}

3.5 Load

语法:

func (m *Map) Load(key any) (value any, ok bool)

逻辑图:
go语言sync.map源码阅读-基于go1.20_第9张图片
或者这样展示
go语言sync.map源码阅读-基于go1.20_第10张图片
源码阅读:

// Load returns the value stored in the map for a key, or nil if no
// Load方法 返回 map中key 存储的值,或者返回nil,如果现在没有值。
// value is present.
// The ok result indicates whether value was found in the map.
// ok返回值 表示 这个值是否存在map中。
func (m *Map) Load(key any) (value any, ok bool) {
    read := m.loadReadOnly()
    e, ok := read.m[key]
    // 当read中没有,但dirty map中有值的时候,从dirty读取一次。
    if !ok && read.amended {
        // 从dirty map 中读值,需要加锁。
        m.mu.Lock()
        // Avoid reporting a spurious miss if m.dirty got promoted while we were
        // blocked on m.mu. (If further loads of the same key will not miss, it's
        // not worth copying the dirty map for this key.)
        read = m.loadReadOnly()
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // Regardless of whether the entry was present, record a miss: this key
            // 不管这条entry记录,是否存在,记录一次 未击中。m.misses 加一次
            // will take the slow path until the dirty map is promoted to the read
            // 这个key,会一直 走到这一个缓慢的逻辑中,直到dirty map升格到read map 中。
            // map.
            m.missLocked()
        }
        m.mu.Unlock()
    }
    // 当 dirty 和 read map中,都为false,没有这个key时候,直接返回。
    if !ok {
        return nil, false
    }
    // 当ok,有这个key的时候,返回read map中的值
    return e.load()
}

3.6 LoadOrStore

语法:

func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)

逻辑图:
go语言sync.map源码阅读-基于go1.20_第11张图片
源码阅读:

// LoadOrStore returns the existing value for the key if present.
// LoadOrStore 返回指定key现在存在的值
// Otherwise, it stores and returns the given value.
// 或者,可以存储并且返回指定的值
// The loaded result is true if the value was loaded, false if stored.
// loaded返回值为 如果是load读取返回true,如果是store返回false
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {
    // Avoid locking if it's a clean hit.
    // 如果是直接的命中read map,可以不用加锁
    read := m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        actual, loaded, ok := e.tryLoadOrStore(value)
        if ok {
            return actual, loaded
        }
    }

    // 未命中 read map
    m.mu.Lock()
    read = m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        // key在read map,但值被删除了,写入到dirty map中
        if e.unexpungeLocked() {
            m.dirty[key] = e
        }
        actual, loaded, _ = e.tryLoadOrStore(value)
    } else if e, ok := m.dirty[key]; ok {
        // 未命中read map,命中dirty map
        actual, loaded, _ = e.tryLoadOrStore(value)
        // 未击中read map,命中dirty map,m.misses 增加1次
        m.missLocked()
    } else {
        // 未命中read和dirty map。
        if !read.amended {
            // We're adding the first new key to the dirty map.
            // 增加第一个 key 到 dirty map
            // Make sure it is allocated and mark the read-only map as incomplete.
            // 确保 read map 的 amended 标记未 为true (代表dirty相对read有差异,增加了新key)
            m.dirtyLocked()
            m.read.Store(&readOnly{m: read.m, amended: true})
        }
        // 新增的key 保存到 dirty map (全量map)中
        m.dirty[key] = newEntry(value)
        actual, loaded = value, false
    }
    m.mu.Unlock()

    return actual, loaded
}

你可能感兴趣的:(go)