原地址:https://geektutu.com/post/qa-golang-2.html
实现原理
1、init()函数是什么时候执行的?
答:init()函数是go程序初始化的一部分。go程序初始化优先于main函数,由runtime初始化每个导入包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。
每个包首先初始化作用域的常量和变量(常量优先于变量),然后执行包的init()函数,同一个包,甚至是同一个源文件可以有多个init()函数。init()函数没有入参和返回值,不能被其他函数调用,同一个包内多个init()函数的执行顺序不做保证。
一句话总结:import- >const->var->init()->main()
2、Go 语言的局部变量分配在栈上还是堆上?
答:由编译器决定。go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析,当发现变量的作用域没有超出函数范围,就放在栈上,反之则必须分配在堆上。
func foo()*int{
v :=11
return&v
}
funcmain(){
m := foo()
println(*m)// 11
}
foo() 函数中,如果 v 分配在栈上,foo 函数返回时,&v 就不存在了,但是这段函数是能够正常运行的。Go 编译器发现 v 的引用脱离了 foo 的作用域,会将其分配在堆上。因此,main 函数中仍能够正常访问该值。
Q3 2 个 interface 可以比较吗?
答:go语言中,interface的内部实现包含了2个字段,类型T和值V,interface可以使用==或!=比较。2个interface相等有一下2中情况
1、两个interface均等于nil(此时V和T都处于unset状态)
2、类型V相同,且对应的值V相等
type Stustruct{
Name string
}
type StuInt interface{}
func main(){
varstu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"}
varstu3, stu4 StuInt = Stu{"Tom"}, Stu{"Tom"}
fmt.Println(stu1 == stu2)// false
fmt.Println(stu3 == stu4)// true
}
stu1和stu2对应的类型是*Stu,值是Stu结构体的地址,两个地址不同,因此结果是false。
stu3和stu4对应的类型是Stu,值是Stu结构体,且各字段相等,因此结果是true。
Q4 两个 nil 可能不相等吗?
答:可能。
接口是对非接口值(例如指针,struct等)的封装,内部实现包含2个字段,类型T和值V.一个接口等于nil,当且仅当T和V处于unset状态(T=nil,V is unset)
两个接口值比较时,会优先比较T,在比较V
接口值和非接口值比较时,会优先比较非接口值尝试转化为接口值,在比较。
func main(){
var p *int=nil
var i interface{} = p
fmt.Println(i == p)// true
fmt.Println(p ==nil)// true
fmt.Println(i ==nil)// false
}
上面这个例子中,将一个 nil 非接口值 p 赋值给接口 i,此时,i 的内部字段为(T=*int, V=nil),i 与 p 作比较时,将 p 转换为接口后再比较,因此 i == p,p 与 nil 比较,直接比较值,所以 p == nil。
但是当 i 与 nil 比较时,会将 nil 转换为接口 (T=nil, V=nil),与i (T=*int, V=nil) 不相等,因此 i != nil。因此 V 为 nil ,但 T 不为 nil 的接口不等于 nil。
Q5 简述 Go 语言GC(垃圾回收)的工作原理
答:最常见的垃圾回收算法有标记清除(Mark-Sweep) 和引用计数(Reference Count),go语言采用的是标记清除算法。并在此基础上使用了三色标记法和写屏障技术,提高了效率。
标记清除收集器是跟踪式垃圾收集器,其执行过程可以分为标记mark和清除sweep两个阶段:
标记阶段 ---- 从根对象出发查找并标记堆中所有存活的对象。
清除阶段 ---- 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表
标记清除算法的一大问题是在标记期间,需要暂停程序(Stop the world,STW),标记结束后,用户程序才能继续进行,为了能够异步执行,减少STW的时间,go语言采用了三色标记法。
三色标记算法将程序中的对象分成白色,黑色和灰色三类。
白色:不确定对象。灰色:存货对象,子对象待处理。黑色:存活对象。
标记开始时,所有对象加入白色集合(这一步需要STW)。首先将根对象标记为灰色,加入灰色集合,垃圾搜集器取出一个灰色对象,将其标记为黑色,并将其指向的对象标记为灰色,加入灰色集合。重复这个过程,指导灰色集合为空为止,标记阶段结束。name白色对象即可需要清理的对象,而黑色对象均为根可达的对象,不能被清理。
三色标记法因为多了个白色的状态来存放不确定对象,所以后续的标记阶段可以并发的执行。当然并发执行的代价是可能造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已是不可达的了。所以三see标记法是一个false negative(假阴性)的算法。
三色标记法并发执行仍存在一个问题,即GC处理过程中,对象指针发生了改变。
1A (黑) -> B (灰) -> C (白) -> D (白)
正常情况下,D对象最终会被标记为黑色,不应被回收。但在标记和用户程序并发执行过程中,用户程序删除了C对D的引用,而A获得了D的引用。标记继续执行,D就没有机会被标记为黑色了(A已经处理过,这一轮不会再被处理)。
A (黑) -> B (灰) -> C (白)
↓
D (白)
为了解决这个问题,go使用了内存屏障技术,它是在用户程序读取对象、创建对象以及更新对象指针时执行的一段代码,类似于一个钩子。垃圾收集器使用了写屏障(Write Barrier)技术,当对象新增活更新时,会将其着色为灰色。这样即使与用户程序并发执行,对象的引用发生改变时,垃圾收集器也能正确处理了。
一次完整的GC分为四个阶段:
1、标记准备(Mark Setup,需 STW),打开写屏障(Write Barrier)
2、使用三色标记法标记(Marking, 并发)
3、标记结束(Mark Termination,需 STW),关闭写屏障
4、清理(Sweeping, 并发)
Q6 函数返回局部变量的指针是否安全?
答:这在go中是安全的,go编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上。
Q7 非接口非接口的任意类型 T() 都能够调用 *T 的方法吗?反过来呢?
答:一个T类型的值可以调用为*T类型声明的方法,当且仅当此T的值是可寻址的情况下。编译器在调用指针属主方法前,会自动取此T值的地址。因为不是任何T值都是可寻址的,所以并非任何T值都能够调用为类型*T声明的方法。
反过来,一个*T类型的值可以调用为类型T声明的方法,这是因为解引用指针总是合法的。事实上,你可以认为对于每一个为类型T声明的方法,编译器都会为类型*T自动隐式声明一个同名和同签名的方法。
哪些值是不可寻址的呢?
字符串中的字节、map对象中的元素(slice对象中的元素是可寻址的,slice的底层是数组)、常量、包级别的函数等。
举一个例子,定义类型T,并为类型*T声明一个方法hello(),变量t1可以调用该方法,但是常量t2调用该方法时,会产生编译错误。
typeTstring
func (t *T) hello() {
fmt.Println("hello")
}
func main() {
vart1 T ="ABC"
t1.hello()// hello
constt2 T ="ABC"
t2.hello()// error: cannot call pointer method on t
}







网友评论