1.什么是协程泄露(goroutine leak)?
协程泄露是指,在程序运行过程中,有一些协程由于某些原因,无法正常退出。
2.协程泄露的危害?
协程的运行是需要占用内存和 CPU 时间的,一旦这种协程越来越多,会导致内存无端被浪费,CPU 时间片被占用,程序会越来越卡。
3.协程泄露的原因?
goroutine由于channel的读/写端退出而一直阻塞,导致goroutine一直占用资源,而无法退出。
goroutine由于nil channel而阻塞。
goroutine进入死循环中,导致资源一直无法释放。
goroutine同步锁(mutex)操作不当导致
goroutine waitgroup Add的数量和Done的数量不一致,导致一直Wait
以下是几种场景:
package test
import (
"fmt"
"math/rand"
"os"
"runtime"
"testing"
"time"
)
/**
泄露场景1-以及解决办法
*/
func TestChanLeak(t *testing.T) {
randomStream := func(done chan interface{}) <-chan int{
rsc := make(chan int )
go func() {
defer fmt.Println("当前协程结束...")
defer close(rsc)
for {
select {
case x, ok := <-done:
if !ok {
fmt.Println("x is:", x) // x is: <nil>
}
return
case rsc <- rand.Int()://send 阻塞
}
}
}()
return rsc
}
//channel关闭,也可以通过 contextWithCancel 来结束 goroutine
done := make(chan interface{})
//调用
rst := randomStream(done)
fmt.Println("start...")
for i := 1; i <=3; i++ {
fmt.Println("rand is:",<-rst)
}
fmt.Fprintf(os.Stderr, "%d\n", runtime.NumGoroutine())
// close的特性
// After the last value has been received
// from a closed channel c, any receive from c will succeed without
// blocking, returning the zero value for the channel element. The form
// x, ok := <-c
// will also set ok to false for a closed channel.
close(done)
//模拟一个耗时操作,10秒的延迟,在这期间 rst 还在阻塞
time.Sleep(10 * time.Second)
fmt.Fprintf(os.Stderr, "%d\n", runtime.NumGoroutine())
}
/**
泄露场景2-以及解决办法(这个解决办法就是主协程退出)
*/
func routineTest() {
//进入入死循环
for {
fmt.Println("开启goroutine")
time.Sleep(10 * time.Second)
}
}
func TestChan(t *testing.T) {
fmt.Println("开始...")
//通过 runtime.NumGoroutine() 获取当前运行的goroutine的数量
fmt.Fprintf(os.Stderr, "%d\n", runtime.NumGoroutine())
go routineTest()
fmt.Fprintf(os.Stderr, "%d\n", runtime.NumGoroutine())
fmt.Println("结束...")
}
/**
泄露场景3-nil channel
*/
func TestNilChan(t *testing.T) {
defer func() {
time.Sleep(time.Second)
fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
}()
//ch 就是一个 nil channel, 因为只是声明,但是没有初始化这个ch就直接使用了
var ch chan int
go func() {
<-ch // receive nil channel
// ch<- // send nil channel
}()
//defer func() {
// time.Sleep(time.Second)
// fmt.Println("the number1 of goroutines: ", runtime.NumGoroutine())
//}()
//
//done := make(chan struct{})
//
//var ch chan int
//go func() {
// fmt.Println("the number2 of goroutines: ", runtime.NumGoroutine())
// defer close(done)
//}()
//
////fatal error: all goroutines are asleep - deadlock!
//select {
//case <-ch:// The receive operation might block a goroutine because of the 'nil' channel
//case <-done:
// return
//}
}
/**
泄露场景4-waitGroup
*/
func TestWaitGroup(t *testing.T) {
defer func() {
time.Sleep(time.Second)
fmt.Println("the number1 of goroutines: ", runtime.NumGoroutine())
}()
// 无法退出
go handle()
time.Sleep(5 * time.Second)
}
4.无缓冲的channel和有缓冲的channel的区别?
之前以为有缓冲与无缓冲的区别是 无缓冲的缓冲为 1 的缓冲式 ,其实不是的,无缓冲 就是缓冲为0,
func TestCacheChannel(t *testing.T) {
c := make(chan int, 0) // 或者 make(chan int)
//不带缓冲的channel可以通过Goroutine给当前channel的发送数据,不会阻塞线程,这个是什么原因还在研究,唯一的解释就是 不带缓冲的channel,的send 和 receive 不能是同一个 goroutine,必须要重新起一个goroutine
//go func() {
// c <- 1
//}()
//c := make(chan int, 1)
//c <- 1 //不带缓冲的channel 这里会阻塞,但是带缓冲就不会阻塞
fmt.Println(<-c)
}
二者区别就是一个会阻塞另一个不会阻塞(或者 同步与非同步的区别)
5.channel 底层数据原理?
通过var声明或者make函数创建的channel变量是一个存储在栈上的指针,占用8个字节,指向堆上的hchan结构体,该结构体在src/runtime/chan.go中
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
//G1
func main(){
...
for _, task := range hellaTasks {
ch <- task //sender
}
...
}
//G2
func worker(ch chan Task){
for {
//接受任务
task := <- ch //recevier
//假设设立会护理很长时间,那么 G1可能会阻塞
process(task)
}
}
其中G1是发送者,G2是接收,因为ch是长度为3的带缓冲channel,初始的时候hchan结构体的buf为空,sendx和recvx都为0,当G1向ch里发送数据的时候,会首先对buf加锁,然后将要发送的数据copy到buf里,并增加sendx的值,
最后释放buf的锁。然后G2消费的时候首先对buf加锁,然后将buf里的数据copy到task变量对应的内存里,增加recvx,最后释放锁。整个过程,G1和G2没有共享的内存,底层通过hchan结构体的buf,使用copy内存的方式进行通信,
最后达到了共享内存的目的,这完全符合CSP的设计理念。
CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型。
Golang,其实只用到了 CSP 的很小一部分,即理论中的 Process/Channel(对应到语言中的 goroutine/channel)
G1和G2的调用涉及到golang的GPM模型
6.GPM模型原理?
了解GPM模型之前,先分析一下线程的三种模型: 用户级线程模型、内核级线程模型和两级线程模型(也称混合型线程模型)
它们之间最大的差异就在于用户线程与内核调度实体(KSE,Kernel Scheduling Entity)之间的对应关系上。而所谓的内核调度实体 KSE 就是指可以被操作系统内核调度器调度的对象实体。简单来说 KSE 就是内核级线程,
是操作系统内核的最小调度单元,也就是我们写代码的时候通俗理解上的线程了。
用户级线程模型:
用户线程与内核线程 KSE 是一对一(1 : 1)的映射模型,也就是每一个用户线程绑定一个实际的内核线程,而线程的调度则完全交付给操作系统内核去做,应用程序对线程的创建、终止以及同步都基于内核提供的系统调用来完
成,大部分编程语言的线程库(比如 Java 的 java.lang.Thread、C++11 的 std::thread 等等)都是对操作系统的线程(内核级线程)的一层封装,创建出来的每个线程与一个独立的 KSE 静态绑定,因此其调度完全由操作
系统内核调度器去做,也就是说,一个进程里创建出来的多个线程每一个都绑定一个 KSE。

