常量陷阱
目前golang没有常量陷阱这一说
总结: 就是编译出一个程序里面引用了一个库,然后用了该库里面的常量,编译过后该程序里面就硬编码了常量的值,然后有一天更新了库,但是源程序没有重新编译,这就会造成2边不对应
interface
如何判断 interface 变量存储的是哪种类型
if t, ok := i.(*S); ok {
fmt.Println("s implements I", t)
}
上面代码其实就代表着类型强转
switch t := i.(type) {
case *S:
fmt.Println("i store *S", t)
case *R:
fmt.Println("i store *R", t)
}
获取接口的类型是什么
空的 interface
空的interface可以接收任何类型(注意是类型),但是[]interface{}是不能接收slice
interface的实现类注意点
type I interface {
Set(int)
}
func f(i I){
i.Set(10)
}
//1
func(s *S) Set(age int) {
s.Age = age
}
//2
func(s S) Set(age int) {
s.Age = age
}
//1.1
s := S{}
f(&s)
//2.1
s := S{}
f(&s)
f(s)
也就是说实现接口的类型要是方法是*那就只能用地址,如果不是随便用那种
defer
type _defer struct {
sp uintptr //函数栈指针
pc uintptr //程序计数器
fn *funcval //函数地址
link *_defer //指向自身结构的指针,用于链接多个defer
}
func foo() (ret int) {
defer func() {
ret++
}()
return 0
}
//返回 1
总结:
- defer的数据结构跟一般函数类似,记录了地址和执行到哪那行
- return实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的
- defer对函数内变量的操作其实只是操作一份拷贝,然而操作地址是可以进行修改的
string
rainbowmango.gitbook.io/go/chapter0…
总结: string的实现就是一个 pointer 加一个 len, 创建字符串的时候先创建 stringStruct, 然后转化成 string , byte[] 转 string 基本都是拷贝,然后有几种情况不是,字符串不可修改,在字符串拼接的时候其实就是创建了一块新的内存,创建内存方法返回切片和string, 两者共享内存,所以只需要将拼接的字符串拷贝到切片中就行了
slice
总结:
- slice 数据结构就是
pointerlencap, 然后pointer指向内存中的数组,当str[:]的时候其实就是新建了个数据结构,分割出来的切片和原切片指向同一个内存,所以2个切片互相影响,注意如果某个切片触发了扩容那结果就是2个切片不共有同一个内存了,在触发扩容后,2个切片就分道扬镳了,然后因为公用也就垃圾回收所以要注意 - 使用make创建切片其实就是创建了底层内存,然后返回切片,数组创建就是将数组当作底层内存,分割是共用底层内存,然后new操作返回一个指向已清零内存的指针
- slice与unsafe.Pointer可以互转
- 切片的扩容,前期是2倍,大于1024就是1/4
map
type struct Hmap {
uint8 B; // 可以容纳2^B个项
count int // 当前保存的元素个数
uint16 bucketsize; // 每个桶的大小
byte *buckets; // 2^B个Buckets的数组
byte *oldbuckets; // 前一个buckets,只有当正在扩容时才不为空
};
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
总结:
- 2个结构,一个是
Hmap,一个是bmap - 原理就是
key做hash,然后获得一个16位的数字,低8位定位在那个buckets中,每个bucket都存8个数据,用高8位进行循环确定是否在这个bucket中,在就再次确认key是否相同,不在或不同就去bucket的overflow中寻找,然后当存储的量超过负载因子就触发扩容,扩容是增量也就是每次使用才移动,所以hmap会存储上一个hmap也就是存在oldbuckets上,当所有都移过去后就会删除oldbuckets bmap上的key/value是key1key2...value1value2..这样排列,这样是为了防止字节对齐带来的空间浪费负载因子 = 键数量/bucket数量,有个宏设置可以设置负载因子
扩容条件
- 负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
- overflow数量 > 2^15时,也即overflow数量超过32768时
等量扩容
会有一种极端情况会形成链表的结构,所以有时候会触发一次等量扩容,buckets数量不变,经过重新组织后overflow的bucket数量会减少,即节省了空间又会提高访问效率
nil的语义
任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil
struct
type Server struct {
ServerName string `key1: "value1" key11:"value11"`
ServerIP string `key2: "value2"`
}
数据结构:
// A StructField describes a single field in a struct.
type StructField struct {
// Name is the field name.
Name string
...
Type Type // field type
Tag StructTag // field tag string
...
}
type StructTag string
反射获取:
s := Server{}
st := reflect.TypeOf(s)
field1 := st.Field(0)
fmt.Printf("key1:%v\n", field1.Tag.Get("key1"))
fmt.Printf("key11:%v\n", field1.Tag.Get("key11"))
总结: tag存在意义其实和java中的注解是一样的,变量或者说属性可以用来描述很多东西,但是他们本身用什么描述呢,这时候就可以用tag, 当然你也可以用另一个变量描述这个变量,但是这样有些包比如json,orm实现就不优雅了
const
- rainbowmango.gitbook.io/go/chapter0… const的结构:
ValueSpec struct {
Doc *CommentGroup // associated documentation; or nil
Names []*Ident // value names (len(Names) > 0)
Type Expr // value type; or nil
Values []Expr // initial values; or nil
Comment *CommentGroup // line comments; or nil
}
总结: 单行const定义在go中就是一个ValueSpec, 然后块const就是一个ValueSpec的切片,一行 const 定义多个name其实就是Names存储多个名称, 然后iota其实就是切片的下标
chan
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁,chan不允许并发读写
}
总结:
- 整体数据结构就可知1个环形缓冲,1个发送协程队列,一个接收协程队列,然后元素类型大小,缓冲的下标,长度,互斥锁
- 发送数据流程-> 检测接收队列,有就取出协程,然后写入数据,没有就判定缓冲,有空闲就写入,没有就将当前协程加入待发送队列
- 接收数据流程-> 检测发送队列,有就判定下缓冲区,缓冲区有就取缓冲区数据,然后将发送队列中的数据放入缓冲区的队尾,唤醒该协程,缓冲区没有就直接拿协程的数据,然后唤醒该协程。发送队列没有,就判定下是否有数据,有就从缓冲区取数据,没有就将当前协程放入待读队列中,等待唤醒
- 关闭chan会把待接收队列的协程都唤醒,然后写入nill,所有接收的时候需要判定nill, 然后待发送也会全部唤醒,但是这些协程会panic
- 关闭还未初始化的chan, 关闭已经关闭的chan, 向关闭的chan写数据都会panic
- select语句的多个case执行顺序是随机的, select的case语句读channel不会阻塞
- 通过range可以持续从channel中读出数据,好像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样
select
case的数据结构
type scase struct {
c *hchan // chan
kind uint16
elem unsafe.Pointer // data element
}
c代表当前操作的chankind代表case的类型,也就是caseRecv(读),caseSend(写),caseDefault(默认)elem代表缓冲区,当kind是读类型的时候就代表的是读取chan的数据存放地址,当kind是发送类型的时候就代表的是写入chan的数据存放地址
select的实现逻辑
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
//1. 锁定scase语句中所有的channel
//2. 按照随机顺序检测scase中的channel是否ready
// 2.1 如果case可读,则读取channel中数据,解锁所有的channel,然后返回(case index, true)
// 2.2 如果case可写,则将数据写入channel,解锁所有的channel,然后返回(case index, false)
// 2.3 所有case都未ready,则解锁所有的channel,然后返回(default index, false)
//3. 所有case都未ready,且没有default语句
// 3.1 将当前协程加入到所有channel的等待队列
// 3.2 当将协程转入阻塞,等待被唤醒
//4. 唤醒后返回channel对应的case index
// 4.1 如果是读操作,解锁所有的channel,然后返回(case index, true)
// 4.2 如果是写操作,解锁所有的channel,然后返回(case index, false)
}
cas0代表scase数组的首地址order0为一个两倍case0数组长度的buffer, 保存scase随机序列pollorder和scase中chan地址序列lockorderpollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的。ncases表示scase数组的长度- 返回值
int, 返回选中的编号 - 返回值
bool, 是否成功从chan中读取了数据
- select的case执行是随机的
- select没有default的情况下,又没匹配到case的情况下会阻塞当前协程,由此可见空select会直接阻塞,然后除非该协程被唤醒
总结:
这里也没搞懂为啥弄一个2倍scase大小的buffer,原理好像是循环order0的前半部分进行获取scase来进行判定是否ready, 这里原数组cas0是有序的,但是遍历这个无序的order0从而达到随机case的效果
range
range for slice
// The loop we generate:
// for_temp := range
// len_temp := len(for_temp)
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = for_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
- 可知遍历切片是定长遍历的,所以遍历的时候添加元素不会死循环,然后index和value会取出来再赋值,所以能不接收index/value就不接收
range for map
// The loop we generate:
// var hiter map_iteration_struct
// for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
// index_temp = *hiter.key
// value_temp = *hiter.val
// index = index_temp
// value = value_temp
// original body
// }
- 循环map是经过hash的所以不能边循环边添加,index和value同切片
range for channel
// The loop we generate:
// for {
// index_temp, ok_temp = <-range
// if !ok_temp {
// break
// }
// index = index_temp
// original body
// }
- 当chan关闭的时候会跳出循环,没有元素(
index_temp, ok_temp = <-range)就会阻塞
mutex
type Mutex struct {
state int32
sema uint32
}
- state的结构
- locker代表是否已经锁定,1已经锁定,2没有锁定
- starving代表是否有协程阻塞超过1ms,1有(饥饿),2没有
- Waiter表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量
- Woken表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中
自旋
- 自旋对应于CPU的"PAUSE"指令,CPU对该指令什么都不做,相当于CPU空转,对程序而言相当于sleep了一小段时间,时间非常短,当前实现是30个时钟周期
- 自旋过程中会持续探测Locked是否变为0,连续两次探测间隔就是执行这些PAUSE指令,它不同于sleep,不需要将协程转为睡眠状态
- 自旋次数要足够小,通常为4,即自旋最多4次
- CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁
- 协程调度机制中的Process数量要大于1,比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋
- 协程调度机制中的可运行队列必须为空,否则会延迟协程调度
抢夺锁的过程
-
A协程抢夺锁
-
抢夺成功,给locker改为1
-
抢夺失败,判定是否可以进行自旋
-
可以自旋,进行自旋
-
如果自选中抢到锁,然后肯定有一个协程被唤醒然后没有抢到锁,然后那个协程会在阻塞前看是否大于1ms,如果大于就设置starving(饥饿模式)然后进入阻塞(等待计数加1), 小于就进入阻塞(等待计数加1)
-
处于饥饿模式下,不会启动自旋过程,也即一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1(只要有抢夺锁进入阻塞就会加1)
-
不可以自旋,进入阻塞模式等待信号量唤醒(等待计数加1)
-
work状态其实为了加锁和解锁之间进行通讯,其实就是标明有协程在自旋,不要发送信号量唤醒协程了
重复解锁会panic
因为重复解锁会唤醒多个协程,多个协程会触发枪锁,然后就会增加实现lock的难度,也会出现不必要的争抢
RWMutex
type RWMutex struct {
w Mutex //用于控制多个写锁,获得写锁首先要获取该锁,如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
writerSem uint32 //写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
readerSem uint32 //读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
readerCount int32 //记录读者个数
readerWait int32 //记录写阻塞时读者个数
}
Lock加锁过程
- 抢夺互斥锁
- 成功,原子操作将
readerCount设置为负数, 也就是减去rwmutexMaxReaders,将返回的值加上rwmutexMaxReaders - 返回值等于
0切返回值加上readerWait对readerWait原子操作等于0原子操作成功,就代表读锁也锁定了且没有写锁等待,下面可以尽情操作了 - 失败就阻塞了,如果互斥锁获取失败直接会被互斥锁阻塞
Unlock解锁过程
- 将
readerCount加回rwmutexMaxReaders - 讲获取的读锁的数量依次唤醒,只唤醒当前数量的读锁,后面加的不唤醒
- 解除互斥锁
RLock
- 原子操作将
readerCount加一 - 加完的数小于
0就代表有读锁,所以这时候阻塞 - 大于或等于
0就代表获取到了读锁
RUnlock
- 原子操作将
readerCount减1, - 减完发现小于
0就代表有写锁正在等待 - 然后将写锁
readerWait减去1等于0,也即当时写锁等待的读锁数量,往后的读锁都是阻塞状态,然后将写锁解除阻塞 - 大于
0或等于0都不用管,因为读锁还没解完或者没有写锁抢占