golang学习小结

值类型和引用类型

 

指针与值

&获得当前变量的地址

内存中每一个存储单元都有一个地址

a的存储单元的值是1   存储单元对应的地址为&a

如果存储单元的值是一个指针 那么*就可以获得这个指针对应地址的存储单元的值

而且*只能作用在指针类型

go中都是值传递 没有引用传递 但是map slice chan类型的传递本质上也是值传递 但是传递的值是一个指针 

 

new和make的区别

new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用 于内置引用类型(切片、map 和通道)。它们的用法就像是函数,但是将类型作为参数: new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的 指针。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值, 因此它比 new 进行更多的工作。 new() 是一个函数,不要忘记它的括号。二者都是内存的分配 (堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内 存分配,并且内存置为零

ew是一个用来分配内存的内置函数,在golang中返回一个指向新分配的类型参数的指针,指针指向的内容为零 没有初始化

make返回的是类型本身  因为chan slice map本身就是引用类型  并且初始化

反射

reflect.TypeOf(x)  获得类型 

reflect.ValueOf(x) 获得值

x参数是指针时 就要.Elem()获得指针对应地址的值

x参数时结构体时  Filed()获得对应结构体中某个字段

go数据结构 

slice map  chan interface为引用类型 

切片slice

结构图

 array指针指向底层数组,len表示切片长度,cap表示底层数组容量

扩容

使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上重新一配一块更大的内 存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去

地址是新的地址

扩容操作只关心容量,会把原Slice数据拷贝到新Slice,追加数据由append在扩容结束后完成。上图可见,扩容后 新的Slice长度仍然是5,但容量由5提升到了10,原Slice的数据也都拷贝到了新Slice指向的数组中。 扩容容量的选择遵循以下规则:

如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;

如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍

每个切片都指向一个底层数组 每个切片都保存了当前切片的长度、底层数组可用容量 使用len()计算切片长度时间复杂度为O(1),不需要遍历切片 使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而矣 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片

map

结构图 

 tophash是个长度为8的数组,哈希值相同的键(准确的说是哈希值低位相同的键)存入当前bucket时会将哈 希值的高位存储在该数组中,以方便后续匹配。

data区存放的是key-value数据,存放顺序是key/key/key/…value/value/value,如此存放是为了节省 字节对齐带来的空间浪费。

overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来。

哈希冲突

 当有两个或以上数量的键被哈希到了同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决键冲突。由 于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似链表的 方式将bucket连接起来

哈希扩容

扩容就必须要讲到负载因子

负载因子 = 键数量/bucket数量

go官方的负载因子为6.5

而Go的bucket可能存8个键值对, 所以Go可以容忍更高的负载因子

扩容过程

条件

1.负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个

2.overflow数量 > 2^15时,也即overflow数量超过32768时

当负载因子过大时,就新建一个bucket,新的bucket长度是原来的2倍,然后旧bucket数据搬迁到新的bucket。 考虑到如果map存储了数以亿计的key-value,一次性搬迁将会造成比较大的延时,Go采用逐步搬迁策略,即每次访 问map时都会触发一次搬迁,每次搬迁2个键值对

渐进式扩容

map并不是线程安全的

要么加读写锁

要么使用sync.map

sync.map的结构

  1. 内部分为两个map,read和dirty,读写分离,读取时原子操作;即使在读取时有删除操作也不影响;
  2. 更改和插入数据时,在内部会加锁

具体操作

在load的时候 会先查找read中的数据 原子操作  如果没有则寻找dirty中的 misses加一

当misses达到一定的值 则dirty覆盖到read中 自身为nil  read中的amended为false

在delete时 如果read有则设为nil 如果没有并且amended为true 则去dirty中查找并删除

在store时 如果read有并且没有删除则更新  如果没有则更新到dirty中 并且amended为false的时候将 read没过期的复制到dirty中 然后过期的给个标志 

个人总结 使用sync.map的时候  第一次插入 是存到dirty中的 然后如果查询过多 misses变大就会把dirty覆盖到read中 dirty为nil   期间的新增全部都到dirty中

在第一次覆盖后 read中有了数据 不过read中的数据是不会增加的  有新增的数据还是会到dirty中 这时会判断amended 为false就会把read没过期的复制到dirty中  并且之后新增的都会到dirty而且不会再复制了  在read中的过期有标志的也会更新到dirty中

之后一直等到misses变大 循环操作dirty覆盖到read中 dirty为nil 

channel                 

操作chan正常 无缓存chan正常 有缓存chan为nil已关闭chan
读  <-chan没有发送者时阻塞阻塞默认整型类型为0 其他空
写 chan<-没有接收者时阻塞缓存空间满时阻塞阻塞panic
关闭 close正常关闭正常关闭panicpanic

底层结构

为什么用循环数组:用循环数组后我们就消费元素的时候不需要删除了,只需要记住我们的数组下标就ok

在读写时会加锁保证并发安全 

struct

结构体

内嵌与聚合: 外部类型只包含了内部类型的类型名, 而没有field 名, 则是内嵌。外部类型包含了内部类型的类 型名,还有filed名,则是聚合

defer 

defer语句用于延迟函数的调用,每次defer都会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行

个人实际运用时 就是解锁的时候用到  和recover的时候

异常处理

recover panic 

recover()必须配合defer配合使用

panic后就会执行defer的代码 

go处理并发

加锁

go和channel

atomic

使用go和channel 就是多协程处理并发 那么多协程之间的操作也需要控制

waitgroup

var wg sync.WaitGroup

wg.Add(2)

wg.Done()

wg.Wait()

context 

context包提供了4个方法创建不同类型的context,使用这四个方法时如果没有父context,都需要传入 backgroud,即backgroud作为其父节点:

WithCancel()

WithDeadline()

WithTimeout()

WithValue()

反射

个人觉得反射就是为了处理interface传进来的类型的

Go提供一组方法提取interface的 value,提供另一组方法提取interface的type

反射第一定律:反射可以将interface类型变量转换成反 射对象

反射第二定律:反射可以将反射对象还原成interface对 象

反射第三定律:反射对象可修改,value值必须是可设置 的

reflect.ValueOf()

reflect.TypeOf()

闭包

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。 换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。 闭包会随着函数的创建而被同时创建

闭包=函数+引用环境

内存逃逸

就是对象从栈空间移到了堆空间

指针逃逸

栈空间不足逃逸

动态类型逃逸

闭包引用对象逃逸

栈上分配内存比在堆中分配内存有更高的效率

栈上分配的内存不需要GC处理

堆上分配的内存使用完毕会交给GC处理

逃逸分析目的是决定内分配地址是栈还是堆

逃逸分析在编译阶段完成

测试模块gotest

xxxx_test.go创建 TestXXX()  然后 go test

测试文件名必须以”_test.go”结尾;

测试函数名必须以“TestXxx”开始;

命令行下使用”go test”即可启动测试;

性能分析工具pprof

go tool pprof -http=:xxxx 

查看性能 火焰图等

内存模型

go实现自主管理内存  为了方便自主管理内存,做法便是先向系统申请一块内存,然后将内存切割成小块,通过一定的内存分配算法管理内存

分为

heap(全部内存) 

arena(里面分成8kb的page)

span(管理arena) 每个span管理一种class  可以有多个span管理同一种class  一种class里有1个或者多个对象 一种class的大小加上碎片是8kb的整数倍  

page(8kb)

class的类型大小

 

上表中每列含义如下:

class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型

bytes/obj:该class代表对象的字节数

bytes/span:每个span占用堆的字节数,也即页数*页大小

objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)

