简介
总结一些笔试题
源码地址
https://gitee.com/secondtonone1/go-interview-questions
面试题
1 以下定义包内全局变量,正确的是
1 | A. var str string |
答案是AD,定义全局变量要有var 声明,可以写类型也可以不写,系统自动推导,:=实际上是两个表达式不能定义包内全局变量,会报错。
2 指针访问
通过指针变量 p 访问其成员变量 name
1 | p.name 或者(*p).name 我们能常用p.name即可 |
3 接口
一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口,实现类的时候,只需要关心自己应该提供哪些方法,不用再纠结接口需要拆得多细才合理,接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口,类实现接口时,不需要导入接口所在的包,因为go的interface体系降低了接口实现的耦合性。
4 协程
协程和线程都可以实现程序的并发执行,协程比线程更轻量级,协程和线程一样会存在死锁问题,协程可以通过channel进行通信
5 init函数
一个包中可以包含多个init函数,程序编译时,先导入包的init函数,再执行本包内的init函数,main包中也可以有init函数,init函数不可以被其他函数调用,init函数的执行顺序是按照导入的顺序执行的。
6 for循环
go的for循环支持continue和break来控制循环,也提供了更高级的break,可以跳出到指定位置。for循环不支持以逗号为间隔的多个复制语句,必须使用平行赋值的方式来初始化多个变量。
7 变参函数
1 | func add(args ...int) int { |
对于add调用可以采取如下方式
1 | add(1, 2, 3) |
8 强制类型转换
go的强制类型转换比较简单
1 | type MyStr string |
9 const变量
const变量的定义,可以不写类型,交由系统自动推导。
1 | const pai float64 = 3.1415926 |
但是不可以将变量赋值给常量
1 | const newerr = errors.New("new error") |
10 bool变量
go的bool变量不支持和int做强制转换,也不支持将int类型赋值给bool变量,以下操作为错误的,编译会报错
1 | b := bool(1) |
11 switch语句
switch语句可以包含多个case,每个表达式可以是常量,整数,字符串等等,也可以是channel读取等。不同于C++,case中要明确添加fallthrough才能继续执行紧跟的下一个case
12 golang的this
golang中没有隐藏的this指针,方法施加的对象显示传递,没有被隐藏,可以叫this,that等等什么都可以。方法施加的对象不需要非得是指针,也不用非得叫this
13 golang的引用类型
golang的引用类型包括map,slice,chan,interface,凡是底层有指针实现的都是引用类型。
14 golang的指针
golang的指针,可以通过&取指针的地址,可以通过*取指针指向的数据
15 main函数
main函数不能有参数,不能有返回值,必须在main包,可以通过flag包来获取和解析命令行参数
16 关于nil
nil表示空,常用来给指针赋值,所以当用nil给某个指针赋值时一定要指明类型
1 | var x1 interface{} = nil |
17 切片初始化
1 | s := make([]int, 0) |
18 从切片删除元素
实现一个从slice中删除指定索引元素的函数
1 | func RemoveFromSlice(datasrc []int, index int) []int { |
这么做可以删除指定索引的元素,但是也存在隐患因为append会修改data的数据,举个例子
1 | datasrc := []int{6, 1, 0, 5, 2, 9} |
程序输出
1 | data is [1 0 5 2 9] |
可以看到删除功能都是正常的,但是经过第一次删除后datasrc被改变了,主要原因是RemoveFromSlice内部调用了append,append会修改参数datasrc的数据,append将datasrc数据由[6,1,0,5,2,9]经过第一次截取后做了拼接,此时append并未返回新的数据地址,因为不存在扩容,所以最后一个元素没变,还是9,所以datasrc数据为[1,0,5,2,9,9],这时再删除索引为3的元素2,data的值就是[1,0,5,9,9],这也是切片的双刃剑,我们用切片做参数一定要考虑修改后是否对外界有影响
如果想获取删除后的slice,并且不影响原slice,可以修改RemoveFromSlice函数如下
1 | func RemoveFromSliceCopy(datasrc []int, index int) []int { |
再来测试下
1 | datasrc = []int{6, 1, 0, 5, 2, 9} |
输出结果为
1 | data is [1 0 5 2 9] |
通过copy操作实现slice深copy,这样修改copy副本就不会影响源slice了。
19 方法和接口
如果Add函数的调用代码为
1 | func main() { |
i可以转化为Integer指针并调用Integer方法,则Integer实现了Add方法,当然Integer*实现Add方法也可以。如下两种方式都可以
1 | type Integer int |
所以实现一个结构体或其指针的方法后,无论该类型的变量还是指针都可以调用其方法,但是实现结构体指针的方法有一个好处就是可以修改其内部变量。
接口约束,我们能用接口A定义了一个方法B后,凡是实现该方法B的结构体或指针,都可以给A赋值,A作为函数参数,接受实现其方法的结构体对象即可,如果结构体对象未实现该方法,则作为实参传递会报错。看个例子
1 | package main |
这个例子可以正常输出Fly的打印结果,因为*Plane实现了Fly方法。如果我们写一个函数,参数为Bird,内部做类型转换判断
1 | func CheckFly(bird Bird) { |
如果我们调用
1 | CheckFly(&plane) |
会正常输出
1 | plane pointer convert success, is &{plane} |
如果调用
1 | CheckFly(plane) |
会编译报错,因为Plane类没有实现Fly方法,不能传递给Bird接口,这就达到了接口约束的目的。同样修改CheckFly,新增Plane的转换也会报错
1 | func CheckFly(bird Bird) { |
上述代码编译报错的原因是Plane没有实现Fly方法。
当然如果我们将Fly方法的调用者改为Plane,就不会报错了
1 | func (plane Plane) Fly() string { |
实现了Plane类的Fly方法,就隐式实现了指针的Fly方法。所以接口可以转换类类型和类指针类型,编译器不会报错。
关于接口使用和注意事项的文章
https://llfc.club/category?catid=20RbopkFO8nsJafpgCwwxXoCWAs#!aid/21JhR9PJ3GNWCvTB9OjIHTzKb8C
只要两个接口拥有相同的方法列表,即使次序不同,那么他们就是等价的,可以互相赋值。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A,接口查询是否成功,需要在运行阶段才能确定,接口赋值可行在编译阶段就可确定
20 vendor
vendor目录被添加到除了GOPATH和GOROOT之外的依赖目录查找的解决方案. go查找依赖包路径的解决方案如下
1 在当前包下的vendor目录
2 向上级目录查找,直到找到src下的vendor目录
3 在GOPATH下面查找依赖包
4 在GOROOT下查找依赖包
基本思路是将引用的外部包源代码放在当前工程的vendor目录下,go会优先从vendor目录查找依赖包,有了vendor目录后,打包当前的工程代码到其他机器的$GOPATH/src下都可以通过编译
21 channel特性
1 给一个 nil channel发送数据,造成永远阻塞
2 从一个 nil channel接收数据,造成永久阻塞
3 向一个已经关闭的channel写数据,会造成panic
4 从一个已经关闭的channel读数据,如果缓冲区有数据,先读出数据,缓冲区为空,会读出空值
5 无缓冲的channel是同步的,有缓冲的channel是异步的
6 一个只用于读取int数据的单向channel var ch <- chan int
7 一个只用于写int数据的单项channel var ch chan <- int
22 单元测试
go test 可以测试指定代码,该代码文件为*_test.go,文件内的函数如下
1 | func TestXXX( t *testing.T ) |
写一个测试代码helloworld_test.go
1 | package testinst |
然后可以通过go test helloworld_test.go 或者go test -v helloworld_test.go查看输出
1 | === RUN TestHelloWorld |
我们新增一个TestAnother函数
1 | func TestAnother(t *testing.T) { |
可以通过go test -v -run TestAnother helloworld_test.go 指定测试这个函数
1 | === RUN TestAnother |
也可以Benchmark_开头写函数,用来做性能测试
1 | func Benchmark(b *testing.B) { |
执行 go test -v -bench=. hellowrold_test.go
-bench=.表示测试helloworld_test.go文件下所有基准测试。
此外还可以指定benchtime,benchmem等参数
1 | === RUN TestHelloWorld |
23 闭包捕获range变量
如果用闭包捕获for循环的变量会有什么问题呢
1 | func range1() { |
这段代码会输出 zack zack zack
原因在于str为for循环的变量,通过闭包延长了str的生命周期,闭包捕获的是str的本身引用。当三次循环过后str的值变为zack,所以会输出三个zack,我们可以在关键部位打印str的地址和值,验证一下
1 | func range2() { |
程序输出
1 | str is hello |
可以看到每次for循环打印的str值是变化的,但是str地址没变,因为str就是一个变量,而go func()是在三次循环过后才调用,这就导致了输出str的值都为zack。所以我们能只要控制协程及时调用,就能保证str输出不同值。
1 | func range3() { |
输出
1 | str is hello |
通过sleep,保证了每次循环及时调用go func()这样,输出的str和每次遍历的记过一样。
所以从这个例子我们知道,不要用协程或者闭包捕获for循环的变量,因为for循环的变量值会不断变化,而协程很难在准确的时机获取其值,造成逻辑错误。
24 defer链式调用
defer 只能执行一个函数,defer是栈式调用,后入先出规则。当defer执行链式操作时,前边的表达式都会优先求值,只有最后一个表达式入栈延迟执行。
1 | type Slice []int |
s.AddSlice(1)优先被计算,然后是s.AddSlice(2),最后是AddSlice(3),输出值为123
25 字符串
字符串取索引获取的是字节的asc码值,通过切片截取获取的是字串
1 | str := "Hello" |
上述代码分别打印出72和H,如果对str[0]赋值,会编译报错
1 | str[0] = 'M' |
编译报错
1 | cannot assign to str[0] (strings are immutable) |
26 recover和defer
defer 函数中可以使用recover捕获异常,recover要写在defer中,panic可以由本层或者上一层捕获。
具体原理和注意事项请参考
defer注意事项
27 range注意事项
前面的例子说过defer 或者函数捕获 range 变量是捕获的引用,会导致问题,这个例子同样会出现问题,因为map存储的val为stu的指针
1 | func pase_student() { |
程序输出
1 | name is zhou |
因为遍历结束后stu变为{wang 22},所以map的val为&{wang 22},只需要用map存储stu的值就不会出现问题了
1 | func pase_student2() { |
28协程随机性和闭包
1 | func clouser() { |
上述程序输出
1 | B: 9 |
因为多个协程执行是随机的,所以输出A和B也是随机的,B的输出值是0~9, A的输出值为10,因为第一个循环,func捕获的是i的引用。
29 组合和继承
1 | type People struct{} |
main函数调用
1 | func main() { |
输出
1 | showA |
因为匿名组合实现了继承,所以Teacher类有了People的方法ShowA,所以t.ShowA调用的是People类的ShowA
30 select随机性
1 | func maypanic() { |
由于select由随机性,所以上述代码有可能会panic
31 defer 调用顺序和链式求值
1 | func calc(index string, a, b int) int { |
上述代码输出如下
1 | 10 1 2 3 |
具体原理可参考defer原理
32 make初始化长度
1 | func makeslice() { |
程序输出
1 | [0 0 0 0 0 1 2 3] |
因为make初始化slice时指定长度为5,就会默认为slice初始化5个0
33 lock锁
1 | func (ua *UserAges) Add(name string, age int) { |
上述这段代码并不会引发死锁,但是会因为读操作未加锁导致读出来的数据不准确。
34 chan阻塞
1 | func Iter(set *sync.Map) <-chan interface{} { |
上述代码会有什么问题呢?我们看看调用
1 | func main() { |
程序输出
1 | key is zack |
没有vivo的输出,主要原因是ch是无缓冲的,造成了协程写阻塞,而主协程退出后,子协程因为占有ch而无法被回收,造成资源泄露
35 接口内部实现
1 | type People interface { |
上述代码输出
1 | BBBBBBB |
*Student对象stu为空指针,赋值给接口,接口不是nil,因为接口要保存type,data,以及方法集等信息。接口具体结构请参考
接口结构
36 switch type
1 | package main |
上述代码编译失败,会提示i不是interface类型
37 函数返回值参数
1 | func funcMui(x,y int)(sum int,error){ |
上述代码会编译失败,因为返回值要么全带参数,要么全不带
38 defer和返回值参数
1 | func DeferFunc1(i int) (t int) { |
程序输出 4, 1, 3。 defer的可以捕获返回值,并通过计算修改返回值。
39 结构体比较
结构体比较时
1 如果两个结构体内部成员类型不同,则不能比较
2 如果两个结构体内部成员类型相同,但是顺序不同,则不能比较
3 如果两个结构体内不含有无法比较类型,则无法比较
4 如果两个结构体类型,顺序相同,且不含有无法比较类型(slice, map),则可以进行比较
1 | sn1 := struct { |
上面的代码会输出sn1 == sn2
如果新增sn3,与sn1比较
1 | sn1 := struct { |
则编译器汇编报错,提示miss matched struct,因为两个结构体类型相同但是成员的顺序不同,也无法比较
我们新增sn4,让两个结构体的内容不同
1 | sn3 := struct { |
编译器也会编译报错,提示两个结构体成员类型不同,无法比较
如果我们在结构体内部包含map类型
1 | sm1 := struct { |
编译器仍然会报错,提示编译错误,无法比较两个结构体,因为有无法比较的类型map
1 | sps1 := struct { |
上述代码编译器仍然会报错,因为结构体内包含无法比较的类型slice
对于chan,指针,以及interface这些类型都可以比较, 如下代码都可以实现比较
1 | spch1 := struct { |
40 iota
iota是一个特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在const关键字出现时将被重置为0,const中每新增一行常量声明将使 iota 计数加1,因此iota可作为const 语句块中的行索引。
1 | const ( |
程序输出
1 | 0 |
41 alias
1 | func main() { |
上述代码会报错,不能将i赋值给i1,因为i1为MyInt1类型。可以将i赋值给i2,因为alias声明MyInt2类型为int类型的别名。
42 struct alias
1 | type User struct { |
输出
1 | MyUser1.m1 |
43 方法重名
1 | type T1 struct { |
上述代码会编译报错 ambiguous selector my.m1,无论type alias还是type define 都有可能存在类组合后的方法重名情况,那么外部调用就要指明方法所属类名,改为如下调用就没有问题了。
1 | my.T1.m1() |
44 变量作用域
1 | var ErrDidNotWork = errors.New("did not work") |
程序输出
1 | nil |
因为err = ErrDidNotWork, 这个err为作用域内部的err,而返回的是外层err
45 闭包延迟求值
1 | func test() []func() { |
闭包捕获了i的最后值为2,所以结果为
1 | 0xc00000c0a8 2 |
如果想要打印不同的地址和变量值,可以用一个临时变量x存储i的值,闭包捕获x延长x的生命周期就可以了
1 | func test2() []func() { |
46 闭包引用相同变量
1 | func test3(x int) (func(), func()) { |
程序输出100, 110
47 多个panic
1 | func panic1() { |
当程序有多个panic,只有最后一个panic可以被捕获并恢复,程序第一次panic后会执行defer逻辑,defer又触发panic,所以程序输出deferpanic
48 协程控制
目前有一个加载类接口
1 | type LoaderInter interface{ |
要求实现一个类Loader,
1 包含Load方法,Load方法可以模拟实现一些超时或者阻塞操作。
2 另外实现一个Check函数,其参数为LoadInter类型,返回值为error类型,要求内部调用Load方法,
3 如果Load超时则Check函数返回超时错误,否则Check函数返回nil,超时时间设置为5s
4 用代码实现该需求,请勿修改接口和函数参数以及返回值
Check方法形式如下
1 | func Check(li LoaderInter) error { |
下面用代码实现上述需求
1 | type LoaderInter interface { |
程序输出
1 | Loader begin load data.... |
49 协程同步
目前有一个加载类接口
1 | type LoaderInter interface{ |
同时有一个ProducerInter接口, 返回LoaderInter,如果返回nil表明ProducerInter已经生产完毕
1 | type ProducerInter interface { |
现要求实现一个Create函数,函数参数如下
1 | func Create(producer ProducerInter) { |
要求
1 循环调用Produce函数产生多个LoaderInter,直到返回nil表明无需生产
2 每个LoaderInter调用自己的Load操作,Load操作为耗时或者阻塞io操作
3 保证最多同时并发运行五个LoadInter的Load操作
现在实现上述需求
1 | type Loader struct { |
在main函数中调用
1 | func main() { |
我们初始化Producer最多产出10个loader,程序输出如下
1 | Loader begin load data.... |