1. Golang基础
1.1.1. select是随机的还是顺序的?
答: select会随机选择一个可用通道做收发操作
1.1.2. Go语言局部变量分配在栈还是堆?
答: Go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析,当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。
参考: go语言局部变量分配在栈还是堆
1.1.3. 简述一下你对Go垃圾回收机制的理解?
答:
- v1.1 STW
- v1.3 Mark STW, Sweep 并行
- v1.5 三色标记法
v1.8 hybrid write barrier(混合写屏障:优化STW)
参考: Golang垃圾回收剖析
1.1.4. 简述一下golang的协程调度原理?
答:
- M(machine): 代表着真正的执行计算资源,可以认为它就是os thread(系统线程)。
- P(processor): 表示逻辑processor,是线程M的执行的上下文。
G(goroutine): 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。
参考: Goroutine
1.1.5. 介绍下 golang 的 runtime 机制?
答: Runtime 负责管理任务调度,垃圾收集及运行环境。同时,Go提供了一些高级的功能,如goroutine, channel, 以及Garbage collection。这些高级功能需要一个runtime的支持. runtime和用户编译后的代码被linker静态链接起来,形成一个可执行文件。这个文件从操作系统角度来说是一个user space的独立的可执行文件。 从运行的角度来说,这个文件由2部分组成,一部分是用户的代码,另一部分就是runtime。runtime通过接口函数调用来管理goroutine, channel及其他一些高级的功能。从用户代码发起的调用操作系统API的调用都会被runtime拦截并处理。
Go runtime的一个重要的组成部分是goroutine scheduler。他负责追踪,调度每个goroutine运行,实际上是从应用程序的process所属的thread pool中分配一个thread来执行这个goroutine。因此,和java虚拟机中的Java thread和OS thread映射概念类似,每个goroutine只有分配到一个OS thread才能运行。
参考: go runtime的机制如何
1.1.6. 如何获取 go 程序运行时的协程数量, gc 时间, 对象数, 堆栈信息?
答:调用接口 runtime.ReadMemStats 可以获取以上所有信息, 注意: 调用此接口会触发 STW(Stop The World)
参考: https://golang.org/pkg/runtime/#ReadMemStats
如果需要打入到日志系统, 可以使用 go 封装好的包, 输出 json 格式. 参考:
更深入的用法就是将得到的运行时数据导入到 ES 内部, 然后使用 Kibana 做 golang 的运行时监控, 可以实时获取到运行的信息(堆栈, 对象数, gc 时间, goroutine, 总内存使用等等), 具体信息可以看 ReadMemStats 的那个结构体
1.1.7. 介绍下你平时都是怎么调试 golang 的 bug 以及性能问题的?
答:
- panic 调用栈
- pprof
- 火焰图(配合压测)
- 使用go run -race 或者 go build -race 来进行竞争检测
- 查看系统 磁盘IO/网络IO/内存占用/CPU 占用(配合压测)
1.1.8. 简单介绍下 golang 中 make 和 new 的区别
答: new(T) 是为一个 T 类型的新值分配空间, 并将此空间初始化为 T 的零值, 并返回这块内存空间的地址, 也就是 T 类型的指针 T, 该指针指向 T 类型值占用的那块内存. make(T) 返回的是初始化之后的 T, 且只能用于 slice, map, channel 三种类型. make(T, args) 返回初始化之后 T 类型的值, 且此新值并不是 T 类型的零值, 也不是 T 类型的指针 T, 而是 T 类型值经过初始化之后的引用.
参考1: Go中make和new的区别
参考2: Go中make()和new()的区别
1.1.9. 简单说下Golang逃逸分析
参考: Golang内存分配逃逸分析
1.1.10. 无缓冲 Chan 的发送和接收是否同步?
答:
- channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
- channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
1.1.11. Golang通过哪几种方式来实现并发控制,如何优雅的退出goroutine?
答:
- chan 通过无缓冲通道来实现多 goroutine 并发控制
通过sync包中的WaitGroup 实现并发控制
退出:
使用for-range退出
- 使用,ok退出
- 使用退出通道退出
参考:
https://golangnote.com/topic/184.html
http://lessisbetter.site/2018/12/02/golang-exit-goroutine-in-3-ways/
1.1.12. Golang的interface的特性和技巧,举例一些优雅的实现?
答:
空接口(empty interface)
空接口比较特殊,他不包含任何方法,但是他又可以表示任何类型
golang的所有基础类都实现了空接口,所有我们可以用[]interface表示结构不同的数组,比如:
func main() { data := make([]interface{}, 3) intData := 1 stringData := "abc" boolData := true data[0] = intData data[1] = stringData data[2] = boolData for _, v := range data { fmt.Println(v) } }
接口嵌套接口
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。
比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法。
type ReadWrite interface { Read(b Buffer) bool Write(b Buffer) bool } type Lock interface { Lock() Unlock() } type File interface { ReadWrite Lock Close() }
类型的选择与断言
- 一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用类型断言 来测试在某个时刻 接口varI 是否包含类型 T 的值:
v := varI.(T)
- 类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:
if v, ok := varI.(T); ok { // checked type assertion Process(v) return }
1.1.13. Golang的方法集?
答:
- 类型 T 方法集包含全部 receiver T 方法。
- 类型 T 方法集包含全部 receiver T + T 方法。
- 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
- 如类型 S 包含匿名字段 T,则 S 和 S 方法集包含 T + *T 方法。
- 不管嵌入 T 或 T,S 方法集总是包含 T + *T 方法。
用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。
Go 语言中内部类型方法集提升的规则:
类型 T 方法集包含全部 receiver T 方法。
package main import ( "fmt" ) type T struct { int } func (t T) test() { fmt.Println("类型 T 方法集包含全部 receiver T 方法。") } func main() { t1 := T{1} fmt.Printf("t1 is : %v\n", t1) t1.test() }
输出结果:
t1 is : {1}
类型 T 方法集包含全部 receiver T 方法。
类型 T 方法集包含全部 receiver T + T 方法。
package main import ( "fmt" ) type T struct { int } func (t T) testT() { fmt.Println("类型 *T 方法集包含全部 receiver T 方法。") } func (t *T) testP() { fmt.Println("类型 *T 方法集包含全部 receiver *T 方法。") } func main() { t1 := T{1} t2 := &t1 fmt.Printf("t2 is : %v\n", t2) t2.testT() t2.testP() }
输出结果:
t2 is : &{1}
类型 *T 方法集包含全部 receiver T 方法。
类型 T 方法集包含全部 receiver T 方法。
给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。
package main import ( "fmt" ) type S struct { T } type T struct { int } func (t T) testT() { fmt.Println("如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。") } func main() { s1 := S{T{1}} s2 := &s1 fmt.Printf("s1 is : %v\n", s1) s1.testT() fmt.Printf("s2 is : %v\n", s2) s2.testT() }
输出结果:
s1 is : 1
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
s2 is : &1
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
如类型 S 包含匿名字段 T,则 S 和 S 方法集包含 T + *T 方法。
这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
package main import ( "fmt" ) type S struct { T } type T struct { int } func (t T) testT() { fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法") } func (t *T) testP() { fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法") } func main() { s1 := S{T{1}} s2 := &s1 fmt.Printf("s1 is : %v\n", s1) s1.testT() s1.testP() fmt.Printf("s2 is : %v\n", s2) s2.testT() s2.testP() }
输出结果:
s1 is : 1
如类型 S 包含匿名字段 T,则 S 和 S 方法集包含 T 方法
如类型 S 包含匿名字段 T,则 S 和 S 方法集包含 *T 方法
s2 is : &1
如类型 S 包含匿名字段 T,则 S 和 S 方法集包含 T 方法
如类型 S 包含匿名字段 T,则 S 和 S 方法集包含 *T 方法
叶落山城秋: 上面的其实很好理解!但是我懵逼的是什么呢,感觉上面 不管是 T 还是 T 还是 S或者 S 发现都可以!
那什么情况下是不可以呢 如果 是 代码考题第10题 如果上面是接口
发现如果是 不ok的.. 但是 var peo = Student{} 又是ok的! 那 var peo People = Student{} 这样写的意义在于?? 先记着吧. 如果哪位大佬比较明白这块,跪请留言告知!十分感谢
1.1.14. Golang的GMP模型?
- M(Work Thread) -- 表示操作系统的线程,它是被操作系统管理的线程,与POSIX中的标准线程非常类似
- G(Goroutine) -- 表示Goroutine,每一个Goroutine都包含堆栈,指令指针和其他用于调度的重要信息
P(Processor) -- 表示调度的上下文,它可以被看做一个运行于线程M上的本地调度器
三者关系:
- 每一个运行的M都必须绑定一个P,线程M创建后会去检查并执行G(goroutine)对象
- 每一个P保存着一个协程G的队列
- 除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列
- M从队列中提取G,并执行
- P的个数就是
GOMAXPROCS
(最大256),启动时固定的,一般不修改 - M的个数和P的个数不一定一样多(会有休眠的M或P不绑定M) (最大10000)
- P是用一个全局数组(255)来保存的,并且维护着一个全局的P空闲链表
1.1.15. 用信道实现主程等待协程2s,如果超过2s,主程直接结束(不用sleep)?
func main() {
start := time.Now()
wait := make(chan int,1)
go func() {
fmt.Println("做点东西")
time.Sleep(1*time.Second)
wait<-2
}()
fmt.Println("这里是主程序")
select {
case nums:= <-wait:
fmt.Println(nums)
case <-time.After(2*time.Second):
fmt.Println("2秒后")
}
fmt.Println(time.Since(start))
}
1.1.16. Golang里的结构体是否能进行比较?
先说答案, 部分情况下是可以比较的
比如:
- 结构体属性为一些常规类型比如 int,string 可以进行比较
- 如果结构体属性是一些引用类型,比如 切片,map 等,不可进行比较
假设结构体属性都是常规的类型
如果是同一个结构体,那肯定是可以比较的(申明一个结构体)
func() { type Test struct{ A string B int } a := Test{ A:"1", B:1, } b := Test{ A:"1", B:1, } fmt.Println(a==b) }
如果是两个结构体,但是属性是一样的,且顺序都一样,是可以比较的, 如果顺序不一样,则不能比较
func() { a := struct { a int b string }{ a: 1, b: "1", } b := struct { a int b string }{ a: 1, b: "1", } fmt.Println(a==b) }
另外,如果要比较两个结构体,可以用 reflect.DeepEqual()
方法进行比较
以上部分资源来自网络
参考: go 夜读
参考: 方法集