waste bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)

上表可见最大的对象是32K大小,超过32K大小的由特殊的class表示,该class ID为0,每个class只包含一个对 象 

有了管理内存的基本单位span,还要有个数据结构来管理span,这个数据结构叫mcentral,各线程需要内存时从 mcentral管理的span中申请内存,为了避免多线程申请内存时不断的加锁,Golang为每个线程分配了span的缓 存,这个缓存即是cache

alloc为mspan的指针数组,数组大小为class总数的2倍。数组中每个元素代表了一种class类型的span列表,每 种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指 针,这么做是为了提高GC扫描性能,对于不包含指针的span列表,没必要去扫描。 根据对象是否包含指针,将对象分为noscan和scan两类,其中noscan代表没有指针,而scan则代表有指针,需要 GC进行扫描

内存分配过程

1. 获取当前线程的私有缓存mcache

2. 跟据size计算出适合的class的ID

3. 从mcache的alloc[class]链表中查询可用的span

4. 如果mcache没有可用的span则从mcentral申请一个新的span加入mcache中

5. 如果mcentral中也没有可用的span则从mheap中申请一个新的span加入mcentral

6. 从该span中获取到空闲对象地址并返回 

1. Golang程序启动时申请一大块内存,并划分成spans、bitmap、arena区域

