2 个 interface 可以比较吗 ?
Go 语言中,interface 的内部实现包含了 2 个字段,类型 T 和 值 V,interface 可以使用 == 或 != 比较。2 个 interface 相等有以下 2 种情况
- 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)
- 类型 T 相同,且对应的值 V 相等。
example:
type Stu struct {
Name string
}
type StuInt interface{}
func main() {
var stu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"}
var stu3, 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
。
ps: 两个nil只有在类型相同时才相等
函数返回局部变量的指针是否安全?
这一点和C++不同,在Go里面返回局部变量的指针是安全的。因为Go会进行逃逸分析,如果发现局部变量的作用域超过该函数则会把指针分配到堆区,避免内存泄漏。
go slice是怎么扩容的?(切片扩容)
- 如果当前容量小于1024,则判断所需容量是否大于原来容量2倍,如果大于,当前容量加上所需容量;否则当前容量乘2。
- 如果当前容量大于1024,则每次按照1.25倍速度递增容量,也就是每次加上cap/4。
并发问题
无缓冲的 channel 和有缓冲的 channel 的区别?
- 对于无缓冲区channel:
发送的数据如果没有被接收方接收,那么发送方阻塞;如果一直接收不到发送方的数据,接收方阻塞; - 有缓冲的channel:
发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。
为什么有协程泄露(Goroutine Leak)?
协程泄漏是指协程创建之后没有得到释放。主要原因有:
- 缺少接收器,导致发送阻塞
- 缺少发送器,导致接收阻塞
- 死锁。多个协程由于竞争资源导致死锁。
- 创建协程的没有回收
死锁和活锁
-
死锁:最直观的理解是,p0 等待 p1 占用的资源,而 p1 而在等待 p0 占用的资源,于是两个进程就相互等待。
-
活锁:例如线程 1 可以使用资源,但它很礼貌,让其他线程先使用资源,线程 2 也可以使用资源,但它同样很绅士,也让其他线程先使用资源。就这样你让我,我让你,最后两个线程都无法使用资源。这就像两个过于礼貌的人在路上相遇,他们彼此让路,然后在另一条路上相遇,然后他们就一直这样避让下去。
一个已经关闭的channel,只能读数据,不能写数据。
对已经关闭的chan进行读写会怎么样?
读已经关闭的chan能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
- 如果chan关闭前,buffer内有元素还未读,会正确读到chan内的值,且返回的第二个bool值(是否读成功)为true。
- 如果chan关闭前,buffer内有元素已经被读完,chan内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个bool值一直为false。
- 写已经关闭的chan会panic。
new和make的区别?
- new 只用于分配内存,返回一个指向地址的指针。它为每个新类型分配一片内存,初始化为0且返回类型*T的内存地址,它相当于&T{}。
- make 只可用于slice,map,channel的初始化,返回的是引用。
go GC(垃圾回收机制)
目前的go GC采用三色标记法和混合写屏障技术。
Go GC有四个阶段:
- STW,开启混合写屏障,扫描栈对象;
- 将所有对象加入白色集合,从根对象开始,将其放入灰色集合。每次从灰色集合取出一个对象标记为黑色,然后遍历其子对象,标记为灰色,放入灰色集合;
- 如此循环直到灰色集合为空。剩余的白色对象就是需要清理的对象。
- STW,关闭混合写屏障;
- 在后台进行GC(并发)。
Golang的defer与return的执行顺序
defer 的作用就是把defer关键字之后的函数执行压入一个栈中延迟执行,多个defer的执行顺序是后进先出LIFO,也就是先执行最后一个defer,最后执行第一个defer。
在此之前,先理解一下return返回值的运行机制:return并非原子操作,共分为赋值、返回值两步操作。
defer、return、返回值三者的执行是:return最先执行,先将结果写入返回值中(即赋值);接着defer开始执行一些收尾工作;最后函数携带当前返回值退出(即返回值)。
所以结论是:第一步先return
赋值,第二步再执行defer
,第三步执行return
返回。
但是在有名与无名的函数返回值的情况下会有些区别:
- 如果函数的返回值是无名的(不带命名返回值),则go语言会在执行return的时候会执行一个类似创建一个临时变量作为保存return值的动作
- 有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个返回值, 虽然defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值
当select监控多个chan同时到达就绪态时,如何先执行某个任务?
可以在子case再加一个for select语句。
func priority_select(ch1, ch2 <-chan string) {
for {
select {
case val := <-ch1:
fmt.Println(val)
case val2 := <-ch2:
priority:
for {
select {
case val1 := <-ch1:
fmt.Println(val1)
default:
break priority
}
}
fmt.Println(val2)
}
}
}
(Goroutine)有三个函数,分别打印"cat", “fish”,"dog"要求每一个函数都用一个goroutine,按照顺序打印100次。
此题目考察channel,用三个无缓冲channel,如果一个channel收到信号则通知下一个。
package main
import (
"fmt"
"time"
)
var dog = make(chan struct{})
var cat = make(chan struct{})
var fish = make(chan struct{})
func Dog() {
<-fish
fmt.Println("dog")
dog <- struct{}{}
}
func Cat() {
<-dog
fmt.Println("cat")
cat <- struct{}{}
}
func Fish() {
<-cat
fmt.Println("fish")
fish <- struct{}{}
}
func main() {
for i := 0; i < 100; i++ {
go Dog()
go Cat()
go Fish()
}
fish <- struct{}{}
time.Sleep(10 * time.Second)
}
两个协程交替打印10个字母和数字
思路:采用channel来协调goroutine之间顺序。
主线程一般要waitGroup等待协程退出,这里简化了一下直接sleep。
package main
import (
"fmt"
"time"
)
var word = make(chan struct{}, 1)
var num = make(chan struct{}, 1)
func printNums() {
for i := 0; i < 10; i++ {
<-word
fmt.Println(1)
num <- struct{}{}
}
}
func printWords() {
for i := 0; i < 10; i++ {
<-num
fmt.Println("a")
word <- struct{}{}
}
}
func main() {
num <- struct{}{}
go printNums()
go printWords()
time.Sleep(time.Second * 1)
}