= 和 := 有什么区别?
= 用于给变量或常量赋值,而 := 用于定义并给变量赋值。
new 和 make 的区别是什么?
- new 和 make 都用来分配内存,但 make 即分配内存也初始化内存,new 只将内存清 0,并不初始化内存;
- make 返回的是引用类型本身,而 new 返回的是指向类型的指针;
- make 只能用于分配并初始化 slice、map、chan 类型的数据,而 slice 可以给任何类型的数据分配内存。
Go 面向对象是如何实现的?
面向对象即封装、继承、多态。
封装:对于同一个包,对象等对包内可见,而要对包外可见则需要将对象以大写开头;
继承:Go 以嵌套来实现继承,也就是在 struct 中加入需要继承的类;
多态:Go 的多态通过接口来实现,某个类型的实例可以赋给它所实现的任意接口类型的变量。
二维切片如何初始化?
先用 make 初始化一个二维数组,再遍历这个二维数组使用 make 初始化里面的一维数组。
uint 类型变量分别为 1,2,它们相减的结果是多少?
由于 uint 类型不能为复制,所以会溢出,在 32 位系统上结果为 2^32-1,在 64 位系统上为 2^64-1。
go 有没有函数在 main 函数之前执行?怎么用?
init 函数会在 main 函数之前执行。
init 函数一个包中可以有多个,一个文件中也可以有多个。
在同一个包中执行顺序并不确定,不应该依赖 init 函数的执行顺序。
在不同包中,按照依赖关系来决定执行顺序。
init 函数在 runtime 导入包后,并初始化全局常量和全局变量之后,在 main 函数之前执行。
如何知道一个对象是分配在栈上还是堆上?
即是否会发生逃逸。如果一个对象的大小无法在分配的时候确定,或它的作用域在函数结束了还没结束,那么它会被分配到堆上,否则分配到站上。
有什么有时候要定义一个空值?
1 | var _ Codec = (*GobCodec)(nil) |
将 nil 转化成一个具体类型,再转化为一个接口类型,如果失败了说明该具体类型没有实现该接口类型的所有方法。
什么是 rune 类型?
Go 中的 rune 其实就是 int32 的别名,用于表示 Unicode 中的字符。但是 Go 的字符串底层表示是字节序列,而不是 rune 类型。
如何判断 map 中是否包含某个 key?
map 会返回两个参数,第二个参数就是这个 key 是否存在。
什么是 Go 协程(Goroutine)?
协程即用户态轻量级协程,是线程的基本调度单位,在函数前加上 go 关键字就能实现并发。一个 go 协程的大小很小,因此可以轻易实现成千上万个协程同时启动。
如何高效拼接字符串?
- “+” 会对字符进行遍历,并开辟新的空间;
- fmt.Sprintf 额外存在反射的性能损耗;
- strings.Builder 底层使用 unsafe 直接把字节数组强制转为 string,避免了拷贝和分配新空间的开销;
- bytes.Buffer 和 strings.Builder 类似,但是在将字节数组转化为 string 时使用了标准类型,多了内存分配的开销;
- strings.Join 是基于 strings.Builder 实现的,但在内部调用了 b.Grow(n),提前进行了分配,避免了后续扩容的开销。
得出结论:strings.Join ≈ strings.Builder > bytes.Buffer > + > fmt.Sprintf。
defer 的执行顺序
defer 按照声明的顺序逆序执行,因为 defer 是注册在 Goroutine 上的结构体,以头插法维护一个链表,所以顺序遍历这个 defer 链表时自然就和插入顺序相反。
Go 有异常类型吗?
没有,只有错误类型 Error。
Go 允许多个返回值吗?
允许。
如何判断两个字符串切片是相等的?
使用反射包中的 reflect.DeepEqual(a, b) 来判断,但是性能非常差。
推荐使用遍历切片中的元素来比较。
打印字符串时,%v 和 %+v 的区别?
%v 打印 struct 的值,而 %+v 还会打印出 struct 字段的名称。
Go 语言如何表示枚举?
iota。
空结构体的作用是什么?
Go 语言对空结构体做了特别的优化,使得它完全不占用内存,可以用于作占位符。
比如可以用空结构体实现 Set 数据结构,再或者使用空结构体类型的 chan,来节省空间,也可以用于申明仅包含方法的结构体。
Go 语言中 int 和 int32 是一个概念吗?
不是,跟操作系统是多少位有关。
Go 语言中 interface 可以比较吗?
可以。interface 内部实现中包含了 2 个字段,类型 T 和值 V,可以直接使用 == 或 != 比较。interface 相等有两种情况:
- 都为 nil;
- T 和 V 都相等。
Go 语言中 nil 可以比较吗?
可以,只有同样类型的 nil 才相等。
Go 语言函数返回局部变量的指针是否安全?
安全,因为 Go 会进行逃逸分析,如果局部变量的指针被返回,即作用域超出函数,则会把它分配到堆上。
非接口的任意类型 T 都能够调用 *T 的方法吗?反过来呢?
一个 T 类型的值想要调用 *T 类型声明的方法,当且仅当 T 是可寻址的。而由于 *T 是可以解引用的,所以它可以调用 T 类型声明的方法。
Go slice 是如何扩容的?
在 go 1.18 之前当 slice 的容量小于 1024 的时候,成倍增长,而当 slice 容量大于 1024 的时候以 1.25 的倍率增长。
但是在 go 1.18 时,这个阈值变成了 256。扩容的逻辑也有所变化:
- 如果期望的容量比原容量的两倍还大,那么新容量直接等于期望的容量;
- 否则如果原容量小于 256,那么新容量就等于两倍的原容量;
- 如果原来大于等于 256,那么循环增加 1.25 倍加上 192,直到满足新容量。
接着会通过 growslice 函数传入的切片的类型元数据来进行内存对齐。
无缓冲的 chan 和有缓冲的 chan 有什么区别?
是否有缓冲区决定了是否会阻塞。
为什么有协程泄露?
可能有以下几种原因:
- goroutine 被 chan 阻塞,无法退出;
- goroutine 进入死循环。
要避免协程泄露,就要在创建协程的时候就考虑如何终止协程,可以为协程添加一个 for select 来通知协程退出。可以添加一个空结构体类型的 chan 来发送通知,也可以直接关闭发送消息的 chan。
常见的 Goroutine 操作函数有哪些?
- runtime.GOMAXPROCS(num int):可以用于设置线程数目,默认值为 CPU 逻辑核数,如果设置的过大会引起频繁的性能切换,降低性能;
- runtime.Gosched():让当前 goroutine 让出 CPU 时间片;
- runtime.Goexit():让当前 goroutine 运行终止,并且会执行 defer,在主协程中调用该函数会引起 panic。
什么是 Go 竞态?
所谓的竞态(race condition)就是多个 goroutine 同时访问相同的资源,我们可以对临界区加锁或使用原子操作来解决竞态,原子操作的开销比加锁要小。
defer 可以捕获 goroutine 的子 goroutine 吗?
不可以,defer 仅在当前的 goroutine 中生效,不会跨 goroutine 影响其它 goroutine 的执行。
如果若干个goroutine,有一个panic会怎么做?
一个 goroutine 发生了 panic 只会终止自己,不会影响其它 goroutine,但如果该发生 panic 的协程没有 recover,整个程序都会退出,所以子 goroutine 中最好使用 recover 来处理 panic。
gRPC 是什么?
gRPC 并不是指的 golang rpc,而是 google rpc,是谷歌开源的强大的远程调用库。rpc 也就是远程过程调用,可以调用远程函数就像调用本地函数一样。
grpc 使用 protobuf 作为接口定义语言,同时底层的消息交换格式也是 protobuf。
grpc 通信基本流程:
- 先定义 IDL,即后缀为 .proto 的接口文档;
- 编译 proto 文件,得到存根文件;
- 服务端定义接口并启动,这些接口定义在存根文件里;
- 客户端借助存根文件调用服务端的函数。
grpc 是跨平台跨语言的。
Go 是如何解析 Tag 的?
要采用反射的方式来解析 Tag,通过 reflect.ValueOf 方法获取反射值,获取 Type 属性,再用 Field(i) 获取需要解析 tag 的 field,通过.Tag() 来获取 tag。
Go 项目如何优雅的启停?
所谓优雅的启停要符合以下几个条件:
- 不可以关闭现有连接;
- 启动新的进程来接管旧进程;
- 继续响应用户请求,不能出现拒绝请求的情况;
- 旧进程处理完旧请求之后再退出,新的请求由新的进程来处理。
通过监听信号来实现:
启动:
- 监听 SIGHUP 信号;
- 收到信号后将服务监听的文件描述符传递给新的子进程,新老进程同时接受请求。
退出:
- 监听 SIGINT,SIGSTP,SIGQUIT 等信号;
- 接收到信号后,父进程停止接收新请求,等待旧请求完成或超时;
- 父进程退出。
channel 死锁的场景
- channel 中没有数据,直接读取会死锁。通过用 select 语句,加上 default 来处理没有数据的情况;
- channel 缓冲区已满,再次写入会死锁。通过 select 语句,加上 default 来处理缓冲区满的情况;
- 向一个已经关闭的 channel 中写数据会造成 panic,但如果 channel 里还有未读取的数据是可以正常读取的,第二个返回值为 true,没数据了会返回空值,第二个返回值为 false。
atomic 底层是怎么实现的?
atomic 采用 CAS 指令实现,CAS 指令是 CPU 提供的原子性操作指令。CAS 操作不需要加锁,总是假设被操作的值未曾改变,一旦这个假设成立就立即进行替换,即 Compare and Swap。CAS 需要不断占用 CPU 资源来避免加锁的开销。
Go 的调试/分析工具用过哪些?
- go cover:测试代码覆盖率;
- godoc:用于生成 go 文档;
- pprof:用于性能调优,针对 cpu、内存和并发;
- -race:用于检测竞态;
进程被 kill,如何保证所有 goroutine 顺利退出?
goroutine 需要监听 SIGKILL 信号,接收到该信号之后就退出,可以用 select 语句。
说说 context 包的作用?
context 用于在 goroutine 之间传递上下文,可以同步特定的数据、取消信号以及处理请求的截止时间等。