一、基础技能
什么是协程(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()函数的区别用法是什么?
都是把格式好的字符串输出,只是输出的目标不一样:
-
Printf()
是把格式字符串输出到标准输出。 -
Sprintf()
是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。那就是目标字符串地址。 -
Fprintf()
是把格式字符串输出到指定文件设备中,所以参数笔printf多一个文件指针FILE*。主要用于文件操作。Fprintf()是格式化输出到一个stream,通常是到文件。
new() 与 make() 的区别
new(T)
和 make(T, args)
是Go语言内建函数,用来分配内存,但适用的类型不同。
-
new(T)
会为T
类型的新值分配已置零的内存空间,并返回地址(指针*T
),适用于值类型,如数组 、 结构体等。 -
make(T, args)
返回初始化之后的T类型的值,不是指针*T
,是经过初始化之后的T的引用。 只适用于slice
、map
和channel
。
init() 函数是什么时候执行的?
init() 函数是 Go 程序初始化的一部分。Go 程序初始化先于 main 函数,由 runtime 初始化每个导入的包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。
每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init() 函数。同一个包,甚至是同一个源文件可以有多个 init() 函数。init() 函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init() 函数的执行顺序不作保证。
一句话总结: import –> const –> var –> init() –> main()
无缓冲的 channel 和 有缓冲的 channel 的区别?
-
对于无缓冲的 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 的底层结构。
unsafe.Pointer(&a)
方法可以得到变量a的地址。(*reflect.StringHeader)(unsafe.Pointer(&a))
可以把字符串a转成底层结构的形式。(*[]byte)(unsafe.Pointer(&ssh))
可以把ssh底层结构体转成byte的切片的指针。- 再通过
*
转为指针指向的实际内容。
对已经关闭的的 chan 进行读写,会怎么样?为什么?
读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
-
如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,第二个返回值为 true。
-
如果 chan 关闭前,buffer 内有元素已经被读完,会直接返回 channel 元素的零值,第二个返回值为 false。
写已经关闭的 chan 会 panic
什么是协程泄露(Goroutine Leak)?
协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。 常见的导致协程泄露的场景有以下几种:
- 缺少接收器,导致发送阻塞。
- 缺少发送器,导致接收阻塞。
- 死锁(dead lock) 两个或两个以上的协程在执行过程中,由于竞争资源或者由于彼此通信而造成阻塞,这种情况下,也会导致协程被阻塞,不能退出。
- 无限循环(infinite loops) 当协程中有无限循环,导致进程无法正常退出时,会导致协程泄露。
简述一下 golang 的协程调度原理?
M(machine): 关联了一个内核线程。
P(processor): 代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。
G(goroutine): 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。
知道 golang 的内存逃逸吗?什么情况下会发生内存逃逸?
golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 “逃逸” 了,必须在堆上分配。
能引起变量逃逸到堆上的典型情况:
-
在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
-
发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个
goroutine
会在channel
上接收数据。所以编译器没法知道变量什么时候才会被释放。 -
在一个切片上存储指针或带指针的值。 一个典型的例子就是
[]*string
。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。 -
slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
-
在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的, 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。
简述 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, 并发)