Golang面试题整理(二)

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关键字只能作用于函数或者函数调用。

  1. 延迟函数的参数在defer语句出现时就已经确定了
  2. 延迟函数按后进先出LIFO的顺序执行,即先出现的defer最后执行
  3. 延迟函数可能操作主函数的具体变量名称返回值
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