协程是用户态轻量级线程,它是线程调度的基本单位。
使用者分配足够多的任务,系统能自动帮助使用者把任务分配到 CPU 上,让这些任务尽量并发运作。这种机制在 Go语言中被称为 goroutine(协程)。
Don’t communicate by sharing memory, share memory by communicating.
channel是go语言协程中数据通信的双向通道。但是在实际应用中,为了代码的简单和易懂,一般使用的channel是单向的。
有缓冲
package mainimport "fmt"/*有缓冲*/
func main() {ch := make(chan int, 3)//缓冲区大小为3,消息个数小于等于3都不会阻塞goroutinech <- 1fmt.Println(<-ch)//输出1ch <- 2fmt.Println(<-ch)//输出2ch <- 3ch <- 4fmt.Println(len(ch))//输出2,表示是channel有两个消息fmt.Println(cap(ch))//输出3,表示缓存大小总量为3
}
无缓冲
package mainimport "fmt"/*无缓冲*/
func main() {ch := make(chan int, 3)ch <- 2ch <- 1ch <- 3elem := <-chfmt.Println("The first element received from channel ch:%v\n", elem)
}
defer执行顺序和调用顺序相反,类似于栈先进后出。
defer一般用于资源的释放和异常的捕捉, 作为Go语言的特性之一.
defer 语句会将其后面跟随的语句进行延迟处理. 意思就是说 跟在defer后面的语言 将会在程序进行最后的return之后再执行.
在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关。
int8占1个字节,int16占2个字节,int32占4个字节,int64占8个字节。
切片 | 数组 |
---|---|
长度可以不断增加但不会减少 | 初始化确定数组的长度 |
长度可变 | 长度固定、值传递 |
底层实现数组每个数据的引用、指针,自身是结构体 | 函数参数传递需要制定数组长度 |
make | new |
---|---|
make只用于给slice、map、channel类(引用类型)分配内存,返回对应的类型,并非指针 | new用于各种类型的内存分配,返回指针,指向对应类型的零值 类型的指针需要分配内存才能赋值,与c中一样 |
go运行的基础设施
与python、java的runtime不同的是,在运行时与用户代码没有明显的界限,一起打包
最主要的功能是两个方面:调度、GC
众所周知,操作系统内部有着线程、进程调度器,当触发阻塞、时间片用尽、硬件中断的时候,都会涉及到切换的问题
Go在此基础上实现了自己的调度器(调度Goroutine,Go内部最基本的执行单元)
线程:共享堆,不共享栈,其切换由操作系统控制
协程:共享堆,不共享栈,切换由程序员显式控制,可以避免上下文切换的额外消耗,可以运行在一个或多个线程上
协程的切换调度在用户空间完成,不涉及到用户空间到内核空间的切换(寄存器切换、内存数据切换、栈切换、安全检查),线程调度里面的taskstructure除了CPU信息之外,还会保存线程的私有栈以及寄存器,上下文会多一点,在POSIX中线程获得了许多进程拥有的功能,这些功能在go的调度中都是用不到的,同时也增加了开销
由于线程拥有协程,而一个线程只在一个CPU上执行,导致协程没有办法利用多核
Goroutine与协程类似,且可以实现并行
Go实现了调度器之后
=是赋值变量,:=是定义变量。
个指针可以指向任意变量的地址,它所指向的地址在32位或64位机器上分别固定占4或8个字节。指针的作用有:
import fmtfunc main(){a := 1p := &a//取址&fmt.Printf("%d\n", *p);//取值*}
// 交换函数func swap(a, b *int) {*a, *b = *b, *a}
type A struct{}func (a *A) fun(){}
所谓持久化就是将要保存的字符串写到硬盘等设备。
最简单的方式就是采用ioutil的WriteFile()方法将字符串写到磁盘上,这种方法面临格式化方面的问题。
更好的做法是将数据按照固定协议进行组织再进行读写,比如JSON,XML,Gob,csv等。
如果要考虑高并发和高可用,必须把数据放入到数据库中,比如MySQL,PostgreDB,MongoDB等。
磁盘——》固定协议——》数据库(高并发高可用)
分为两个阶段:标记和清除
标记阶段:从根对象出发寻找并标记所有存活的对象。
清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。
缺点是需要暂停程序STW。
将对象标记为白色,灰色或黑色。
白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。
标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。
这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了写屏障技术,当对象新增或者更新会将其着色为灰色。
Go里面GMP分别代表:G:goroutine,M:线程(真正在CPU上跑的),P:调度器。
GMP模型
调度器是M和G之间桥梁。
go进行调度过程:
某个线程尝试创建一个新的G,那么这个G就会被安排到这个线程的G本地队列LRQ中,如果LRQ满了,就会分配到全局队列GRQ中;
尝试获取当前线程的M,如果无法获取,就会从空闲的M列表中找一个,如果空闲列表也没有,那么就创建一个M,然后绑定G与P运行。
进入调度循环:
Go实现面向对象的两个关键是struct和interface。
封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。
继承:继承是编译时特征,在struct内加入所需要继承的类即可:
type A struct{}
type B struct{
A
}
多态:多态是运行时特征,Go多态通过interface来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。
Go支持多重继承,就是在类型中嵌入所有必要的父类型。
源码位于src\runtime\map.go
中。
go的map和C++map不一样,底层实现是哈希表,包括两个部分:hmap和bucket。
hmap结构体如图:
type hmap struct {count int //map元素的个数,调用len()直接返回此值// map标记:// 1. key和value是否包指针// 2. 是否正在扩容// 3. 是否是同样大小的扩容// 4. 是否正在 `range`方式访问当前的buckets// 5. 是否有 `range`方式访问旧的bucketflags uint8 B uint8 // buckets 的对数 log_2, buckets 数组的长度就是 2^Bnoverflow uint16 // overflow 的 bucket 近似数hash0 uint32 // hash种子 计算 key 的哈希的时候会传入哈希函数buckets unsafe.Pointer // 指向 buckets 数组,大小为 2^B 如果元素个数为0,就为 nil// 扩容的时候,buckets 长度会是 oldbuckets 的两倍oldbuckets unsafe.Pointer // bucket slice指针,仅当在扩容的时候不为nilnevacuate uintptr // 扩容时已经移到新的map中的bucket数量extra *mapextra // optional fields
}
里面最重要的是buckets(桶)。buckets是一个指针,最终它指向的是一个结构体:
// A bucket for a Go map.
type bmap struct {tophash [bucketCnt]uint8
}
每个bucket固定包含8个key和value(可以查看源码bucketCnt=8).实现上面是一个固定的大小连续内存块,分成四部分:每个条目的状态,8个key值,8个value值,指向下个bucket的指针。
创建哈希表使用的是makemap
函数.map 的一个关键点在于,哈希函数的选择。在程序启动时,会检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。这是在函数 alginit() 中完成,位于路径:src/runtime/alg.go
下。
map查找就是将key哈希后得到64位(64位机)用最后B个比特位计算在哪个桶。在 bucket 中,从前往后找到第一个空位。这样,在查找某个 key 时,先找到对应的桶,再去遍历 bucket 中的 key。
go的接口由两种类型实现iface
和eface
。iface是包含方法的接口,而eface不包含方法。
iface
对应的数据结构是(位于src\runtime\runtime2.go
):
type iface struct {tab *itabdata unsafe.Pointer
}
可以简单理解为,tab表示接口的具体结构类型,而data是接口的值。
itab:
type itab struct {inter *interfacetype //此属性用于定位到具体interface_type *_type //此属性用于定位到具体interfacehash uint32 // copy of _type.hash. Used for type switches._ [4]bytefun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
属性interfacetype
类似于_type
,其作用就是interface的公共描述,类似的还有maptype
、arraytype
、chantype
…其都是各个结构的公共描述,可以理解为一种外在的表现信息。interfaetype和type唯一确定了接口类型,而hash用于查询和类型判断。fun表示方法集。
eface
与iface基本一致,但是用_type
直接表示类型,这样的话就无法使用方法。
type eface struct {_type *_typedata unsafe.Pointer
}
go cover 测试代码覆盖率
go doc 用于生成go文档
pprof 用于性能调优,针对cpu、内存和并发
race 用于竞争检测