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
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
协程和线程的区别
goroutine初始栈大小,上下文如何切换,存在哪里
- 初始栈:2kb


GMP说一下,P有多少个,G有多少个,M有多少个。系统调用的时候M会如何,网络IO的时候M会怎样。
- P的个数默认等于cpu个数,可以通过GOMAXPROCS环境变量更改
- M的默认值10000,go程序启动时,会设置M的最大数量,默认10000.runtime/debug中的SetMaxThreads函数,设置M的最大数量,实际上是按需创建

网络io的时候g的状态为变成Gwaiting状态,M可继续执行下一个状态为Grunable的g,如果没有,则为sleeping

系统调用的时候,g的状态为Gsyscall,M为变为sleep状态
--------------------------------------------------------------------
Go的GC说一下,扫描从哪里发起?

golang 的所有类型都对应一个 _type 结构,可以在 runtime/type.go 里面找到
--------------------------------------------------------------------
Go的内存分配说一下

Go有哪些坑?for range为什么会有坑?
- 因为使用

Go的map是如何实现的,是并发安全的吗?sync.map是如何实现的?
- 不是并发安全的
---------------------------------------------------------------------
CAS知道吗,在Go的哪些地方用到了?

原子操作主要由硬件提供支持,锁一般是由操作系统提供支持,比起直接使用锁,使用CAS这个过程不需要形成临界区和创建互斥量,所以会比使用锁更加高效。

从硬件层面来实现原子操作,有两种方式:

1、总线加锁:因为CPU和其他硬件的通信都是通过总线控制的,所以可以通过在总线加LOCK#锁的方式实现原子操作,但这样会阻塞其他硬件对CPU的访问,开销比较大。

2、缓存锁定:频繁使用的内存会被处理器放进高速缓存中,那么原子操作就可以直接在处理器的高速缓存中进行而不需要使用总线锁,主要依靠缓存一致性来保证其原子性。

缺点:只能对一个变量原子性操作

sync.Mutex库中使用到对应的操作CAS
————————————————

sync.pool是如何实现的

channel的优点是什么,如何实现的(腾讯云)
Go 通过 channel 实现 CSP 通信模型,主要用于 goroutine 之间的消息传递和事件通知。


操作	nil channel	closed channel	not nil, not closed channel
close	panic	panic	正常关闭
读 <- ch	阻塞	读到对应类型的零值	阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞
写 ch <-	阻塞	panic	阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞

————————————————
原文作者:JaguarJack
转自链接:https://learnku.com/articles/32142
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。
---------------------------------------------------------------------
channel的使用场景。close channel的时候发生了什么?
---------------------------------------------------------------------
Go select是怎么用的,具体如何实现的?

Golang 的 select 机制可以理解为是在语言层面实现了和 select, poll, epoll 相似的功能:监听多个描述符的读/写等事件,一旦某个描述符就绪(一般是读或者写事件发生了),就能够将发生的事件通知给关心的应用程序去处理该事件。 golang 的 select 机制是,监听多个channel,每一个 case 是一个事件,可以是读事件也可以是写事件,随机选择一个执行,可以设置default,它的作用是:当监听的多个事件都阻塞住会执行default的逻辑。

当我们在 Go 语言中使用 select 控制结构时,会遇到两个有趣的现象:

select 能在 Channel 上进行非阻塞的收发操作;
select 在遇到多个 Channel 同时响应时,会随机执行一种情况;

小结:
空的 select 语句会被转换成调用 runtime.block 直接挂起当前 Goroutine;
如果 select 语句中只包含一个 case,编译器会将其转换成 if ch == nil { block }; n; 表达式;
首先判断操作的 Channel 是不是空的;
然后执行 case 结构中的内容;
如果 select 语句中只包含两个 case 并且其中一个是 default,那么会使用 runtime.selectnbrecv 和 runtime.selectnbsend 非阻塞地执行收发操作;
在默认情况下会通过 runtime.selectgo 获取执行 case 的索引,并通过多个 if 语句执行对应 case 中的代码;
在编译器已经对 select 语句进行优化之后,Go 语言会在运行时执行编译期间展开的 runtime.selectgo 函数,该函数会按照以下的流程执行:

