golang学习笔记和部分特性源码分析

channel 负责在语言层面提供goroutine的通信方式,类似unix的管道;主要还是进程内的goroutine通信,跨进程的通信还是用分布式系统实现
channel源码结构看出 由队列、类型信息、goroutine等待队列 锁组成
slice 动态数组 底层是数组的部分引用 方便扩容 传递;底层数组空间不足 会重新分配生成新的slice
type slice struct {
array unsafe.Pointer
len int
cap int
}
map 底层是hashtable 对应多个hashNode也就是bucket,每个bucket保存一组键值对
hmap = 当前保存元素个数 + buckets数组大小 + buckets指针;key经过hash运算落到某个bucket存储
bucket哈希桶 = 存储hash值的高8位(长度8的数组,哈希值低位相同的键会将哈 希值的高位存储在该数组中) + data(k-v数据 可以存储8哥kv) + overflow(溢出bucket的地址,指向下一个bucket)
hash冲突 两个以上的hash键被映射到同一个bucket 使用链地址法解决冲突;bucket超过8个键值对时会以链表的方式将创建的键值对连接起来
hash负载因子 = 键数量/bucket数量 hash表对因子的大小控制在合适大虾,超过阈值会rehash;redis的因子>1就会rehash,go会在6.5时rehash
扩容 : map新加入元素会检查扩容(以空间换时间),触发条件:负载因子>6.5 overflow数量 > 2^15 32768
增量扩容 等量扩容
struct go的struct声明可以附带tag做字段标记;主要用于反射场景,reflect包有操作Tag的方法,tag的写法也要遵循一定的规则 ·以空格分隔的 key:value 对·
go可以利用反射动态的给结构体的成员赋值 tag是结构体的一部分 可以利用tag特性 json orm也是这样利用tag的
iota 很多博客上这样说,iota在const关键字出现时被重置为0,const声明块中每新增一行iota值自增1
看过编译器代码后发现只有一条规则,iota代表了const声明块的行索引(从0开始),const声明还有个特点,即第一个常量必须指定一个表达 式,后续的常量如果没有表达式,则继承上面的表达式。
string Go标准库 builtin 给出了所有内置类型的定义,string是8bit字节集合,不一定是utf-8的编码
string可以是空 长度为0 但是不会是nil string对象不可修改
type stringStruct struct {
str unsafe.Pointer //字符串的首地址
len int //长度
}
str := "hello world" //声明string 先构建stringStruct,再转换成string;string的结构类似切片 但是没有cap
defer 延迟函数的调用 每次defer都会把函数压入栈,主栈函数返回之前调用延迟函数取出并执行;延迟函数的参数在defer时就确定下来了,FILO(defer 设计的目的是清理资源所以这样)
defer后面一定是一个函数 结构和函数一样(栈地址 程序计数器 函数地址) 多一个指针 指向下个defer;goroutine也有一个defer指针 指向defer的单链表
return不是原子操作,执行过程是: 保存返回值(若有)—>执行defer(若有)—>执行ret跳转,申请资源后立即使用defer关闭资源是好习惯
select golang的语言层面提供的多路IO复用的机制 检测多个channel是否ready;没有ready会一直阻塞或者走default;关闭的chan也是可读的
range 是遍历的一种方式; 数组 切片 Map channel sting等;index,value接收range返回值会发生一次数据拷贝
for-range对切片进行遍历,value的值特别大或者是string这样对value的赋值是多余的slice[index]引用value的值
动态遍历,循环内给slice加元素不会影响循环次数;slice在range前会获取循环次数
map遍历 没有指定循环次数 但是随机的插入顺序 不能保证新插入的数据被遍历到
chan 遍历时 没有数据会阻塞
Mutex 互斥锁 并发程序中对共享资源的访问控制,非常简单 只暴露了 Lock 和Unlock两个方法,重复解锁引发panic
type Mutex struct {
state int32 //互斥锁的状态 Locked Woken Starvinng Waiter
sema uint32 //信号量 协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程
}
协程之间抢锁实际上是抢给Locked赋值的权利,能给Locked域置1,就说明抢锁成功。抢不到的话就阻塞等待
Mutex.sema信号量,一旦持有锁的协程解锁,等待的协程会依次被唤醒
协程自旋 加锁失败 探测locked的变化 没有释放再转入阻塞态 自旋时间非常短 30个时钟周期
所以 重复Unlock会释放多个信号量 这样唤起多个协程去抢锁 引起不必要的协程切换 和 抢锁的复杂度
RWMutex 读写互斥锁 是Mutex的改进版本 提供四个接口RLock RUnlock Lock Unlock
type RWMutex struct {
w Mutex //用于控制多个写锁,获得写锁首先要获取该锁,如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
writerSem uint32 //写阻塞等待的信号量,最后一个读锁的goroutine释放锁时会释放信号量
readerSem uint32 //读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
readerCount //记录读者个数
readerWait //记录写阻塞时读者个数
}
Lock 写锁 获取互斥锁 + 等待所以读操作结束
Unlock 解除写锁 唤起读操作阻塞的协程 + 解除互斥锁
RLock 读锁定 读操作计数++ + 阻塞等待写操作结束
RUnlock 解除读锁定 减少读操作计数 + 唤起等待写操作的协程
goroutine MPG
内存管理 go中维护一大块全局内存,每个线程中为P维护小块私有内存存储上下文,私有内存不足从全局内存申请
全局内存 切割吃层 spans bitmap arena 三部分;arena就是堆区,应用需要的内存从这里分配;spans bitmap是为了管理arena区存在的
arena 划分成page每个page是8k
spans存放span的指针,每个指针对应一个page,指针大小8byte*page数量就是 span区的大小
span是管理arena的关键数据结构,每个span包含一个多个连续页,每页也划分为更小的粒度class
class表示固定大小的对象,最大的对象32k
bitmap区域是主要用作GC的 也是通过arena计算的
cache 数据结构包含两组span 一组包含指针 一组不包含指针 也就是noscan scan 需不需要GC
初始化时cache没有任何span,使用过程动态的从central获取缓存span(对应大小的class列表)
central cache 是线程的私有资源 为单个线程服务 central是全局资源为多个线程服务,每个线程内存不足都会向central申请,线程释放的内存也会挥手到central
mcentral 用来管理span,各线程需要内存 从mcentral管理的span申请内存;为了避免多线程申请内存加锁饿问题,为每个线程分配span的缓存cache
mcentral = lock(线程互斥锁,防止多线程读写冲突)
+ spanClass每个mcenntral管理者一组相同class的span列表
+ noempty 还有内存可以用的span列表 + empty 没有可用的span列表 + nmoalloc 累计分配的对象个数
从central获取span = 加锁 + 从noempty列表获取可用的span,从链表删除 + span加入empty + 将span返回给线程 + 解锁 + span缓存进cache
键span还给central = 加锁 + 从 empty列表删除 + 加入noempty + 解锁
heap 么个mcentral都是管理特定规格的class的span,就是每种class对应一个mcentral;所以mcentral的合集放到mheap结构中
mheap = lock互斥锁 + spans指向spans区域映射span和spans关系
+ bitmap(bitmap的起始地址) + arena_start + arena_used + central
就是说mheap管理全部内存 = arena + bitmap + spans + central(spans(固定class的列表))
GC机制 常用的gc算法 引用计数(维护一个引用计数器 对象被引用+1 引用的对象被销毁-1 为0清除 python php swift)
(回收快 不会出现内存耗尽和达到阈值才回收 ,循环引用不好处理,维护计数器代价也很高)
+ 标记清除(从根节点遍历所以引用对象 被引用的对象标记为被引用,没有标记的被回收)
(解决计数器的缺点丹娜丝要stw 暂停程序运行 golang采用三色标记法的标记清除)
+ 分代收集(按照对象的声明周期划分成不同的代空间,声明周期长的放入老年代 短的放入新生代 不同的代用不同的算法和频率回收java)
(回收性能好 算法复杂 java)
golang的gc的核心就是 标记内存你那些还在被引用,哪些内存不再被引用了,把未被引用的内存回收
标记(扫描所有变量 标记内存 考虑到内存块中存放的可能是指针,所以还需要递归的进行标记)
三色标记 灰色(对象还在标记队列等待) 黑色(对象已经被标记 不会被gc) 白色(对象未被标记 会被GC清理)
STW gc过程中要控制内存变化,否则指针传递会引起内存引用变化,会错误的回收还在使用的内存 所以要停掉所以goroutine
减少STW时间
写屏障:开启后,指针传递时会标记指针 就是说本轮不会回收写屏障开启后的指针传递的内存
新内存:GC过程中新分配的内存也会立即被标记,也是在本轮不会回收的内存
辅助GC: GC执行过程中有goroutine需要分配内存,这个goroutine就会参与部分GC工作
触发机制:每次分配内存都会检测有没有发到阈值 (内存扩大一倍时)
默认每2分钟出发一次GC
runtime.GC() 来手动触发GC 这主要用于GC性能测试和统计。
GC性能 :和对象数量负相关,所以减少对象的分配是优化思路之一,使用对象复用和大对象组合小对象等
内存逃逸现象 隐式的内存分配,有可能称为GC的负担
逃逸分析 是说 由编译器决定内存分配的位置,不需要程序员指定;函数申请的内存:在栈中函数执行完自行回收,在堆中函数执行结束GC处理回收
逃逸策略:每次函数申请新的对象 编译器会根据对象是否被函数和外部引用来决定是否逃逸,就是如果被函数外部引用放到堆中,否则放到栈中;注意,对于函数外部没有引用的对象,也有可能放到堆中,比如内存过大超过栈的存储能力
逃逸场景:指针逃逸-》函数返回局部变量指针
栈空间不足 -》栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中
动态类型逃逸 -》很多函数的参数都写成interface类型,编译期间很难确定参数的具体类型,也会产生逃逸
闭包引用对象逃逸-》闭包 引用的外部环境变量 肯定会逃逸
总结:栈上分配内存比堆中分配内存更高效 栈上分配内存不需要GC处理 堆上分配的内存会交给GC处理 逃逸分析的目的是决定在栈猴嗨森堆分配内存地址 逃逸分析在编译阶段完成
函数传递指针 真的比传值高效吗?指针传递会产生逃逸 增加GC的负担 所以不一定高效
并发控制 三种方案:channel waitgroup context
channel 用于协程间通信 也可以用于并发控制;每个协程创建一个channel用于和主goroutine通信,对于孙子协程不方便控制
waitGroup 比channel看起来优雅一些,是开源组件常用的并发控制技术-》wait goroutine group 等待一组goroutine结束
信号量 是unix系统保护共享资源的机制,用于防止多个线程同时访问资源;signal>0资源可用,获取信号量时系统自动将信号量-1;signal==0表示资源暂不可用,线程进入睡眠,信号量为正时被唤醒
type WaitGroup{
state1 [3]unit32
}
state1 是长度为3的数组 = counter(32bit)当前还在执行的goroutine计数器 + waiter(32bit)等待 group技术活的goroutine数量 + semaphore(32bit)信号量
WaitGroup对外提供的三个接口:Add Wait Done
Add = 把delta值累加到counter,delta可用为负 -》 counter==0 时 根据waiter数值释放信号量 唤醒所以等待的goroutine,counter为负值就panic
Wait = 累加waiter + 阻塞等待信号量
Done = counter - 1 就是Add(-1)
context 也是开源组件用于并发控制的技术,和WaitGroup不同是context对于派生的goroutine有更强的控制力,可以多级的goroutine;控制一组树状结构的goroutine,每个goroutine有相同的上下文
context是定义的接口
type Context interface{
Deadline()(deadline time.Time, ok bool) //返回deadline 和 释放这是deadline的bool值
Done() <-chan struct{} //返回一个channel ,需要select语句使用
Err() error //返回context关闭的原因 比如context deadline exceeded,context canceled;当context还未关闭时,Err()返回nil;
Value(key interface{}) interface{} //据key值查询map中的value,用于goroutine进行信息传递
}
context包中定义了一个emptyCtx 作为context的根节点;var background = new(emptyCtx);context.Background()获取
4个方法创建不同类型的context:WithCancel(),WithDeadline(),WithTimeout(),WithValue()
有cancelCtx、timerCtx和valueCtx三种struct 三种context实例可互为父节点,从而可以组合成不同的应用形式
反射 官方介绍:提供让程序检查自身结构的能力;反射是困惑的源泉;
就是说 反射 就是一种检查interface变量的底层类型和值的机制
go是静态类型语言 每个变量都有一个静态类型 编译时就确定了,interface是一种特殊的静态类型,可以存放任何值(只要实现了它的方法);
空interface可以存放任何值 这也是go被认为是董涛类型的原因 这是个错觉。但是存储的值的类型是什么,interface{};底层类型如何体现
反射三定律:interface类型有(value type)对,反射就是检查interface的(value type)对,反射提供方法提取value 和 type就是 reflect.Type reflect.Value
1,反射可以将interface类型转成反射对象
2,反射可以将反射对象还原长城interface对象
3,反射对象可以修改,value值必须是可以修改的
reflect.Value 提供了 Elem() 方法,可以获得指针向指向的 value
单元测试 性能测试 示例子测试

