Go核心特性
1.goroutine
协程是用户态轻量级线程,它是线程调度的基本单位。
使用者分配足够多的任务,系统能自动帮助使用者把任务分配到 CPU 上,让这些任务尽量并发运作。这种机制在 Go语言中被称为 goroutine(协程)。
2.channel
Don’t communicate by sharing memory, share memory by communicating.
channel是go语言协程中数据通信的双向通道。但是在实际应用中,为了代码的简单和易懂,一般使用的channel是单向的。
缓冲机制
有缓冲
package main
import "fmt"
/*有缓冲*/
func main() {
ch := make(chan int, 3)//缓冲区大小为3,消息个数小于等于3都不会阻塞goroutine
ch <- 1
fmt.Println(<-ch)//输出1
ch <- 2
fmt.Println(<-ch)//输出2
ch <- 3
ch <- 4
fmt.Println(len(ch))//输出2,表示是channel有两个消息
fmt.Println(cap(ch))//输出3,表示缓存大小总量为3
}
无缓冲
package main
import "fmt"
/*无缓冲*/
func main() {
ch := make(chan int, 3)
ch <- 2
ch <- 1
ch <- 3
elem := <-ch
fmt.Println("The first element received from channel ch:%v\n", elem)
}
3.defer
defer执行顺序和调用顺序相反,类似于栈先进后出。
defer一般用于资源的释放和异常的捕捉, 作为Go语言的特性之一.
defer 语句会将其后面跟随的语句进行延迟处理. 意思就是说 跟在defer后面的语言 将会在程序进行最后的return之后再执行.
在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
4.切片
5.垃圾回收机制
6.int与int32
go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关。
int8占1个字节,int16占2个字节,int32占4个字节,int64占8个字节。
7.interface代替继承多态
8.字母大小设置可见性
9.没有模板泛型
10.所有类型初始化默认为0
11.反射
底层实现
切片(slice)和数组的区别
切片 | 数组 |
---|---|
长度可以不断增加但不会减少 | 初始化确定数组的长度 |
长度可变 | 长度固定、值传递 |
底层实现数组每个数据的引用、指针,自身是结构体 | 函数参数传递需要制定数组长度 |
make与new的区别
make | new |
---|---|
make只用于给slice、map、channel类(引用类型)分配内存,返回对应的类型,并非指针 | new用于各种类型的内存分配,返回指针,指向对应类型的零值 类型的指针需要分配内存才能赋值,与c中一样 |
并发编程
runtime
go运行的基础设施
- 协调多线程调度、内存分配、GC
- 操作系统、CPU级别操作的封装(信号处理,系统调用、寄存器操作、原子操作)、CGO
- 性能分析等,pprof、trace、race
- 反射实现
与python、java的runtime不同的是,在运行时与用户代码没有明显的界限,一起打包
最主要的功能是两个方面:调度、GC
调度
众所周知,操作系统内部有着线程、进程调度器,当触发阻塞、时间片用尽、硬件中断的时候,都会涉及到切换的问题
Go在此基础上实现了自己的调度器(调度Goroutine,Go内部最基本的执行单元)
线程与协程
线程:共享堆,不共享栈,其切换由操作系统控制
协程:共享堆,不共享栈,切换由程序员显式控制,可以避免上下文切换的额外消耗,可以运行在一个或多个线程上
协程的切换调度在用户空间完成,不涉及到用户空间到内核空间的切换(寄存器切换、内存数据切换、栈切换、安全检查),线程调度里面的taskstructure除了CPU信息之外,还会保存线程的私有栈以及寄存器,上下文会多一点,在POSIX中线程获得了许多进程拥有的功能,这些功能在go的调度中都是用不到的,同时也增加了开销
由于线程拥有协程,而一个线程只在一个CPU上执行,导致协程没有办法利用多核
Goroutine与协程类似,且可以实现并行
Go实现了调度器之后
- 可以自己管理上下文切换等,减少切换成本(协程切换)
- GC的时候需要暂停所有运行的goroutine,使得内存状态一致的时候进行垃圾回收
面试题:
=和:=的区别
=是赋值变量,:=是定义变量。
指针的作用
个指针可以指向任意变量的地址,它所指向的地址在32位或64位机器上分别固定占4或8个字节。指针的作用有:
- 获取变量的值
import fmt
func main(){
a := 1
p := &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等。
磁盘——》固定协议——》数据库(高并发高可用)
简述Go语言GC(垃圾回收)的工作原理
标记清楚法
分为两个阶段:标记和清除
标记阶段:从根对象出发寻找并标记所有存活的对象。
清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。
缺点是需要暂停程序STW。
三色标记法
将对象标记为白色,灰色或黑色。
白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。
标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。
这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了写屏障技术,当对象新增或者更新会将其着色为灰色。
三色标记法+混合写屏障
❤go如何进行调度的。GMP中状态流转。
Go里面GMP分别代表:G:goroutine,M:线程(真正在CPU上跑的),P:调度器。
GMP模型
调度器是M和G之间桥梁。
go进行调度过程:
-
某个线程尝试创建一个新的G,那么这个G就会被安排到这个线程的G本地队列LRQ中,如果LRQ满了,就会分配到全局队列GRQ中;
-
尝试获取当前线程的M,如果无法获取,就会从空闲的M列表中找一个,如果空闲列表也没有,那么就创建一个M,然后绑定G与P运行。
-
进入调度循环:
-
- 找到一个合适的G
- 执行G,完成以后退出
Go面向对象是如何实现的
Go实现面向对象的两个关键是struct和interface。
封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。
继承:继承是编译时特征,在struct内加入所需要继承的类即可:
type A struct{}
type B struct{
A
}
多态:多态是运行时特征,Go多态通过interface来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。
Go支持多重继承,就是在类型中嵌入所有必要的父类型。
Channel底层实现
map的底层实现
源码位于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`方式访问旧的bucket
flags uint8
B uint8 // buckets 的对数 log_2, buckets 数组的长度就是 2^B
noverflow uint16 // overflow 的 bucket 近似数
hash0 uint32 // hash种子 计算 key 的哈希的时候会传入哈希函数
buckets unsafe.Pointer // 指向 buckets 数组,大小为 2^B 如果元素个数为0,就为 nil
// 扩容的时候,buckets 长度会是 oldbuckets 的两倍
oldbuckets unsafe.Pointer // bucket slice指针,仅当在扩容的时候不为nil
nevacuate 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 interface的实现
go的接口由两种类型实现iface
和eface
。iface是包含方法的接口,而eface不包含方法。
iface
对应的数据结构是(位于src\runtime\runtime2.go
):
type iface struct {
tab *itab
data unsafe.Pointer
}
可以简单理解为,tab表示接口的具体结构类型,而data是接口的值。
itab:
type itab struct {
inter *interfacetype //此属性用于定位到具体interface
_type *_type //此属性用于定位到具体interface
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [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 *_type
data unsafe.Pointer
}
Go的调试和分析工具有哪些
go cover 测试代码覆盖率
go doc 用于生成go文档
pprof 用于性能调优,针对cpu、内存和并发
race 用于竞争检测