随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成锁定顺序 lockOrder;
根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel;
如果存在,直接获取 case 对应的索引并返回;
如果不存在,创建 runtime.sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒;
当调度器唤醒当前 Goroutine 时,会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 对应的索引;
select 关键字是 Go 语言特有的控制结构,它的实现原理比较复杂,需要编译器和运行时函数的通力合作。
---------------------------------------------------------------------
如果我希望主协程通知并且等待子协程关闭,应该如何做?context了解吗,怎么用的,如何实现的?

---------------------------------------------------------------------
写一个生产者消费者模型吧
用channel实现
---------------------------------------------------------------------
Gin的中间件是如何实现的
中间件的作用
中间件的作用在于,可以对于多个不同的请求,都应用某些同样的代码,做到可以复用代码的用途。例如,登陆、日志等等。
---------------------------------------------------------------------
gin的中间件
gin的中间件,在gin里面是一个HandlerFunc,实际上跟路由的处理函数是一样的,一个中间件,即是一个带 *gin.Context 参数的函数。
gin中间件可以处理在路由函数前,也可以处理在路由函数后的事务。主要根据c.Next()在中间件中的调用位置来决定。灵活度很高。

gin的代码实现
读gin的代码(v1.6.3)在run之后,进入到Engine的handleHTTPReqquest之后,就调用c.Next(),包括了正式的处理路由业务的方法,也是从这个 c.Next() 调用,开始执行。

Next的代码
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}
Next会依序执行路由的各个Handler(包括中间件、路由业务方法)。c.index则是当前执行到的Handler标识,而且就是根据这个index,可以保证不会重复执行,并且在中间件里面调用Next之后,会进入到下一个handler,而在执行完之后,又可以依次回到调用的地方。进行后置的处理。
同时,即使在中间件或者路由业务方法里面不调用Next, 亦可以进行到下一个handler的执行。
--------------------------------------------------------------------
Gin的一次请求整个过程是如何调用的
1)创建 Gin 实例
2)注册路由及处理函数
3)启动 Web 服务

Gin 实例创建通过 gin.Default() 方法完成
Default() 方法实现如下功能:
1)创建 Gin 框架对象 Engine
2)配置 Gin 默认的中间件,Logger() 和 Recovery(),其实现分别位于 logger.go 和 recovery.go 文件内
3)返回 Gin 框架对象


完成 Gin 的实例化之后,我们可以通过 r.GET("/ping", func(c *gin.Context) {}) 定义 HTTP 路由及处理 handler 函数。

gin.Run() 是 net/http 标准库 http.ListenAndServe(addr, router) 的简写,功能是将路由连接到 http.Server 启动并监听 HTTP 请求。

使用Gin.Context 进行上下文处理


--------------------------------------------------------------------
Gin的基数树知道吗,说一下
基数树是gin实现路由的存储的方式。当一个url进行路由注册时,会根据method获取不同的root tree,path也会完整的存入到对应的基数树中,每次有新的url进行路由注册时,会进行公共子序列的判断,如果有公共部分,将公共部分提取出来单独存储,把不同的部分当作其子树存于children。
--------------------------------------------------------------------
Gin请求里的request buffer可以读几次
默认情况下只能一次,从request中读取后
方法- :
ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(data)) // 关键点
方法二:
ctx.Set("name",info.Name)
ctx.Set("phone",info.Phone)
ctx.Set("data",info.Data)
---------------------------------------------------------------------
goroutine的泄露如何排查?pprof了解吗,说一下
--------------------------------------------------------------------

老调度器的流程及缺点
流程:
调用 schedlock 方法来获取全局锁。

获取全局锁成功后,将当前 Goroutine 状态从 Running(正在被调度) 状态修改为 Runnable(可以被调度)状态。

调用 gput 方法来保存当前 Goroutine 的运行状态等信息,以便于后续的使用。

调用 nextgandunlock 方法来寻找下一个可运行 Goroutine,并且释放全局锁给其他调度使用。

获取到下一个待运行的 Goroutine 后,将其运行状态修改为 Running。

调用 runtime·gogo 方法,将刚刚所获取到的下一个待执行的 Goroutine 运行起来,进入下一轮调度。
————————————————

缺点:
创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。
M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M'。
系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。