内核级线程模型:
用户线程与内核线程 KSE 是多对一(N : 1)的映射模型,多个用户线程的一般从属于单个进程并且多线程的调度是由用户自己的线程库来完成,线程的创建、销毁以及多线程之间的协调等操作都是由用户自己的线程库来负责而
无须借助系统调用来实现。一个进程中所有创建的线程都只和同一个 KSE 在运行时动态绑定,也就是说,操作系统只知道用户进程而对其中的线程是无感知的,内核的所有调度都是基于用户进程。这种实现方式相比内核级线程可
以做的很轻量级,对系统资源的消耗会小很多,因此可以创建的线程数量与上下文切换所花费的代价也会小得多。但该模型有个原罪:并不能做到真正意义上的并发,假设在某个用户进程上的某个用户线程因为一个阻塞调用(比如
I/O 阻塞)而被 CPU 给中断(抢占式调度)了,那么该进程内的所有线程都被阻塞(因为单个用户进程内的线程自调度是没有 CPU 时钟中断的,从而没有轮转调度),整个进程被挂起。即便是多 CPU 的机器,也无济于事,因
为在用户级线程模型下,一个 CPU 关联运行的是整个用户进程,进程内的子线程绑定到 CPU 执行是由用户进程调度的,内部线程对 CPU 是不可见的,此时可以理解为 CPU 的调度单位是用户进程。所以很多的协程库会把自己一
些阻塞的操作重新封装为完全的非阻塞形式,然后在以前要阻塞的点上,主动让出自己,并通过某种方式通知或唤醒其他待执行的用户线程在该 KSE 上运行,从而避免了内核调度器由于 KSE 阻塞而做上下文切换,这样整个进程
也不会被阻塞了。

