记录Golang面试八股文
Go基础
init和main函数的相关特点
init函数(没有输入参数、返回值)的主要作用:
- 初始化不能采用初始化表达式初始化的变量;
- 在程序运行前注册;
- 实现
sync.Once
功能; - 其他
init顺序
- 在同一个
package
中, 可以多个文件中定义init
方法; - 在同一个
go
文件中, 可以重复定义init
方法; - 在同一个
package
中, 不同文件中的init
方法执行按照文件名先后执行各个文件中的init
方法; - 在同一个文件中的多个
init
方法, 按照在代码中编写的顺序依次执行不同的init
方法; - 对于不同的
package
, 如果不相互依赖的话, 按照main
包中import
的顺序调用其包中的init()
函数; - 如果
package
存在依赖, 调用顺序为最后被依赖的最先被初始化, 例如: 导入顺序main -> A -> B -> C, 则初始化顺序为C -> B -> A -> main, 一次执行对应的init
方法;
Golang的数据结构的零值是什么?
所有整型类型: 0
浮点类型: 0.0
布尔类型: false
字符串类型: “”
指针、interface、切片(slice)、channel、map、function: nil
Go的零值初始是递归的, 即数组、结构体等类型的零值初始化就是对其组成元素逐一进行零值初始化
byte和rune有什么区别
rune
和byte
在Go语言中都是字符类型, 且都是别名类型;byte
型本质上是uint8
类型的别名, 代表了ASCII码的一个字符;rune
型本质上是int32
型的别名, 代表一个UTF-8字符;
Go struct 能不能比较
需要根据具体情况分析, 如果struct中含有不能被比较的字段类型, 就不能被比较;
如果struct中所有的字段类型都支持比较, 那么就支持比较;
不能被比较的类型
- slice, 因为slice是引用类型, 除非是和nil比较;
- map, 和slice同理, 如果要比较两个map只能通过循环遍历实现;
- 函数类型;
注意
- 结构体之间只能比较它们是否相等, 而不能比较它们的大小;
- 只能所有属性相等而且属性顺序一致的结构体才能进行比较;
Go import的三种方式
加下划线
import下划线(如:_ “github.com/xxx/xxx”)的作用: 当导入一个包时, 该包下的文件里所有的init()
函数都会被执行。然而有些时候我们并不需要把整个包都导入进来, 仅仅是希望它执行init()
函数而已。这个时候就可以使用import _
引用该包。
即: 使用[import _ 包路径]只是引用该包, 仅仅是为了调用init函数
, 所以无法通过包名来调用包中的其他函数;
加点(.)
import和引用的包名之间加点(.)操作的含义就是这个包导入之后在调用这个包的函数时, 可以省略前戳的包名;
别名
别名操作可以把包命名成另一个用起来容易记忆的名字;
Golang的常量地址
1 | const i = 100 |
string和[]byte如何取舍
string擅长的场景:
- 需要字符串比较的场景;
- 不需要nil字符串的场景;
[]byte擅长的场景
- 修改字符串的场景, 尤其是修改粒度为1个字节;
- 函数返回值, 需要用nil表示含义的场景;
- 需要切片操作的场景;
字符串转成byte数组, 会发生内存拷贝吗
字符串转成切片, 会产生拷贝。严格来说, 只要是发生类型强化转都会发生内存拷贝。频繁的内存拷贝操作听起来对性能不太友好。有没有什么办法可以在字符串转成切片的时候不用发生拷贝呢?
1 | package main |
解释
StringHeader
是字符串
在go的底层结构1
2
3
4type StringHeader struct {
Data uintptr
Len int
}SliceHeader
是切片
在go的底层结构1
2
3
4
5type SliceHeader struct {
Data uintptr
Len int
Cap int
}那么如果想要在底层转换二者, 只需要把
StringHeader
的地址强转成SliceHeader
就行。那么go有个包很强的包叫unsafe
。- unsafe.Pointer(&a)方法可以得到变量
a
的地址; - (*reflect.StringHeader)(unsafe.Pointer(&a))可以把字符串a转成底层结构的形式;
- (*[]byte)(unsafe.Pointer(&ssh))可以把ssh底层结构转成byte的切片的指针;
- 再通过
*
转成指针指向的实际内容;
- unsafe.Pointer(&a)方法可以得到变量
翻转含有中文、数字、英文字符的字符串
翻转含有
中文、数字、英文字母
的字符串
1 | package main |
解释
rune
关键字, 从golang源码中看出, 它是int32的别名(-2^31 ~ 2^31-1), 比如byte(-128 ~ 127), 可表示更多的字符。- 由于rune可表示的范围更大, 所以能处理一切字符, 当然也包括中文字符。在平时计算中文字符, 可用rune。
- 因此将
字符串
转成rune的切片
, 再进行翻转;
json包变量不加tag会怎么样?
json
包里使用的时候, 结构体里的变量不加tag
能不能正常转成json
里的字段?
回答
- 如果变量
首字母小写
, 则为private
。无论如何不能转
, 因为取不到反射信息
。 - 如果变量
首字母大写
, 则为public
:不加tag
, 可以正常转为json
里的字段,json
内字段名跟结构体内字段原名一致
;加了tag
, 从struct
转json
的时候,json
的字段名就是tag
里的字段名, 原字段名已经没用;
代码示例
1 | package main |
解释
- 结构体里定义了四个字段, 分别对应
小写无tag
,小写加tag
、大写无tag
、大写加tag
; - 转为
json
后首字母小写的
不管加不加tag都不能
转为json
里的内容, 而大写的
加了tag可以取别名
, 不加tag
则json
内的字段跟结构体字段原名一致
;
Go语言中cap函数可以作用于哪些内容?
- array返回数组的元素个数
- slice返回slice的最大容量
- channel返回chennel的容量
Go语言的引用类型有什么?
Go语言中的引用类型有func(函数类型)、interface(接口类型)、slice(切片类型)、map(字典类型)、channel(管道类型)、*(指针类型)
for-select, 如果通道已经关闭会怎么样?如果select中只有一个case呢?
for循环select时, 如果通道已经关闭会怎么样?如果select中的case只有一个, 又会怎么样?
回答
- for循环select时, 如果其中一个case通道已经关闭, 则每次都会执行到这个case;
- 如果select里边只有一个case, 而这个case被关闭了, 则会出现死循环;
解释
- for循环里被关闭的通道输出结果
1
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
29package main
import (
"fmt"
"time"
)
const (
fmat = "2006-01-02 15:04:05"
)
func main() {
c := make(chan int)
go func() {
time.Sleep(1 * time.Second)
c <- 10
close(c)
}()
for {
select {
case x, ok := <-c:
fmt.Printf("%v, 通道读取到: x=%v, ok=%v\n",time.Now().Format(fmat) x, ok)
time.Sleep(500 * time.Millisecond)
default:
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
162024-02-27 22:29:44, 没读到信息进入default
2024-02-27 22:29:44, 没读到信息进入default
2024-02-27 22:29:45, 通道读取到: x=10, ok=true
2024-02-27 22:29:45, 通道读取到: x=0, ok=false
2024-02-27 22:29:46, 通道读取到: x=0, ok=false
2024-02-27 22:29:46, 通道读取到: x=0, ok=false
2024-02-27 22:29:47, 通道读取到: x=0, ok=false
2024-02-27 22:29:47, 通道读取到: x=0, ok=false
2024-02-27 22:29:48, 通道读取到: x=0, ok=false
2024-02-27 22:29:48, 通道读取到: x=0, ok=false
2024-02-27 22:29:49, 通道读取到: x=0, ok=false
2024-02-27 22:29:49, 通道读取到: x=0, ok=false
2024-02-27 22:29:50, 通道读取到: x=0, ok=false
2024-02-27 22:29:50, 通道读取到: x=0, ok=false
2024-02-27 22:29:51, 通道读取到: x=0, ok=false
2024-02-27 22:29:51, 通道读取到: x=0, ok=false
c通道
是一个缓冲为0的通道, 在main
开始时, 启动一个协程对c通道
写入10, 然后就关闭掉这个通道;- 在
main
中通过通过x, ok := <-c
接受通道c
里的值, 从输出结果里看出, 确实从通道里读出了之前塞入通道的10, 但是这个通道关闭后, 这个通道一直能读出内容;
- 怎样才能不读关闭后的通道输出结果
1
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
33package main
import (
"fmt"
"time"
)
const (
fmat = "2006-01-02 15:04:05"
)
func main() {
c := make(chan int)
go func() {
time.Sleep(1 * time.Second)
c <- 10
close(c)
}()
for {
select {
case x, ok := <-c:
fmt.Printf("%v, 通道读取到: x=%v, ok=%v\n", time.Now().Format(fmat), x, ok)
time.Sleep(500 * time.Millisecond)
if !ok {
c = nil // 把关闭后的通道赋值为nil, 则select读取则会阻塞
}
default:
fmt.Printf("%v, 没读到信息进入default\n", time.Now().Format(fmat))
time.Sleep(500 * time.Millisecond)
}
}
}1
2
3
4
5
6
7
8
9
102024-02-27 23:08:06, 没读到信息进入default
2024-02-27 23:08:07, 没读到信息进入default
2024-02-27 23:08:07, 通道读取到: x=10, ok=true
2024-02-27 23:08:08, 通道读取到: x=0, ok=false
2024-02-27 23:08:08, 没读到信息进入default
2024-02-27 23:08:09, 没读到信息进入default
2024-02-27 23:08:09, 没读到信息进入default
2024-02-27 23:08:10, 没读到信息进入default
2024-02-27 23:08:10, 没读到信息进入default
2024-02-27 23:08:11, 没读到信息进入default
go的内存逃逸是什么? 什么情况下会发生内存逃逸?
回答
golang程序变量
会携带有一组校验数据, 用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验, 它就可以在栈上
分配, 否则就说它逃逸了, 必须在堆上分配。
能引起变量逃逸到堆上的典型情况:
- 在方法内把局部变量指针返回: 局部变量原本应该在栈中分配, 在栈中回收。但是由于返回时被外部调用, 因此其生命周期大于栈, 则溢出;
- 发送指针或带有指针的值到chennel中: 在编译时, 是没有办法知道哪个goroutinue会在channel上接收数据。所以编译器没法知道变量什么时候才会释放;
- 在一个切片上存储指针或带指针的值: 一个典型的例子就是[]*string。这会导致切片内容逃逸。尽管其后面的数组可能是在栈上分配的, 但其引用的值一定是在堆上;
- slice的背后数组被重新分配了, 因为append时可能会超出其容量(cap): slice初始化的地方在编译时是可以知道的, 它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充, 就会在堆上分配;
- 在interface类型上调用方法: 在interface类型上调用方法都是动态调度的–方法的真正实现只能在运行时知道。想象一个io.Reader类型的变量r, 调用r.Read(b)会使得r的值和切片b背后存储的数据逃逸掉, 所以会在堆上分配;
代码举例
通过一个例子加深理解, 接下来尝试怎么通过
go build -gcflags=-m
查看逃逸的情况
1 | package main |
执行go build -gcflags=-m test_Demo_01.go
1 | go build -gcflags=-m test_Demo_01.go |
./test_Demo_01.go:10:10: new(A) escapes to heap
说明new(A)
逃逸了, 符合上述提到的常见情况的第一种;./test_Demo_01.go:17:11: a.s + "world" does not escape
说明b
变量没有逃逸, 因为它只存在方法内存中, 会在方法结束时被回收;./test_Demo_01.go:18:9: b + "!" escapes to heap
说明c
变量逃逸, 通过fmt.Println(a ...interface{})
打印的变量, 都会发生逃逸;
Go关键字fallthrough有什么作用
fallthrough关键字只能用在switch中, 且只能在每一个case分支中的最后一行, 作用是如果这个case分支被执行, 将继续执行下一个case分支, 而且不会取判断下一个分支额case条件是否成立;
1 | package main |
空结构体占不占内存空间? 为什么使用空结构体?
空结构体是没有内存大小的结构体;
通过unsafe.Sizeof()可以查看空结构体的宽度, 代码如下:
1 | var s struct{} |
准确的来说, 空结构体有一个特殊起点: zerobase
变量; zerobase
是一个占用8个字节的uintptr
全局变量。每次定义struct{}
类型的变量, 编译器只是把zerobase
变量的地址给出去。也就是说空结构体的变量的内存地址都是一样的。
空结构体的使用场景主要有三种:
- 实现方法接收者: 在业务场景下, 我们需要将方法组合起来, 代表其一个”分组”的, 便于后续拓展和维护;
- 实现集合类型: 在Go语言的标准库中并没有提供集合(Set)的相关实现, 因此一般在代码中图方便, 会直接用map来替代:
type Set map[string]struct{}
。 - 实现通道: 在Go channel的使用场景中, 常常会遇到通知型channel, 其不需要发送任何数据, 只是用于协调Goroutinue的运行, 用于流转各类状态或是控制并发情况;
Go语言中, 下面哪个关于指针的说法是错误的?
- 指针不能进行算术运算
- 指针可以比较
- 指针可以是nil
- 指针可以指向任何类型
针对在Go语言中只能指向相同类型的结构体或者基本类型。例如,一个int类型的变量, 只能指向int类型的指针。如果尝试将一个不同类型的指针赋给一个变量, 将会导致编译错误。
Go语言的接口类型是如何实现的?
在Go语言中, 接口类型是通过**类型嵌入(embedding)**的方式实现的。每个实现了接口的类型的结构体中都有一个隐含的成员, 该成员是指向接口类型的指针。通过这种方式, 接口实现了堆类型的约束和定义。
具体来说, 当一个类型实现了某个接口的所有方法后, 该类型就被认为是实现了该接口。结构体中, 可以通过嵌入接口类型的方式来实现接口方法。在实现接口方法时, 方法的签名需要与接口定义中的方法签名保持一致。
Go string的底层实现
源码包src/runTime/string.go stringStruct定义了string的数据结构
1 | Type stringStruct struct { |
声明:
如下代码所示, 可以声明一个string变量赋予初值
1 | var str string |
字符串构建过程是根据字符串构建stringStruct, 再转化成string, 转化源码如下:
1 | func gostringnocopy(str *byte) string { |
Go如何避免panic
首先明确panic定义: go把真正的异常叫做panic, 是指出现重大错误, 比如数据越界之类的编程BUG或者是那些需要人工介入才能修复的问题, 比如程序启动时加载资源出错等等。
几个容易出现panic的点:
- 函数返回值或参数为指针类型, nil, 未初始化结构体, 此时调用容易出现panic, 可加 !=nil 进行判断;
- 数组切片越界
- 如果我们关闭未初始化通道, 重复关闭通道, 向已经关闭的通道中发送数据, 这三种情况会引发panic, 导致程序崩溃;
- 如果我们直接操作未初始化的映射(map), 也会引发panic, 导致程序崩溃;
- 另外, 操作映射可能会遇到的更为严重的一个问题是, 同时对同一个映射并发读写, 它会触发runtime.throw, 不像panic可以使用recover捕获。所以, 我们再对同一个映射并发读写时, 一定要使用锁;
- 如果类型断言使用不当, 比如我们不接受布尔值的话, 类型断言失败也会引发panic, 导致程序崩溃;
- 如果很多时候不可避免地出现了panic, 记得使用defer/recover;
空结构体的使用场景
空结构体(empty struct)是在Go语言中一个特殊地概念, 它没有任何字段。在Go中, 它通常被称为匿名结构体或零宽度结构体。尽管没有字段, 但它在某些请款下仍然有其他用途
1. 占位符
空结构体可以用作占位符, 用于表示某个数据结构或数据集合地存在而不实际存储任何数据。这在某些数据结构的实现中非常有用, 特别是要实现某种数据结构的集合或映射时, 但并不需要存储实际的值。
1 | // 表示集合中是否包含某个元素的映射 |
2. 信号量
空结构体可以用作信号量, 用于控制并发操作。通过向通道发送或接收空结构体, 可以实现信号的传递和同步;
1 | // 用通道作为信号量 |
3.强调结构
有时, 空结构体可用于强调某个结构的重要性或存在。它可以用作结构体的标签, 表示关注该结构的存在而不是其内容;
1 | // 表示一篇文章的元信息, 不包含实际内容 |
4. JSON序列化
在处理JSON数据时, 有时需要表示一个空对象, 可以使用空结构体来表示JSON中的空对象
{}
;
1 | emptyJSON := struct{}{} |
尽管空结构体没有字段, 但它在上述情况下提供了一种轻量级的方式来处理特定的需求, 而无需分配额外的内存或定义具体的数据结构。
struct的特点:
- 用来自定义复杂数据结构;
- struct里面可以包含多个字段(属性);
- struct类型可以定义方法, 注意和函数的区分;
- struct类型是值类型;
- struct类型可以嵌套;
- Go语言没有calss类型, 只有struct类型;
特殊之处:
- 结构体是用户单独定义的类型, 不能和其他类型进行强制转换;
- golang中的struct没有构造函数, 一般可以使用工厂模式来解决这个问题;
- 我们可以为struct的每个字段, 写上一个tag。这个tag可以通过反射的机制获取到, 最常用的场景就是json序列化和反序列化;
- 结构体中的字段可以没有名字, 即匿名字段;