2. arena区域按页划分成一个个小块

3. span管理一个或多个页

4. mcentral管理多个span供线程申请使用

5. mcache作为线程私有资源,资源来源于mcentral

垃圾回收算法

不需要的数据在内存中就是垃圾,需要回收,不然就会造成内存泄漏,说通俗点就是占着内存却没有任何作用

 Golang垃圾回收一般分为2个阶段,标记和清除

golang使用三色标记法

白色  灰色 黑色

白色是标记结束后被回收的对象

灰色是正在等待的对象(需要从灰色的对象中找引用的对象 并且标记为灰色  自身变成黑色)

黑色是标记结束后不会被回收的对象

1首先进行STW,开启GC,做完前置操作后退出STW。
2GC开始后新分配的对象会被直接标记黑色。
3然后并行将GC ROOTS中的对象标记为黑色,并将其引用的对象标记为灰色。注意在这个过程中,stack的标记是有先有后的,每个goroutine都有自己的stack,每当GC协程需要标记某个goroutine的stack时,就会停止goroutine的运行,然后在标记它对应的stack,最后在恢复goroutine。GC协程运行的过程中,除了当前正在被扫描的stack对应的goroutine不能运行以外,其它的goroutine都可以在其它的Processor上运行。
4每当对象A引用一个对象X的时候,golang会调用IsStackAddr函数去判断对象A的地址是否是在栈上。如果是地址位于栈中, 则不做处理。如果地址位于堆中,golang就会将对象A原来引用的对象标记为灰色,如果当前调用它的goroutine的stack还没被扫描,就会将对象A新引用的对象标记为灰色。
5等GC协程完成整个内存的标记。
6再次进入STW,在STW中完成清理阶段的前置准备。
7退出STW,开始并行清理垃圾
 

个人总结 标记和清理时都是并行的

标记时从root出发 将直接引用的对象标记为灰色

一般是全局变量   然后从灰色的对象中找到引用的对象标记为灰色  并把自身标记为黑色 递归下去

在标记期间其他gorountine也是在运行了 也有有新的对象产生  这时把栈上新的对象都标记为黑色

堆上变换的对象标记为灰色

gmp调度算法

线程数过多,意味着操作系统会不断的切换线程,频繁的上下文切换就成了性能瓶颈。Go提供一种机制,可以在线程中自己实现调度,上下文切换更轻量,从而达到了线程数少,而并发数并不少的效果。而线程中调度的就是 Goroutine

gmp模型当中p作为一个分配器  给本地队列种的g分配m来获得cpu的执行

G(Goroutine): 即Go协程,每个go关键字都会创建一个协程。

M(Machine): 工作线程,在Go中称为Machine。

P(Processor): 处理器(Go中定义的一个摡念,不是指CPU),包含运行Go代码的必要资源,也有调度 goroutine的能力 每个p有一个mcache (缓存span)全局有一个mcentol

M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行

一般m多于p 

  

 M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,跟据M0是否能获取到P,将会将G0做不同的 处理: 1. 如果有空闲的P,则获取一个P,继续执行G0。 2. 如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值