两级线程模型:
用户线程与内核 KSE 是多对多(N : M)的映射模型:首先,区别于用户级线程模型,两级线程模型中的一个进程可以与多个内核线程 KSE 关联,也就是说一个进程内的多个线程可以分别绑定一个自己的 KSE,这点和内核级线
程模型相似;其次,又区别于内核级线程模型,它的进程里的线程并不与 KSE 唯一绑定,而是可以多个用户线程映射到同一个 KSE,当某个 KSE 因为其绑定的线程的阻塞操作被内核调度出 CPU 时,其关联的进程中其余用户线
程可以重新与其他 KSE 绑定运行。即用户调度器实现用户线程到 KSE 的『调度』,内核调度器实现 KSE 到 CPU 上的『调度』。Go语言中的并发就是使用的这种实现方式,Go为了实现该模型自己实现了一个运行时调度器来负
责Go中的"线程"与KSE的动态关联。

GPM模型:
在 Go 语言中,每一个 goroutine 是一个独立的执行单元,相较于每个 OS 线程固定分配 2M 内存的模式,goroutine 的栈采取了动态扩容方式, 初始时仅为2KB,随着任务执行按需增长,最大可达 1GB(64 位机器最大是 1G,
32 位机器最大是 256M),且完全由 golang 自己的调度器 Go Scheduler 来调度。此外,GC 还会周期性地将不再使用的内存回收,收缩栈空间。 因此,Go 程序可以同时并发成千上万个 goroutine 是得益于它强劲的调度器和
高效的内存模型。
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 个。
每个 P 维护一个 G 的本地队列;
当一个 G 被创建出来,或者变为可执行状态时,就把他放到 P 的本地可执行队列中,如果满了则放入Global;
当一个 G 在 M 里执行结束后,P 会从队列中把该 G 取出;如果此时 P 的队列为空,即没有其他 G 可以执行, M 就随机选择另外一个 P,从其可执行的 G 队列中取走一半。
调度算法如下:

