Goalng 面试题记录

sunls24 于 2021-04-08 发布

一、基础技能

什么是协程(Goroutine)

Goroutine 是与其他函数或方法同时运行的函数或方法。 Goroutines 可以被认为是轻量级的线程。 与线程相比,创建 Goroutine 的开销很小。 Go应用程序同时运行数千个 Goroutine 是非常常见的做法。

如何高效地拼接字符串

Go 语言中,字符串是只读的,也就意味着每次修改操作都会创建一个新的字符串。如果需要拼接多次,应使用 strings.Builder,最小化内存拷贝次数。

defer 的执行顺序

多个 defer 语句,遵从后进先出 (Last In First Out,LIFO) 的原则,最后声明的 defer 语句,最先得到执行。 defer 在 return 语句之后执行,但在函数退出之前,defer 可以修改返回值。

说说 go 语言中,数组与切片的区别?

Go 语言 tag 的用处?

tag 可以理解为 struct 字段的注解,可以用来定义字段的一个或多个属性。框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。tag 丰富了代码的语义,增强了灵活性。

go语言中,Printf()、Sprintf()、Fprintf()函数的区别用法是什么?

都是把格式好的字符串输出,只是输出的目标不一样:

new() 与 make() 的区别

new(T) 和 make(T, args)  是Go语言内建函数,用来分配内存,但适用的类型不同。

init() 函数是什么时候执行的?

init() 函数是 Go 程序初始化的一部分。Go 程序初始化先于 main 函数,由 runtime 初始化每个导入的包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。

每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init() 函数。同一个包,甚至是同一个源文件可以有多个 init() 函数。init() 函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init() 函数的执行顺序不作保证。

一句话总结: import –> const –> var –> init() –> main()

无缓冲的 channel 和 有缓冲的 channel 的区别?

map 可以边遍历边删除吗

map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。

上面说的是发生在多个协程同时读写同一个 map 的情况下。 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。但是,遍历的结果就可能不会是相同的了,有可能结果遍历结果集中包含了删除的 key,也有可能不包含,这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。

一般而言,这可以通过读写锁来解决:sync.RWMutex。 读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。 另外,sync.Map 是线程安全的 map,也可以使用。

如何比较两个 map 相等

map 深度相等的条件:

1、都为 nil
2、非空、长度相等,指向同一个 map 实体对象
3、相应的 key 指向的 value “深度”相等

直接将使用 map1 == map2 是错误的。这种写法只能比较 map 是否为 nil。 因此只能是遍历map 的每个元素,比较元素是否都是深度相等。

二、 进阶

字符串转成byte数组,会发生内存拷贝吗?

字符串转成切片,会产生拷贝。严格来说,只要是发生类型强转都会发生内存拷贝。

不产生内存拷贝的方法,在底层转换类型,可使用 unsafe 包:

func main() {
	a :="aaa"
	ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a))
	b := *(*[]byte)(unsafe.Pointer(&ssh))
	fmt.Printf("%v",b)
}

StringHeader 是字符串在 go 的底层结构。 SliceHeader 是切片在 go 的底层结构。

  1. unsafe.Pointer(&a)方法可以得到变量a的地址。
  2. (*reflect.StringHeader)(unsafe.Pointer(&a)) 可以把字符串a转成底层结构的形式。
  3. (*[]byte)(unsafe.Pointer(&ssh)) 可以把ssh底层结构体转成byte的切片的指针。
  4. 再通过*转为指针指向的实际内容。

对已经关闭的的 chan 进行读写,会怎么样?为什么?

读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。

写已经关闭的 chan 会 panic

什么是协程泄露(Goroutine Leak)?

协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。 常见的导致协程泄露的场景有以下几种:  

简述一下 golang 的协程调度原理?

M(machine): 关联了一个内核线程。
P(processor): 代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。
G(goroutine): 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。

知道 golang 的内存逃逸吗?什么情况下会发生内存逃逸?

golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 “逃逸” 了,必须在堆上分配。

能引起变量逃逸到堆上的典型情况:

简述 Go 语言GC(垃圾回收)的工作原理

最常见的垃圾回收算法有标记清除(Mark-Sweep) 和引用计数(Reference Count),Go 语言采用的是标记清除算法。并在此基础上使用了三色标记法和写屏障技术,提高了效率。

标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段: 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象; 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表。

标记清除算法的一大问题是在标记期间,需要暂停程序(Stop the world,STW),标记结束之后,用户程序才可以继续执行。为了能够异步执行,减少 STW 的时间,Go 语言采用了三色标记法。

一次完整的 GC 分为四个阶段: 1)标记准备(Mark Setup,需 STW),打开写屏障(Write Barrier) 2)使用三色标记法标记(Marking, 并发) 3)标记结束(Mark Termination,需 STW),关闭写屏障。 4)清理(Sweeping, 并发)

三、链接

https://github.com/lifei6671/interview-go

https://github.com/golang-design/Go-Questions