坚持, 量变才会引起质变
new
和make
的区别?
new
只用于分配内存,返回一个指向地址的指针。它为每个新类型分配一片内存,初始化为0且返回类型*T
的内存地址,它相当于&T{}
make
只可用于slice
,map
,channel
的初始化,返回的是引用。
Go面向对象是如何实现的?
Go实现面向对象的两个关键是struct
和interface
。
- 封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。
- 继承: 继承是编译时特征,在struct内加入所需要继承的类即可; (组合)
- 多态: 多态是运行时特征,Go多态通过
interface
来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。
Go支持多重继承,就是在类型中嵌入所有必要的父类型。
uint型变量值分别为 1,2,它们相减的结果是多少?
1 | var a uint = 1 |
结果会溢出,如果是32位系统,结果是
2^32-1
,如果是64位系统,结果2^64-1
有没有函数在main之前执行?怎么用?
init函数
1 | func init() { |
- init函数非常特殊:
- 初始化不能采用初始化表达式初始化的变量
- 程序运行前执行注册
- 实现
sync.Once
功能 - 不能被其它函数调用
init
函数没有入口参数和返回值- 每个包可以有多个
init
函数,每个源文件也可以有多个init
函数 - 同一个包的
init
执行顺序,golang
没有明确定义,编程时要注意程序不要依赖这个执行顺序 - 不同包的
init
函数按照包导入的依赖关系决定执行顺序
下面这句代码是什么作用,为什么要定义一个空值?
1 | type GobCodec struct { |
将
nil
转换为*GobCodec
类型,然后再转换为Codec
接口,如果转换失败,说明*GobCodec
没有实现Codec
接口的所有方法。
mutex有几种模式?
mutex
有两种模式:normal
和starvation
正常模式
所有goroutine
按照FIFO
的顺序进行锁获取,被唤醒的goroutine
和新请求锁的goroutine
同时进行锁获取,通常新请求锁的goroutine
更容易获取锁(持续占有cpu),被唤醒的goroutine
则不容易获取到锁。公平性:否。
饥饿模式
所有尝试获取锁的goroutine
进行等待排队,新请求锁的goroutine
不会进行锁获取(禁用自旋),而是加入队列尾部等待获取锁。公平性:是。
go如何进行调度的。GMP中状态流转
Go里面GMP分别代表:G:goroutine
,M:线程
(真正在CPU上跑的),P:调度器
。
调度器是M和G之间桥梁。
go进行调度过程
- 某个线程尝试创建一个新的
G
,那么这个G就会被安排到这个线程的G
本地队列LRQ
中,如果LRQ
满了,就会分配到全局队列GRQ
中; - 尝试获取当前线程的
M
,如果无法获取,就会从空闲的M
列表中找一个,如果空闲列表也没有,那么就创建一个M
,然后绑定G
与P
运行。 - 进入调度循环:
- 找到一个合适的
G
- 执行
G
,完成以后退出
- 找到一个合适的
Go什么时候发生阻塞?阻塞时,调度器会怎么做。
- 用于原子、互斥量或通道操作导致
goroutine
阻塞,调度器将把当前阻塞的goroutine
从本地运行队列LRQ
换出,并重新调度其它goroutine
; - 由于网络请求和IO导致的阻塞,Go提供了网络轮询器(
Netpoller
)来处理,后台用epoll
等技术实现IO多路复用
其他回答
- channel阻塞: 当goroutine读写channel发生阻塞时,会调用
gopark
函数,该G脱离当前的M和P,调度器将新的G放入当前M。 - 系统调用: 当某个G由于系统调用陷入内核态,该P就会脱离当前M,此时P会更新自己的状态为
Psyscall
,M与G相互绑定,进行系统调用。结束以后,若该P状态还是Psyscall
,则直接关联该M和G,否则使用闲置的处理器处理该G。 - 系统监控: 当某个G在P上运行的时间超过10ms时候,或者P处于
Psyscall
状态过长等情况就会调用retake
函数,触发新的调度。 - 主动让出: 由于是协作式调度,该G会主动让出当前的P(通过
GoSched
),更新状态为Grunnable
,该P会调度队列中的G运行。
如果有一个G一直占用资源怎么办?
如果有个goroutine
一直占用资源,那么GMP模型会从正常模式转变为饥饿模式(类似于mutex),允许其它goroutine
使用work stealing
抢占(禁用自旋锁)。
什么是work stealing
算法?
指,一个线程如果处于空闲状态,则帮其它正在忙的线程分担压力,从全局队列取一个G任务来执行,可以极大提高执行效率。
Go竞态条件了解吗?
所谓竞态竞争,就是当两个或以上的goroutine访问相同资源时候,对资源进行读/写。
比如var a int = 0
,有两个协程分别对a+=1
,我们发现最后a不一定为2.这就是竞态竞争。
通常我们可以用go run -race xx.go
来进行检测。
解决方法是,对临界区资源上锁,或者使用原子操作(atomics),原子操作的开销小于上锁。
如果若干个goroutine,有一个panic会怎么做?
有一个panic
,那么剩余goroutine
也会退出,程序退出。如果不想程序退出,那么必须通过调用 recover()
方法来捕获 panic
并恢复将要崩掉的程序。
defer
可以捕获goroutine
的子goroutine
吗?
不可以
它们处于不同的调度器P中。对于子goroutine
,必须通过recover()
机制来进行恢复,然后结合日志进行打印(或者通过channel传递error),下面是一个例子:
1 | // Ping 心跳函数 |
Go解析Tag是怎么实现的?
Go解析tag采用的是反射。
具体来说使用reflect.ValueOf
方法获取其反射值,然后获取其Type
属性,之后再通过Field(i)
获取第i+1
个field,再.Tag
获得Tag。
反射实现的原理在:src/reflect/type.go
中
channel
死锁的场景
当一个channel
中没有数据,而直接读取时,会发生死锁:
1 | q := make(chan int,2) |
解决方案是采用select
语句,再default
放默认处理方式:
1 | q := make(chan int,2) |
说说 atomic底层怎么实现的
atomic
源码位于sync\atomic
。通过阅读源码可知,atomic采用CAS(CompareAndSwap)
的方式实现的。
所谓CAS
就是使用了CPU
中的原子性操作。在操作共享变量的时候,CAS
不需要对其进行加锁,而是通过类似于乐观锁的方式进行检测,总是假设被操作的值未曾改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。本质上是不断占用CPU
资源来避免加锁的开销。
go的调试/分析工具用过哪些
- go的自带工具链相当丰富
go cover
: 测试代码覆盖率pprof
: 用于性能调优,针对cpu,内存和并发;godoc
: 用于生成go文档race
:用于竞争检测
实现使用字符串函数名,调用函数。
采用反射的Call方法实现。
1 | type Animal struct { |