Golang面试题整理(二)
list 结构 及底层实现?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // Element is an element of a linked list.
type Element struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element
// The list to which this element belongs.
list *List
// The value stored with this element.
Value any
}
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
root Element // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
// Init initializes or clears list l.
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
|
通过代码可以发现在go中list的底层结构是一个带有头结点的循环双链表,有一个前驱节点和一个后继节点。
通过其初始化的代码可以发现在其前驱和后继都指向其自身,且长度置0。Init不仅用于初始化,还可以用户清空链表

进程 线程 协程
进程:进程是程序一次动态执行的过程,是程序运行的基本单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。进程占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、页表、文件句柄等)比较大,但相对比较稳定安全。
线程 线程又叫做轻量级进程,是CPU调度的最小单位线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。多个线程共享所属进程的资源,同时线程也拥有自己的专属资源。程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。

GMP调度模型原理
内容过多
详解点这里
协程如何等其余协程结束后再操作
sync.WaitGroup。WaitGroup就是用来等待一组操作完成的。WaitGroup内部实现了一个计数器,用来记录未完成的操作个数.
它提供了三个方法,Add()用来添加计数。Done()用来在操作结束时调用,使计数减一。Wait()用来等待所有的操作结束,即计数变为0,该函数会在计数不为0时等待,在计数为0时立即返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
time.Sleep(time.Second * time.Duration(rand.Intn(3)))
fmt.Println("当时运行的i:", i)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("运行结束,程序即将退出")
}
|
go的结构体能否比较
可以,但是有限制
同一个结构体且结构体不含有不可比较的类型可以比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| type Person struct {
Name string
Age int
}
func main() {
p1 := Person{
Name: "战鹰",
Age: 3,
}
p2 := Person{
Name: "捷豹",
Age: 4,
}
p3 := Person{
Name: "战鹰",
Age: 3,
}
fmt.Println(p1 == p2) //false
fmt.Println(p1 == p3) //true
}
|
不同的结构体就算内容和值都一样也没法比较

同一个结构体如果包含了不可比较的类型,也无法直接比较

但是可以利用反射进行比较 reflect.DeepEqual()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| func main() {
p1 := Person1{
Name: "战鹰",
Age: 3,
Hobby: []string{"鱼肉肠", "送外卖"},
}
p2 := Person1{
Name: "战鹰",
Age: 3,
Hobby: []string{"鱼肉肠", "送外卖"},
}
p3 := Person1{
Name: "战鹰",
Age: 3,
Hobby: []string{"鱼肉肠", "送外卖", "抽象"},
}
fmt.Println(reflect.DeepEqual(p1, p2)) //true
fmt.Println(reflect.DeepEqual(p1, p3)) //false
}
|
goroutine锁机制 互斥锁模式 底层实现
Go语言中使用sync包控制并发
有无缓冲channel的区别
无缓冲的与有缓冲channel有着重大差别:一个是同步的 一个是非同步的
ch1:=make(chan int) //无缓冲

ch1<-1 程序运行到这里后如果ch1里的数据没人拿走,后面的代码不会继续执行
ch2:=make(chan int,1) //有缓冲

ch2<-1 程序运行到这里后如果1进入了ch2,不管ch2里的数据有没有人取走,程序都会继续执行