调度过程:
当通过 go 关键字创建一个新的 goroutine 的时候,它会优先被放入 P 的本地队列。为了运行 goroutine,M 需要持有(绑定)一个 P,接着 M 会启动一个 OS 线程,循环从 P 的本地
队列里取出一个 goroutine 并执行。执行调度算法:当 M 执行完了当前 P 的 Local 队列里的所有 G 后,P 也不会就这么在那划水啥都不干,它会先尝试从 Global 队列寻找 G 来执行,
如果 Global 队列为空,它会随机挑选另外一个 P,从它的队列里中拿走一半的 G 到自己的队列中执行。
参考:
https://www.minazuki.cn/post/blog_os/blog_os-1btagnqm2aabq/blog_os-1bti5cvfu5of1/ //理论
https://blog.csdn.net/guoafite/article/details/114833136 //理论
https://www.kuangstudy.com/bbs/1359135859894509570 //理论
https://blog.csdn.net/u010853261/article/details/85887948 //gopark()[用于协程的切换], goready()[主要功能就是唤醒某一个goroutine,该协程转换到runnable的状态,并将其放入P的local queue,等待调度]
7.什么是rune类型?
rune 是 int32的别名,主要用具计算不同字符下字符串的真实长度。(参考另一篇文章:go之rune关键字)
8.struct能不能比较?
回答这个问题之前先看一下golang中哪些数据类型可以比较哪些不可以比较
可比较:Integer,Floating,String,Boolean,Complex(复数型),Pointer,Channel,Interface,Array
不可比较:Slice,Map,Function
func TestCompare(t *testing.T) {
//都是可比较成员
type S struct {
Name string
Age int
Address *int
}
//没有初始化的时候 都是nil
//var a S
//var b S
//=== RUN TestCompare
//true
//--- PASS: TestCompare (0.00s)
//PASS
//fmt.Println(a == b)
// 分别初始化
var n = 1
var m = 2
a := S{
Name: "aa",
Age: 1,
Address: &n,
}
b := S{
Name: "aa",
Age: 1,
Address: &m, // 若是&n,就返回true, 如是&m,就返回false,因为 指针指向的地址不一样
}
//=== RUN TestCompare
//true
//--- PASS: TestCompare (0.00s)
//PASS
fmt.Println(a == b)
//结论:golang中 Slice,Map,Function 这三种数据类型是不可以直接比较的。我们再看看S结构体,该结构体并没有包含不可比较的成员变量,所以该结构体是可以直接比较的。
//包含不可比较成员
type S2 struct {
Name string
Age int
Address *int
Data []int
}
a2 := S2{
Name: "aa",
Age: 1,
Address: new(int),
Data: []int{1, 2, 3},
}
b2 := S2{
Name: "aa",
Age: 1,
Address: new(int),
Data: []int{1, 2, 3},
}
//这样 直接编辑器报错,因为 S2包含不可比较类型
//fmt.Println(a2 == b2)
//通过 reflect.DeepEqual 反射中的函数进行比较是可以的
//=== RUN TestCompare
//true
//--- PASS: TestCompare (0.00s)
//PASS
fmt.Println(reflect.DeepEqual(a2, b2))
//拓展:
//DeepEqual函数用来判断两个值是否深度一致。具体比较规则如下:
//
//不同类型的值永远不会深度相等当两个数组的元素对应深度相等时,两个数组深度相等当两个相同结构体的所有字段对应深度相等的时候,两个结构体深度相等当两个函数都为nil时,两个函数深度相等,其他情况不相等(相同函数也不相等)当两个interface的真实值深度相等时,两个interface深度相等map的比较需要同时满足以下几个
//
//两个map都为nil或者都不为nil,并且长度要相等相同的map对象或者所有key要对应相同map对应的value也要深度相等
//指针,满足以下其一即是深度相等
//
//两个指针满足go的==操作符两个指针指向的值是深度相等的
//切片,需要同时满足以下几点才是深度相等
//
//两个切片都为nil或者都不为nil,并且长度要相等两个切片底层数据指向的第一个位置要相同或者底层的元素要深度相等注意:空的切片跟nil切片是不深度相等的
//其他类型的值(numbers, bools, strings, channels)如果满足go的==操作符,则是深度相等的。要注意不是所有的值都深度相等于自己,例如函数,以及嵌套包含这些值的结构体,数组等
//两个不同的结构体实例,如果S3 S4成员不同,一定也是不能比较的
type S3 struct {
Name string
Age int
Arr [2]bool
ptr *int
}
type S4 struct {
Name string
Age int
Arr [2]bool
ptr *int
}
var k = 1
var j = 2
var a3 = S3{
Name: "aa",
Age: 1,
Arr: [2]bool{true, false},
ptr: &k,
}
var b3 = S4{
Name: "aa",
Age: 1,
Arr: [2]bool{true, false},
ptr: &j,//若是&k,就返回true, 如是&j,就返回false,因为 指针指向的地址不一样
}
//由于结构体类型不一样,编辑器报错,不能比较
//fmt.Println(a3 == b3)
//类型强转以后,就可以比较了
//=== RUN TestCompare
//false
//--- PASS: TestCompare (0.00s)
//PASS
fmt.Println(a3 == S3(b3))
}
根据上面的论证可以知道:结构体能不能比较是要分情况的
9.struct可以作为map的key吗?
func TestMapKey(t *testing.T) {
type S1 struct {
Name string
Age int
Arr [2]bool
ptr *int
slice []int
map1 map[string]string
}
type S2 struct {
Name string
Age int
Arr [2]bool
ptr *int
}
n := make(map[S2]string, 0) // 无报错
//=== RUN TestMapKey
//map[]--- PASS: TestMapKey (0.00s)
//PASS
fmt.Print(n)
//m := make(map[S1]string, 0)//Invalid map key type: comparison operators == and != must be fully defined for the key type
//fmt.Println(m)
}
根据上面的论证可以知道:struct必须是可比较的,才能作为key,否则编译时报错
10.如何判断map中是否包含某个key?
func TestMapKeyExist(t *testing.T){
demo := map[string]string{
"age": "12345",
}
//判断方法
if v, ok := demo["age"]; ok {
fmt.Println(v)
}
}
11.map如何顺序读取?
解决方案:通过sort中的排序包进行对map中的key进行排序。然后遍历key
func TestMapOrder(t *testing.T){
var m = map[string]int{
"9": 0,
"2": 1,
"5": 2,
"1": 3,
}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys) //升序
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
}
=== RUN TestMapOrder
Key: 1 Value: 3
Key: 2 Value: 1
Key: 5 Value: 2
Key: 9 Value: 0
--- PASS: TestMapOrder (0.00s)
PASS
12.关于switch语句,下面说法正确的是?
A. 单个case中,可以出现多个结果的选项;
B. 需要使用break来明确退出一个case;
C. 只有在case中明确添加fallthrought关键字,才会继续执行紧跟的下一个case;
D. 条件表达式必须为常量或者整数。
AC
分析:
支持多条件匹配,使用逗号分隔,例如:case val1,val2,val3
不同的case之间不使用break分隔,默认自带break,只会执行一个case;
如果想要执行多个case,需要使用fallthrought关键字,且不会判断下一个case的表达式是否 为true;也可以使用break终止。
switch语句还可以被用于type-switch来判断某个interface变量中实际存储的变量类型。如:switch i := x.(type){}
13.下面的代码会打印什么?
func TestSwitch(t *testing.T) {
switch alwaysFalse()
{
case true:
fmt.Println(true)
case false:
fmt.Println(false)
}
// 代码中,switch后面没有跟{,而是另起了一行,在这种情况下,alwaysFalse后自动添加了分号";",上述代码等价于:
//switch alwaysFalse(); true {
//case true: fmt.Println("true")
//case false: fmt.Println("false")
//}
// 所以会返回 true,如果 { 没有换行 就返回 false
}
14.下面的代码会打印什么?
func print() {
//这里相当于吧 GPM 中的P 设置为1,所以
// for 循环的时候 会先执行main 里的 for,在执行 go func,就是先主协程再子协程,最后全部输出10
//这实际是一个 协程引用循环变量的问题
// 解决办法是,再声明一个变量 i2 := i,将 i2传给 func;或者 给func 传递参数,将 i 传递
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
wg.Done()
}()
}
for j := 0; j < 10; j++ {
go func() {
fmt.Println(j)
wg.Done()
}()
}
wg.Wait()
}
func TestWt(t *testing.T) {
print()
}
=== RUN TestWt
2
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
10
--- PASS: TestWt (0.00s)
PASS
15.下面的代码会打印什么?
func TestDefer(t *testing.T){
//这个简单,defer 肯定最后执行,所以是31
if true {
defer fmt.Println("1")
}else {
defer fmt.Println("2")
}
fmt.Println("3")
}
=== RUN TestDefer
3
1
--- PASS: TestDefer (0.00s)
PASS
16.golang中大多数数据类型都可以转化为有效的JSON文本,下面几种类型除外()
A 指针
B channel
C complex
D 函数
BCD
分析:
golang 中的类型比如:channel(通道)、complex(复数类型)、func(函数)均不能进行 JSON 格式化。
有疑问的地方可能是在A选项指针。
其实 Pointer(指针)也是能被 JSON 格式化的,因为指针会被系统隐式转换为指针所指向的具体对象值,具体的对象值是可以被JSON格式化的。
17.下面的代码会打印什么?
func printEx(x int) (func(), func()) {
return func() {
println(x)
x += 10
},
func() {
println(x)
}
}
func TestCodeExec(t *testing.T) {
a, b := printEx(1)
a()
b()
}
//结果如下
//=== RUN TestCodeExec
//1
//11
//--- PASS: TestCodeExec (0.00s)
//PASS
18.下面的代码会打印什么?
func TestCodeExec(t *testing.T) {
defer func() {
if err := recover(); err != nil {
fmt.Println("++++")
f := err.(func() string) // err 是一个func类型,打印出一个 0x894040 说明 func是引用类型
fmt.Println(err,"--", f(),"--", reflect.TypeOf(err).Kind().String())
}else {
fmt.Println("fatal")
}
}()
//这个defer 会比上面的先执行
defer func() {
panic(func() string {
return "defer panic"
})
}()
//这个defer 会比上面的先执行
defer func() {
panic(func() string {
return "defer panic2"
})
}()
//最先执行
panic("panic")
//=== RUN TestCodeExec
//++++
//0x894040 -- defer panic -- func
//--- PASS: TestCodeExec (0.00s)
//PASS
结论:
如果过有多个panic,那么最终 recover 会拦截最后的那个panic的错误
}
19.golang中的引用类型包括()
A.数组
B.map
C.channel
D.interface
BCD
解析:go中引用类型有:指针、slice切片、管道channel、接口interface、map、函数等