定时器 ,go中提供了两种定时器 一次性定时器 周期性定时器 timer ticker
timer 单一事件定时器,指定时间后触发事件,通过本身的channel通知 time.NewTimer(d Duration)创建;对外暴露channel,指定事件到来就写入channel系统时间,就触发事件
type timer struct {
C <-chan Time //上层应用据此接收事件 面向Timer用户的
r runtimeTimer //runtime定时器 对上层不可见 面向底层的定时器实现
}
实际上创建timer实际上就是将一个计时任务交给系统守护协程,系统的守护协程管理着所有timer,timer时间到达后向Timer管道发送当前时间作为事件
func (t *Timer) Stop() bool; func (t *Timer) Reset(d Duration) bool;
<-time.After(1 * time.Second) 匿名定时器 没有方法停止和重置
time.AfterFunc(1 * time.Second, func() {} 异步执行的
每个golang的协程都会有专门负责管理timer的协程,这个协程负责监控timer是否过期,过期后执行预定义的动作,就是发送当前时间到channel
runtimeTimer 交给协程监控的任务载体 每创建一个Timer就创建一个runtimeTimer变量,然后把它交给系统进行监控;我们通过设置runtimeTimer过期后的行为来达到定时的目的
type runtimeTimer struct {
tb uintptr // 存储当前定时器的数组地址, 系统底层存储runtimeTimer的数组地址
i int // 存储当前定时器的数组下标,就是当前runtimeTimer在tb数组中的下标;
when // 当前定时器触发时间
period // 当前定时器周期触发间隔 (对于Timer来说,此值恒为0);
f // 定时器触发时执行的函数 回调函数接收两个参数;
arg // 定时器触发时执行函数传递的参数一
seq // 定时器触发时执行函数传递的参数二(该参数只在网络收发场景下使用)
}
停止Timer就是简单的把Timer从系统协程中移除,系统协程监控Timer触发,触发后也要移除Timer;Stop()函数执行 对应触发没触发返回false true
重置Timer会先将Timer从系统协程中删除,修改新的时间后重新添加到系统协程中。
Ticker 周期性定时器 周期性触发事件 也是通过Ticker的管道将事件传递出去,数据结构和Timer一样
type timer struct {
C <-chan Time //上层应用据此接收事件 面向用户的
r runtimeTimer //runtime定时器 对上层不可见 面向底层的定时器实现
}
NewTicker(d Duration) *Ticker创建定时器ticker;
Stop()停止定时器,但是channel不会释放,Ticker使用完必须释放否则会产生内存泄漏;
调用stopTicker()即通知系统协程把该Ticker移除,即不再监控。系统协程只是移除Ticker并不会关闭管道,以避免 用户协程读取错误
Ticker的channel是含有一个缓冲区的,如果下次周期触发时chan的数据没有取走,这次事件会丢失(sendTime()也不会阻塞,而是直接退出)
Go在实现时预留了64个timersBucket,只有当GOMAXPROCS大于64时才会出现多个Process分布于同一个timersBucket中。
创建Timer和Ticker = 创建管道 + 创建timer并启动;每个timer归属一个timerBucket(就是协程ID和timerBucket数组求模);timer在timersBucket中的顺序是按照触发时间排序的小头堆,每次加入新的timer就调整排序;Go实现时使用的是四叉堆,使用四叉堆的好处是堆的高度降低,堆调整时更快
如果堆中已没有事件需要触发,则系统协程将进入暂停态,也可认为是无限时睡眠,直到有新的timer加入才会被唤醒
创建Ticker的协程并不负责计时,只负责从Ticker的管道中获取事件;其次,系统协程只负责定时器计时, 向管道中发送事件,并不关心上层协程如何处理事件;
如果创建了Ticker,则系统协程将持续监控该Ticker的timer,定期触发事件。如果Ticker不再使用且没有 Stop(),那么系统协程负担会越来越重,最终将消耗大量的CPU资源 -》defer ticker.Stop()
语法糖 go中的语法糖不多 := 和 ...
:= 声明变量并赋值,不能出现在函数外部 因为赋值语句不能出现在函数外部的
... 可变参数 0或多个;参数类型前加... func Println(a ...interface{});可变参数必须在函数参数列表的尾部;函数内部当作切片处理;可变参数可以传递切片
posted @ 2020-09-22 16:14  helloworldlee  阅读(337)  评论(0编辑  收藏  举报