golang(初级)

258 阅读13分钟

常量陷阱

目前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

总结:

  1. defer的数据结构跟一般函数类似,记录了地址和执行到哪那行
  2. return实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的
  3. defer对函数内变量的操作其实只是操作一份拷贝,然而操作地址是可以进行修改的

string

rainbowmango.gitbook.io/go/chapter0…
总结: string的实现就是一个 pointer 加一个 len, 创建字符串的时候先创建 stringStruct, 然后转化成 string , byte[]string 基本都是拷贝,然后有几种情况不是,字符串不可修改,在字符串拼接的时候其实就是创建了一块新的内存,创建内存方法返回切片和string, 两者共享内存,所以只需要将拼接的字符串拷贝到切片中就行了

slice

总结:

  1. slice 数据结构就是 pointer len cap, 然后 pointer 指向内存中的数组,当str[:]的时候其实就是新建了个数据结构,分割出来的切片和原切片指向同一个内存,所以2个切片互相影响,注意如果某个切片触发了扩容那结果就是2个切片不共有同一个内存了,在触发扩容后,2个切片就分道扬镳了,然后因为公用也就垃圾回收所以要注意
  2. 使用make创建切片其实就是创建了底层内存,然后返回切片,数组创建就是将数组当作底层内存,分割是共用底层内存,然后new操作返回一个指向已清零内存的指针
  3. slice与unsafe.Pointer可以互转
  4. 切片的扩容,前期是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的地址
}

总结:

  1. 2个结构,一个是Hmap,一个是bmap
  2. 原理就是 keyhash,然后获得一个16位的数字,低8位定位在那个 buckets 中,每个 bucket 都存8个数据,用高8位进行循环确定是否在这个 bucket中,在就再次确认 key是否相同,不在或不同就去 bucketoverflow 中寻找,然后当存储的量超过负载因子就触发扩容,扩容是增量也就是每次使用才移动,所以hmap会存储上一个hmap也就是存在oldbuckets上,当所有都移过去后就会删除oldbuckets
  3. bmap上的key/value是key1key2...value1value2..这样排列,这样是为了防止字节对齐带来的空间浪费
  4. 负载因子 = 键数量/bucket数量,有个宏设置可以设置负载因子

扩容条件

  1. 负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
  2. 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

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个环形缓冲,1个发送协程队列,一个接收协程队列,然后元素类型大小,缓冲的下标,长度,互斥锁
  2. 发送数据流程-> 检测接收队列,有就取出协程,然后写入数据,没有就判定缓冲,有空闲就写入,没有就将当前协程加入待发送队列
  3. 接收数据流程-> 检测发送队列,有就判定下缓冲区,缓冲区有就取缓冲区数据,然后将发送队列中的数据放入缓冲区的队尾,唤醒该协程,缓冲区没有就直接拿协程的数据,然后唤醒该协程。发送队列没有,就判定下是否有数据,有就从缓冲区取数据,没有就将当前协程放入待读队列中,等待唤醒
  4. 关闭chan会把待接收队列的协程都唤醒,然后写入nill,所有接收的时候需要判定nill, 然后待发送也会全部唤醒,但是这些协程会panic
  5. 关闭还未初始化的chan, 关闭已经关闭的chan, 向关闭的chan写数据都会panic
  6. select语句的多个case执行顺序是随机的, select的case语句读channel不会阻塞
  7. 通过range可以持续从channel中读出数据,好像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样

select

case的数据结构

type scase struct {
    c           *hchan         // chan
    kind        uint16
    elem        unsafe.Pointer // data element
}
  • c代表当前操作的chan
  • kind代表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随机序列pollorderscasechan地址序列lockorder
  • pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。
  • lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的。
  • ncases表示scase数组的长度
  • 返回值int, 返回选中的编号
  • 返回值bool, 是否成功从chan中读取了数据
  1. select的case执行是随机的
  2. 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的结构
  1. locker代表是否已经锁定,1已经锁定,2没有锁定
  2. starving代表是否有协程阻塞超过1ms,1有(饥饿),2没有
  3. Waiter表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量
  4. Woken表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中

自旋

  1. 自旋对应于CPU的"PAUSE"指令,CPU对该指令什么都不做,相当于CPU空转,对程序而言相当于sleep了一小段时间,时间非常短,当前实现是30个时钟周期
  2. 自旋过程中会持续探测Locked是否变为0,连续两次探测间隔就是执行这些PAUSE指令,它不同于sleep,不需要将协程转为睡眠状态
  • 自旋次数要足够小,通常为4,即自旋最多4次
  • CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁
  • 协程调度机制中的Process数量要大于1,比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋
  • 协程调度机制中的可运行队列必须为空,否则会延迟协程调度

抢夺锁的过程

  1. A协程抢夺锁

  2. 抢夺成功,给locker改为1

  3. 抢夺失败,判定是否可以进行自旋

  4. 可以自旋,进行自旋

  5. 如果自选中抢到锁,然后肯定有一个协程被唤醒然后没有抢到锁,然后那个协程会在阻塞前看是否大于1ms,如果大于就设置starving(饥饿模式)然后进入阻塞(等待计数加1), 小于就进入阻塞(等待计数加1)

  6. 处于饥饿模式下,不会启动自旋过程,也即一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1(只要有抢夺锁进入阻塞就会加1)

  7. 不可以自旋,进入阻塞模式等待信号量唤醒(等待计数加1)

  8. work状态其实为了加锁和解锁之间进行通讯,其实就是标明有协程在自旋,不要发送信号量唤醒协程了

重复解锁会panic

因为重复解锁会唤醒多个协程,多个协程会触发枪锁,然后就会增加实现lock的难度,也会出现不必要的争抢

RWMutex

type RWMutex struct {
    w           Mutex  //用于控制多个写锁,获得写锁首先要获取该锁,如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
    writerSem   uint32 //写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
    readerSem   uint32 //读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
    readerCount int32  //记录读者个数
    readerWait  int32  //记录写阻塞时读者个数
}

Lock加锁过程

  1. 抢夺互斥锁
  2. 成功,原子操作将readerCount设置为负数, 也就是减去rwmutexMaxReaders,将返回的值加上 rwmutexMaxReaders
  3. 返回值等于0切返回值加上readerWaitreaderWait原子操作等于0原子操作成功,就代表读锁也锁定了且没有写锁等待,下面可以尽情操作了
  4. 失败就阻塞了,如果互斥锁获取失败直接会被互斥锁阻塞

Unlock解锁过程

  1. readerCount加回rwmutexMaxReaders
  2. 讲获取的读锁的数量依次唤醒,只唤醒当前数量的读锁,后面加的不唤醒
  3. 解除互斥锁

RLock

  1. 原子操作将readerCount加一
  2. 加完的数小于0就代表有读锁,所以这时候阻塞
  3. 大于或等于0就代表获取到了读锁

RUnlock

  1. 原子操作将readerCount减1,
  2. 减完发现小于0就代表有写锁正在等待
  3. 然后将写锁readerWait减去1等于0,也即当时写锁等待的读锁数量,往后的读锁都是阻塞状态,然后将写锁解除阻塞
  4. 大于0或等于0都不用管,因为读锁还没解完或者没有写锁抢占