Big Picture
年底了,面试/被面试的需求比较多,正好有需要让我整理一份面试题,干脆写一份,权当对自己的复习/整理了
- go语言切片和数组区别,go切片的原理:
- 数组[array]:
初始化后长度是固定的,无法修改其长度
初始化: array := [5]int{1,2,3,4,5}/ array := […]int{1,2,3,4,5}
Go中的数组是值类型
- 切片[slices]: s :=[]int{1,2,3} / s := make([]int,len,cap)
长度可变的"数组",可以追加元素,一是len长度,二是cap容量,长度是指已经被赋过值的最大下标+1,可通过内置函数len()获得
初始化:
切片是引用类型, 数据结构其实如下:
type SliceHeader struct { Data uintptr Len int Cap int }
其中,Data是uintptr, 就是个指针类型,表示Data指向的一块连续的内存空间,所以切片可以理解为一片连续的内存空间加上长度与容量标识,扩容后,如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一,如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
- go并发模型有哪些?现在需要启动30个协程来处理一个文件,怎么写
for i:=0; i < 30; i++ { go func{ // do something }() }
- go的channel介绍一下
channel用于goroutines之间的通信,让它们之间可以进行数据交换。像管道一样,一个goroutine_A向channel_A中放数据,另一个goroutine_B从channel_A取数据。
初始化: ch := make(chan int)
读写,ch <- VALUE(给ch赋值), res := <- ch(将channel数据send出去)
非缓存 channel, 同步操作,channel 里的数据没有send出去则一直阻塞
缓存 channel, 异步操作,channel 里的数据没有满之前,可以继续写入,如果写满了,且没有send出去则一直阻塞,缓存channel 有长度和容量2个概念,和切片类似。
- go的死锁
简单来说就是channel 阻塞,比如协程A 生产消息到channel ch,然后协程B 消费 channel ch的消息,然后在同一时间,channel ch的消息无法被协程B消费,且由于channel ch存在数据导致了协程A 被阻塞,从而产生死锁,最常见的死锁代码:
package main import ( "fmt" ) func main (){ counter := make(chan int) counter <- 32 fmt.Println(<-counter) }
上面的代码会报fatal error: all goroutines are asleep - deadlock!,原因是main函数也是个协程,协程Main 里的channel <- 32导致了协程Main被阻塞,导致了后续的<-counter无法消费channel的数据,从而导致了死锁,解决方案可以再启动一个协程
package main import ( "fmt" ) func main (){ counter := make(chan int) go func() { counter <- 32 }() fmt.Println(<-counter) }
- go为什么这么快
- golang 的协程是非常轻量级的用户态线程,不像线程那样需要内核-用户态的上下文切换
- 线程栈空间通常是 2M,Goroutine 栈空间最小 2K,可以动态扩容,最大64位机器是1G,32位是256MB
- go的G-P-M模型
- GC 还会周期性地将不再使用的内存回收,收缩栈空间。
- 什么的go的G-P-M 模型
goroutine 并非传统意义上的协程,现在主流的线程模型分三种:内核级线程模型、用户级线程模型和两级线程模型(也称混合型线程模型),传统的协程库属于用户级线程模型,而 goroutine 和它的 Go Scheduler 在底层实现上其实是属于两级线程模型。
- 内核级线程模型: 用户线程绑定一个内核线程,应用程序对线程的调度完全交给内核来控制。
- 用户级线程模型: 多个用户线程绑定一个内核线程,用户线程的控制都可以在用户态由程序控制,但问题在于假如用户线程里的一个线程被阻塞了,会导致其他线程都会阻塞。
- 混合型线程模型: 多个用户进程和多个内核线程绑定,这样当某个用户线程被阻塞了,也只会影响同样绑定到对于内核线程的用户线程。
GPM的含义: - G: 表示 Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。
- P: Processor,表示逻辑处理器, 对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量),P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256。
- M: Machine,OS 线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础,M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。
调度过程:
当通过 go 关键字创建一个新的 goroutine 的时候,它会优先被放入 P 的本地队列。为了运行 goroutine,M 需要持有(绑定)一个 P,接着 M 会启动一个 OS 线程,循环从 P 的本地队列里取出一个 goroutine 并执行。执行调度算法:当 M 执行完了当前 P 的 Local 队列里的所有 G 后,P 也不会就这么在那划水啥都不干,它会先尝试从 Global 队列寻找 G 来执行,如果 Global 队列为空,它会随机挑选另外一个 P,从它的队列里中拿走一半的 G 到自己的队列中执行。
- 函数参数是值传递还是引用传递
值传递 - go的继承和多态是如何实现的
- 继承: 基于结构体继承:
type Father struct { name string age int } type Son struct { Father agent int } student := Son{Father{"jake",46},12}
- 多态: 基于interface实现:
package main import "fmt" type Human interface { speak(language string) } type Chinese struct { } type American struct { } func (ch Chinese) speak(language string ) { fmt.Printf("Chinese speck %s\n",language) } func (am American ) speak(language string ) { fmt.Printf("America speck %s\n",language) } func main() { var ch Human var am Human ch = Chinese{} am = American{} ch.speak("Chinese") am.speak("English") }
- 进程,线程,协程
进程:进程是系统进行资源分配的基本单位,有独立的内存空间。
线程:线程是 CPU 调度和分派的基本单位,线程依附于进程存在,每个线程会共享父进程的资源。
协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程间切换只需要保存任务的上下文,没有内核的开销。
- sync.WaitGroup啥时候用,channel 啥时候用?
先看下sync.WaitGroup作用,主进程中启动了多个协程,为了避免主进程结束了,而协程没有结束,所以一般会sleep 对主进程进行阻塞,但sleep多少秒是为止的,所以通过sync.waitgroup 进行协程的阻塞,确保协程执行完以后,才到主进程, 好处在于处理多个协程很方便,但如果多个协程需要channel 进行通信,则不合适。
package main import ( "sync" "time" "fmt" ) func main() { var wg sync.WaitGroup wg.Add(1) go func(wg *sync.WaitGroup) { fmt.Println("Wait worker sleep") time.Sleep(5 * time.Second) wg.Done() }(&wg) wg.Wait() }
channel 一样可以实现类似的效果,通过channel 阻塞的特性进行实现,好处在于可以利用channel 进行多个进程的通信,但是需要创建很多个channel,影响性能
package main import ( "fmt" ) func main() { a := 1 b := make(chan int) go func() { a = (a + 1) * 100 b <- 0 }() <-b fmt.Println(a) }
go编译之后文件太大优化处理
go build -ldflags -w test.go //会去除 DWARF调试信息、符号信息go的并发安全性
说白了为了避免2个进程/协程同时写同一段内存,需要在操作这段内存(变量)前加锁
package main import ( "sync" "fmt" ) type TestData struct { lock sync.RWMutex data string } func (t *TestData) write(wg *sync.WaitGroup) { for i := 0; i < 10; i++ { t.lock.Lock() t.data = "a" t.lock.Unlock() } wg.Done() } func (t *TestData) read(wg *sync.WaitGroup) { for i := 0; i < 10; i++ { t.lock.Lock() fmt.Println(t.data) t.lock.Unlock() } wg.Done() } func main() { var wg sync.WaitGroup var t TestData wg.Add(1) go t.write(&wg) wg.Add(1) go t.read(&wg) wg.Wait() }
同样也可以通过channel保证线程安全
package main import ( "fmt" ) func write(data string, lch chan bool) { for i := 0; i < 10; i++ { data = "a" } lch <- true } func read(data string, lch chan bool) { for i := 0; i < 10; i++ { fmt.Println(data) } lch <- true } func main() { lch := make(chan bool) var data string go write(data, lch) go read(data, lch) <- lch fmt.Println("finished") }