如果ch2里有数据,也就是缓存满了,后面的代码不会继续执行
go defer的原理
用于延迟函数的调用,常用于关闭文件或者关闭锁的场景。
defer语句采用类似栈的方式,每遇到一个defer就会把defer后面的函数压入栈中,在函数返回前再把栈中的函数依次取出执行。
一般函数正常返回时会执行被defer延迟的函数,特别的遇到return和panic时也会触发延迟函数。
defer作用于资源释放(关闭文件句柄、断开数据库连接、停止定时器ticker以及关闭管道)、流程控制(控制函数执行顺序,如wait.Group)和异常处理(recover()),但是defer关键字只能作用于函数或者函数调用。
- 延迟函数的参数在defer语句出现时就已经确定了
- 延迟函数按后进先出LIFO的顺序执行,即先出现的defer最后执行
- 延迟函数可能操作主函数的具体变量名称返回值
1
2
3
4
5
6
7
| //数据结构
type _defer struct{
sp uintptr // 函数栈指针
pc uintptr // 程序计数器
fn *funcval // 函数地址
link *_defer // 用于链接多个defer
}
|
每个_defer实例是对一个函数的封装,编译器会把每一个延迟函数编译成一个_defer实例暂存到goroutine数据结构中,待函数结束时再逐个取出执行。
多个_defer实例使用指针link链接成一个单链表,保存到goroutine中,下面是goroutine结构中关于_defer的部分
1
2
3
| type g struct {
_defer *_defer // defer链表
}
|
每次插入_defer实例都是从链表的头部插入,函数执行结束再依次从头部取出defer执行。
defer的创建和执行
创建defer: deferproc() 将defer函数处理成_defer实例,并加入goroutine的链表中;
执行defer: deferreturn() 将defer从goroutine中取出并执行。
整个流程就是:编译器在编译阶段把defer语句替换成函数deferproc(),在return前插入函数deferreturn(), 每次执行deferproc()都会创建一个运行时_defer实例并存储,函数返回前执行deferreturn()依次拿出_defer实例并执行。
go select用处
Golang Select
go slice如何扩容
切片(slice)是 Golang 中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片(slice)是可以看做是一个长度可变的数组。
切片(slice)自身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。
1
2
3
4
5
6
| //数据结构
type slice struct {
array unsafe.Pointer
len int
cap int
}
|
扩容规则
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。
如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
go 逃逸分析是什么
Golang逃逸分析
退出程序时如何防止channel没有消费完
退出时将生产者关闭,不会产生多余的数据给消费者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var wg sync.WaitGroup
// 生产者
func Send(ch chan int) {
x := 0
defer func() {
if err := recover(); err != nil && err.(runtime.Error).Error() == "send on closed channel" {
fmt.Println(err)
fmt.Println("即将产生的数据:", x)
} else {
close(ch) //关闭的目的:不在发送数据
}
wg.Done()
}()
for i := 0; i < 10; i++ {
x++
ch <- x
}
}
// 消费者
func Receive(ch chan int) {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
close(ch) //关闭的目的:不要让生产者继续发送数据
fmt.Println(<-ch) //继续消费,输出结果为0,说明已经不会生产者已经不会再发送数据了
}
wg.Done()
}()
for x := range ch {
time.Sleep(time.Second)
fmt.Println(x)
if x == 3 {
panic("发生意外的错误") //中断主程序,但是协程还是不会关闭的
}
}
fmt.Println("Receive任务结束")
}
func main() {
fmt.Println("退出程序时,防止channel没有消费完")
ch := make(chan int)
wg.Add(2)
go Send(ch)
go Receive(ch)
wg.Wait()
fmt.Println("任务完成")
_, ok := <-ch
fmt.Println(ok)
}
|
循环队列 是否线程安全 如何做到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| package main
import "fmt"
type Queue struct {
arr []int
front int
rear int
}
func NewQueue(maxSize int) *Queue {
return &Queue{
arr: make([]int, maxSize),
front: 0,
rear: 0,
}
}
func (q *Queue) Push(data int) {
q.arr[q.rear] = data
q.rear = (q.rear + 1) % len(q.arr)
}
func (q *Queue) Pop() int {
tmp := q.arr[q.front]
q.front = (q.front + 1) % len(q.arr)
return tmp
}
func (q *Queue) IsEmpty() bool {
return q.front == q.rear
}
func (q *Queue) IsFull() bool {
return q.front == (q.rear+1)%len(q.arr)
}
func (q *Queue) Length() int {
return (q.rear - q.front + len(q.arr)) % len(q.arr)
}
func main() {
q := NewQueue(10)
var i int
for i < 15 {
if q.IsFull() {
q.Pop()
}
q.Push(i)
fmt.Println("Push ", i, "front:", q.front, "rear:", q.rear, " | ", q.arr)
i++
}
}
运行结果:
Push 0 front: 0 rear: 1 | [0 0 0 0 0 0 0 0 0 0]
Push 1 front: 0 rear: 2 | [0 1 0 0 0 0 0 0 0 0]
Push 2 front: 0 rear: 3 | [0 1 2 0 0 0 0 0 0 0]
Push 3 front: 0 rear: 4 | [0 1 2 3 0 0 0 0 0 0]
Push 4 front: 0 rear: 5 | [0 1 2 3 4 0 0 0 0 0]
Push 5 front: 0 rear: 6 | [0 1 2 3 4 5 0 0 0 0]
Push 6 front: 0 rear: 7 | [0 1 2 3 4 5 6 0 0 0]
Push 7 front: 0 rear: 8 | [0 1 2 3 4 5 6 7 0 0]
Push 8 front: 0 rear: 9 | [0 1 2 3 4 5 6 7 8 0]
Push 9 front: 1 rear: 0 | [0 1 2 3 4 5 6 7 8 9]
Push 10 front: 2 rear: 1 | [10 1 2 3 4 5 6 7 8 9]
Push 11 front: 3 rear: 2 | [10 11 2 3 4 5 6 7 8 9]
Push 12 front: 4 rear: 3 | [10 11 12 3 4 5 6 7 8 9]
Push 13 front: 5 rear: 4 | [10 11 12 13 4 5 6 7 8 9]
Push 14 front: 6 rear: 5 | [10 11 12 13 14 5 6 7 8 9]
|
channel缓冲长度的确定
channel缓冲长度可以与上下游的速度比例成线性关系
sync Pool
sync.pool是一个临时对象存储池
因为项目中频繁的创建对象和回收内存,造成了GC的压力;而sync.pool可以缓存对象暂时不用但是之后会用到的对象,并且不需要重新分配内存;这在很大程度上降低了GC的压力,并且提高了程序的性能
首先,需要为sync.Pool设置一个New函数,这个函数就是当你获取不到对象时,返回的默认值。接下来,你就可以通过Get和Put方法检索对象和临时存储对象了
注:创建的这个pool在第一次使用过后就不能再被赋值了;还有就是Pool中的对象随时都会被移除,并且不会有通知机制。而如果你存储的是一个对象的引用,那么这个对象也会被回收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| package main
import "sync"
type Person struct {
Name string
}
//Initialing pool
var personalPool = sync.Pool{
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
New: func() interface{} {
return &Person{}
},
}
func main() {
// Get hold of an instance
newPerson := personalPool.Get().(*Person)
// Defer release function
// After that the same instance is
// reusable by another routine
defer personalPool.Put(newPerson)
// using instance
newPerson.Name = "Jack"
}
|
如果有一个对象需要频繁的创建,并且有很大的开销,那么你就可以使用sync.pool