面试问题
# 基础相关
# map怎么实现顺序读取
把map中的key通过sort包排序
package main
import (
"fmt"
"sort"
)
func main() {
var m = map[string]int{
"hello": 0,
"morning": 1,
"keke": 2,
"jame": 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])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 两个nil可能不相等吗
Go中两个Nil可能不相等。
接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。
两个接口值比较时,会先比较 T,再比较 V。 接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
func main() {
var p *int = nil
var i interface{} = p
fmt.Println(i == p) // true
fmt.Println(p == nil) // true
fmt.Println(i == nil) // false
}
2
3
4
5
6
7
这个例子中,将一个nil非接口值p赋值给接口i,此时,i的内部字段为(T=*int, V=nil),i与p作比较时,将 p 转换为接口后再比较,因此 i == p
,p 与 nil 比较,直接比较值,所以 p == nil
。
但是当 i 与nil比较时,会将nil转换为接口(T=nil, V=nil),与i(T=*int, V=nil)不相等,因此 i != nil
。因此 V 为 nil ,但 T 不为 nil 的接口不等于 nil。
# Go函数返回局部变量的指针是否安全
在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上
# 什么是零值
go语言中的零值是变量没有做初始化时系统默认设置的值。
var b bool // bool型零值是false var s string // string的零值是"" var a *int var a []int var a map[string] int var a chan int var a func(string) int var a error // error是接口 // 以上六种类型零值常量都是nil
所有其他数值型的类型(包括complex64/128)零值都是0,可以用常量表达式代表数值0的任何形式表示出来。
对于以上各种类型都可以通过==条件判断是不是零值:
if <变量> == <零值表达式> {
}
2
但是类型不能混用,变量类型和零值类型必须匹配。
但是类型不能混用,变量类型和零值类型必须匹配。
结构也有零值。如果所有(递归的)字段都是零值,那么整个结构就是零值。但是没有零值常量用来表示某个结构的零值,所以也就无法用判断语句来识别一个结构是否处于零值。而且零值状态的结构也没有一个通用的语义,处于零值状态的结构可能意味着没有初始化,也可能是一个正常有用的状态。比如sync.Mutex零值状态就是处于没有锁住状态,是有意义的。所以不需要结构的零值常量
数组和结构类似,有零值,但是没有相应的零值常量。
string的零值是"",也可以用len(x)==0 来判断零值字符串。但是用""更好一点,把len(x)==0留给slice用。
go语言中的零值 - Go语言中文网 - Golang中文社区 (studygolang.com) (opens new window)
# 关键字相关
# Go的Struct能不能比较
- 相同struct类型的可以比较
- 不同struct类型的不可以比较,编译都不过,类型不匹配
# select可以用于什么
Golang 的 select 机制可以理解为是在语言层面实现了和 select, poll, epoll 相似的功能:监听多个描述符的读/写等事件,一旦某个描述符就绪(一般是读或者写事件发生了),就能够将发生的事件通知给关心的应用程序去处理该事件。 golang 的 select 机制是,监听多个channel,每一个 case 是一个事件,可以是读事件也可以是写事件,随机选择一个执行,可以设置default,它的作用是:当监听的多个事件都阻塞住会执行default的逻辑。
goroutine作为Golang并发的核心,我们不仅要关注它们的创建和管理,当然还要关注如何合理的退出这些协程,不(合理)退出不然可能会造成阻塞、panic、程序行为异常、数据结果不正确等问题。goroutine在退出方面,不像线程和进程,不能通过某种手段强制关闭它们,只能等待goroutine主动退出。
# defer相关
a := 1
defer fmt.Println("the value of a1:",a)
defer func() {
fmt.Println("the value of a2:",a)
}()
a++
/**
the value of a2: 2
the value of a1: 1
**/
2
3
4
5
6
7
8
9
10
11
第一个情况:defer延迟函数调用的fmt.Println(a)函数的参数值在defer语句出现时就已经确定了,所以无论后面如何修改a变量都不会影响延迟函数。 所以打印结果为1
第二个情况:defer延迟函数调用的函数参数的值在defer定义时候就确定了,而defer延迟函数内部所使用的值需要在这个函数运行时候才确定。
# 内存管理相关
# Golang的内存模型中为什么小对象多了会造成GC压力
go里面的垃圾回收用的是三色法,通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配.
# GC的触发条件
Go中对 GC 的触发时机存在两种形式:
- 主动触发(手动触发),通过调用
runtime.GC
来触发GC
,此调用阻塞式地等待当前GC
运行完毕. - 被动触发,分为两种方式: a. 使用系统监控,当超过两分钟没有产生任何
GC
时,强制触发GC
. b. 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,当前内存分配达到一定比例则触发.
# GPM调度
参考: 并发编程 | 面试问题浓缩总结 (xiaoyou66.com) (opens new window)
# GPM调度模型中P里面g0的作用
在Go中 g0作为一个特殊的goroutine,为 scheduler 执行调度循环提供了场地(栈)。对于一个线程来说,g0 总是它第一个创建的 goroutine。
之后,它会不断地寻找其他普通的 goroutine 来执行,直到进程退出。
当需要执行一些任务,且不想扩栈时,就可以用到 g0 了,因为 g0 的栈比较大。
g0 其他的一些“职责”有:创建 goroutine
、deferproc
函数里新建 _defer
、垃圾回收相关的工作(例如 stw、扫描 goroutine 的执行栈、一些标识清扫的工作、栈增长)等等。
# GO如何打印堆栈
有两个方法,第一个是runtime.Stack库
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(stack())
}
func stack() string {
var buf [2 << 10]byte
return string(buf[:runtime.Stack(buf[:], true)])
}
/**
qxcs-MacBook-Pro% go run test.go
goroutine 1 [running]:
main.stack(0x0, 0xc42003bf68)
/Users/qxc/work/test/test.go:14 +0x5b
main.main()
/Users/qxc/work/test/test.go:9 +0x26
**/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
第二个是runtime/debug 库
package main
import (
"fmt"
"runtime/debug"
)
func test1() {
test2()
}
func test2() {
test3()
}
func test3() {
fmt.Printf("%s", debug.Stack())
debug.PrintStack()
}
func main() {
test1()
}
/**
$ go run test_stacktrace.go
goroutine 1 [running]:
runtime/debug.Stack(0x0, 0x0, 0x0)
/usr/lib/golang/src/runtime/debug/stack.go:24 +0x80
main.test3()
/tmp/test_stacktrace.go:17 +0x24
main.test2()
/tmp/test_stacktrace.go:13 +0x14
main.test1()
/tmp/test_stacktrace.go:9 +0x14
main.main()
/tmp/test_stacktrace.go:22 +0x14
goroutine 1 [running]:
runtime/debug.Stack(0x0, 0x0, 0x0)
/usr/lib/golang/src/runtime/debug/stack.go:24 +0x80
runtime/debug.PrintStack()
/usr/lib/golang/src/runtime/debug/stack.go:16 +0x18
main.test3()
/tmp/test_stacktrace.go:18 +0x101
main.test2()
/tmp/test_stacktrace.go:13 +0x14
main.test1()
/tmp/test_stacktrace.go:9 +0x14
main.main()
/tmp/test_stacktrace.go:22 +0x14
**/
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
参考:
Go语言打印调用堆栈 - 简书 (jianshu.com) (opens new window)
golang中如何打印堆栈信息 - Go语言中文网 - Golang中文社区 (studygolang.com) (opens new window)
# 并发相关(重点)
# 协程、进程、线程的区别
- 进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
- 线程
线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
- 协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
# Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量
Golang中Goroutine 可以通过 Channel 进行安全读写共享变量,还可以通过原子性操作进行.
# 无缓冲Chan的发送和接收是否同步
ch := make(chan int) 无缓冲的channel由于没有缓冲发送和接收需要同步.
ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步.
2
- channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
- channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
# Golang中常用的并发模型
- 通过channel来进行并发控制
- 通过sync包中的WaitGroup实现并发控制
- 使用context来实现并发控制
# 数据竞争问题如何解决
- 使用互斥锁sync.Mutex
- 通过CAS无锁并发
CAS参考高并发无锁实现CAS原理 - 简书 (jianshu.com) (opens new window)
就是不使用锁来控制并发,CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。不过也有有问题
# 怎么查看Goroutine的数量
在Golang中,GOMAXPROCS
中控制的是未被阻塞的所有Goroutine,可以被 Multiplex
到多少个线程上运行,通过GOMAXPROCS
可以查看Goroutine的数量。
# Go中的锁有哪些
主要包括下面三大类,互斥锁,读写锁,sync.Map
的安全的锁
互斥锁和读写锁参考:并发编程 | 面试问题浓缩总结 (xiaoyou66.com) (opens new window)
这里我主要讲一下sync.Map
,使用load获取元素,store存储元素,delete删除元素,参考下面这个:
package main
import (
"sync"
"fmt"
)
func main() {
//开箱即用
var sm sync.Map
//store 方法,添加元素
sm.Store(1,"a")
//Load 方法,获得value
if v,ok:=sm.Load(1);ok{
fmt.Println(v)
}
//LoadOrStore方法,获取或者保存
//参数是一对key:value,如果该key存在且没有被标记删除则返回原先的value(不更新)和true;不存在则store,返回该value 和false
if vv,ok:=sm.LoadOrStore(1,"c");ok{
fmt.Println(vv)
}
if vv,ok:=sm.LoadOrStore(2,"c");!ok{
fmt.Println(vv)
}
//遍历该map,参数是个函数,该函数参的两个参数是遍历获得的key和value,返回一个bool值,当返回false时,遍历立刻结束。
sm.Range(func(k,v interface{})bool{
fmt.Print(k)
fmt.Print(":")
fmt.Print(v)
fmt.Println()
return true
})
}
/**
a
a
c
1:a
2:c
**/
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
# Channel是同步的还是异步的
Channel是异步进行的
# Goroutine和线程的区别
1.从调度上看,goroutine的调度开销远远小于线程调度开销。
OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中,查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。
Go运行的时候包含一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。
2.从栈空间上,goroutine的栈空间更加动态灵活。
每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小,如果对于goroutine来说,可能是一种巨大的浪费。作为对比goroutine在生命周期开始只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。
3.goroutine没有一个特定的标识。
在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。
goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还取决于运行它的线程标识。
# goroutine的优雅退出方法
# 使用for-range退出
range能够感知channel的关闭,当channel被发送数据的协程关闭时,range就会结束,接着退出for循环。
func main() {
in:=make(chan int,10)
in<-1
in<-2
in<-3
in<-4
go func(in <-chan int) {
// Using for-range to exit goroutine
// range has the ability to detect the close/end of a channel
for x := range in {
fmt.Printf("Process %d\n", x)
}
}(in)
time.Sleep(time.Second)
}
/*
Process 1
Process 2
Process 3
Process 4
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用select case ,ok退出
go func() {
// in for-select using ok to exit goroutine
for {
select {
case x, ok := <-in:
if !ok {
return
}
fmt.Printf("Process %d\n", x)
processedCnt++
case <-t.C:
fmt.Printf("Working, processedCnt = %d\n", processedCnt)
}
}
}()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用退出通道退出
使用一个专门的管道来退出
func worker(stopCh <-chan struct{}) {
go func() {
defer fmt.Println("worker exit")
// Using stop channel explicit exit
for {
select {
case <-stopCh:
fmt.Println("Recv stop signal")
return
case <-t.C:
fmt.Println("Working .")
}
}
}()
return
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# go协程有问题怎么查找
可以使用pprof来进行检测,输入 go tool pprof -http=:8001 http://127.0.0.1:7899/debug/pprof/goroutine\?debug\=1
在本地8081端口启动一个HTTP可视化工具。
参考:
Golang 如何排查协程泄漏问题 - 简书 (jianshu.com) (opens new window)
golang 定位和优化GC问题 case(一) - ssdut_buster的个人空间 - OSCHINA - 中文开源技术交流社区 (opens new window)
# 标准库相关
# JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗
如果是nilslice,其实会解析为null,空slice会解析为[]
首先Go的JSON 标准库对 nil slice
和 空 slice
的处理是不一致.
通常错误的用法,会报数组越界的错误,因为只是声明了slice,却没有给实例化的对象。
var slice []int
slice[1] = 0
2
此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下:
slice := make([]int,0)
slice := []int{}
2
当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。总之,nil slice
和 empty slice
是不同的东西,需要我们加以区分的.