【学习笔记】golang基础2

98 阅读4分钟

golang基础2

1.接口、方法、断言

一个结构体实现某个interface所有方法,则默认实现了这个接口。

go的接口行为相对隐式,不像java那么明显。

关于类型断言

空接口可以实现类型断言。

c.(string)

这种形式即是代表把c类型转换成string的意思。

关于方法

当一个方法对结构体实现,则编译会自动实现方法对该结构体的指针实现。

而用结构体指针实现的方法,不会自动生成结构体实体方法。

关于空接口

新版叫any,其实就是泛型。

底层是eface,有两个字段,type和ptr。当两个字段都是nil,它是nil接口。

关于nil

对于某类型的一个初始空值。pointer,channel,func,interface,map,or slice type。

nil是有类型的。

关于空结构体

统一指向一个空结构体地址。初始化的指针指向。节约了内存。

2.内存对齐

地址被对齐系数(一般是该类型长度,特殊的有string大小是16,对齐系数是8)整除

关于结构体内存对齐

字段偏移量为{自身大小,对齐系数}min\{自身大小,对齐系数\}_{min}的倍数。

结构体长度为{最大字段长度,系统字长}min\{最大字段长度,系统字长\}_{min}的倍数

由于结构体的建构顺序是由字段顺序从小到大分配内存,可以通过改变字段顺序来优化结构体内存配置。

结构体对齐系数是其字段最大对齐系数。

结构体内放置空结构体,需要给该空结构体地址补齐字长,避免地址撞车,因为空结构体没有长度。

3.协程

类并发行为不再是cpu在线程之间来回切换,而是每个线程在协程之间来回切换。

关于GPM模型

有现成的说法可以快速参阅,重点看图。

haoxuebing.github.io/go%E8%BF%9B…

关于抢占式调度

blog.csdn.net/caspar_note…

4.锁

atomic操作:硬件层面的加锁机制,原子锁。原子可以理解为最小单位。

底层有一个sema锁,是一个记录协程的等待堆树(平衡二叉树)。

为了方便描述,其实可以直接把等待的堆树(信号量堆)称作sema堆。还有goroutine(协程)我们简称g。

关于互斥锁

Mutex:互斥锁。带两个字段:state和sema。

有两种模式,正常模式和饥饿模式。

在正常模式下,如果一个g尝试获取锁,但发现锁已经被其他 g持有,会进行一定次数的自旋(等待释放,约4-6次)。如果还没获得锁,会进入等待队列(sema堆)挂起,等待被唤醒。

如果有一个 g被唤醒后,发现自己多次被其他新来的 g 抢占(即新的 Goroutine 多次在它前面成功获取了锁),这种情况会触发锁进入饥饿模式。进入starving后,新来的g(协程)全部进入sema树而不会抢锁(也就是CAS,compareAndSwap),而原来sema树中排在前面的g会优先获得锁,直到sema树清空就恢复正常模式。

这个sema堆先进先出(FIFO),因此也可以看做队列。

关于读写锁

读比写多场景适用。读操作线程安全。

互斥锁的写锁由Mutex实现,然后有读锁和写锁各有一个自己维持的队列。

总的来说,写的时候有必要停下读写,而读的时候就没事了。

关于waitGroup

等待协程组完成用,比如进入游戏之前我需要把所有加载基础组件的事情完成,然后才能进入游戏。这时候加载基础组件就需要用到waitGroup。那么在加载游戏之前,waitGroup把任务数记录,每完成一个任务进行Done(),也就是count--。在协程后面使用Wait(),等待所有waitGroup的完成。本质上相当于一组协程等待另一组协程(后续的协程),等待队列还是用sema实现。计数归零,则sema中的下一组被唤醒。

另外锁相关的结构都不可拷贝,因为他是掌管线程安全的东西,如果拷贝了就也是线程不安全。永远不要拷贝锁,也不要拷贝带锁结构体。

关于Once

让一个方法只能被协程调用一次。

底层可以用CAS实现,然而多协程去竞争改值容易遇到问题。

用Mutex就可以,让没抢到的人陷入sema休眠。然后CAS,后面直接判断某个flag即可快速跳过。

关于死锁

死锁指相互等待对方释放的锁。

可以用简单代码进行表述:

package main

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

func main() {
    var mu1, mu2 sync.Mutex

    go func() {
        mu1.Lock()
        fmt.Println("Goroutine 1: locked mu1")
        time.Sleep(time.Second)
        
        mu2.Lock() // 在这里会等待 mu2 被释放
        fmt.Println("Goroutine 1: locked mu2")
        mu2.Unlock()
        
        mu1.Unlock()
    }()

    go func() {
        mu2.Lock()
        fmt.Println("Goroutine 2: locked mu2")
        time.Sleep(time.Second)
        
        mu1.Lock() // 在这里会等待 mu1 被释放
        fmt.Println("Goroutine 2: locked mu1")
        mu1.Unlock()
        
        mu2.Unlock()
    }()

    time.Sleep(3 * time.Second)
}

但现实情况比较复杂,复杂起来可能肉眼一眼出不来。因此我们可以借助一些工具。下面有三个方案:

1.锁拷贝会导致死锁。vet工具可以诊断某文件是否有锁拷贝。

2.race工具可以发现隐含的数据竞争问题。

3.一个死锁检测项目github.com/sasha-s/go-…