昨天AstaXie发布GoCN每日新闻(2017-07-19)含一篇Go面试题。阅读和评论量挺高,是测试面试者对Go本身基础概念理解掌握程度,以及Go实战经验。这也是在Go中容易遇到的坑,我也曾遇到过。于是快马加鞭,抢在原作者前发布Go面试题答案和解析说明,供大家参考。如有错误请指出,谢谢。

1、写出下面代码输出内容。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. defer_call()
  7. }
  8. func defer_call() {
  9. defer func() { fmt.Println("打印前") }()
  10. defer func() { fmt.Println("打印中") }()
  11. defer func() { fmt.Println("打印后") }()
  12. panic("触发异常")
  13. }

在线运行
答: 输出内容为:

  1. 打印后
  2. 打印中
  3. 打印前
  4. panic: 触发异常

解析:
考察对defer的理解,defer函数属延迟执行,延迟到调用者函数执行 return 命令前被执行。多个defer之间按LIFO先进后出顺序执行。
故考题中,在Panic触发时结束函数运行,在return前先依次打印:打印后、打印中、打印前 。最后由runtime运行时抛出打印panic异常信息。
需要注意的是,函数的return value 不是原子操作.而是在编译器中分解为两部分:返回值赋值 和 return 。而defer刚好被插入到末尾的return前执行。故可以在derfer函数中修改返回值。如下示例:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. fmt.Println(doubleScore(0)) //0
  7. fmt.Println(doubleScore(20.0)) //40
  8. fmt.Println(doubleScore(50.0)) //50
  9. }
  10. func doubleScore(source float32) (score float32) {
  11. defer func() {
  12. if score < 1 || score >= 100 {
  13. //将影响返回值
  14. score = source
  15. }
  16. }()
  17. score = source * 2
  18. return
  19. //或者
  20. //return source * 2
  21. }

在线运行
该实例可以在defer中修改返回值score的值。具体参见官方文档

2、以下代码有什么问题,说明原因

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type student struct {
  6. Name string
  7. Age int
  8. }
  9. func pase_student() map[string]*student {
  10. m := make(map[string]*student)
  11. stus := []student{
  12. {Name: "zhou", Age: 24},
  13. {Name: "li", Age: 23},
  14. {Name: "wang", Age: 22},
  15. }
  16. for _, stu := range stus {
  17. m[stu.Name] = &stu
  18. }
  19. return m
  20. }
  21. func main() {
  22. students := pase_student()
  23. for k, v := range students {
  24. fmt.Printf("key=%s,value=%v \n", k, v)
  25. }
  26. }

在线运行
答:输出的均是相同的值:&{wang 22}
解析 因为for遍历时,变量stu指针不变,每次遍历仅进行struct值拷贝,故m[stu.Name]=&stu实际上一致指向同一个指针,最终该指针的值为遍历的最后一个struct的值拷贝。形同如下代码:

  1. var stu student
  2. for _, stu = range stus {
  3. m[stu.Name] = &stu
  4. }

修正方案,取数组中原始值的指针:

  1. for i, _ := range stus {
  2. stu:=stus[i]
  3. m[stu.Name] = &stu
  4. }

3、下面的代码会输出什么,并说明原因

  1. func main() {
  2. runtime.GOMAXPROCS(1)
  3. wg := sync.WaitGroup{}
  4. wg.Add(20)
  5. for i := 0; i < 10; i++ {
  6. go func() {
  7. fmt.Println("i: ", i)
  8. wg.Done()
  9. }()
  10. }
  11. for i := 0; i < 10; i++ {
  12. go func(i int) {
  13. fmt.Println("i: ", i)
  14. wg.Done()
  15. }(i)
  16. }
  17. wg.Wait()
  18. }

在线运行
答: 将随机输出数字,但前面一个循环中并不会输出所有值。
解析:
实际上第一行是否设置CPU为1都不会影响后续代码
2017年7月25日:将GOMAXPROCS设置为1,将影响goroutine的并发,后续代码中的go func()相当于串行执行。
两个for循环内部go func 调用参数i的方式是不同的,导致结果完全不同。这也是新手容易遇到的坑。
第一个go func中i是外部for的一个变量,地址不变化。遍历完成后,最终i=10。故go func执行时,i的值始终是10(10次遍历很快完成)。
第二个go func中i是函数参数,与外部for中的i完全是两个变量。尾部(i)将发生值拷贝,go func内部指向值拷贝地址。

4、下面代码会输出什么?

  1. type People struct{}
  2. func (p *People) ShowA() {
  3. fmt.Println("showA")
  4. p.ShowB()
  5. }
  6. func (p *People) ShowB() {
  7. fmt.Println("showB")
  8. }
  9. type Teacher struct {
  10. People
  11. }
  12. func (t *Teacher) ShowB() {
  13. fmt.Println("teacher showB")
  14. }
  15. func main() {
  16. t := Teacher{}
  17. t.ShowA()
  18. }

在线运行
答: 将输出:

  1. showA
  2. showB

解析
Go中没有继承! 没有继承!没有继承!是叫组合!组合!组合!
这里People是匿名组合People。被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法,但他们的方法(ShowA())调用时接受者并没有发生变化。
这里仍然是People。毕竟这个People类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Teacher类型的功能。
因此这里执行t.ShowA()时,在执行ShowB()时该函数的接受者是People,而非Teacher。具体参见官方文档

5、下面代码会触发异常吗?请详细说明

  1. func main() {
  2. runtime.GOMAXPROCS(1)
  3. int_chan := make(chan int, 1)
  4. string_chan := make(chan string, 1)
  5. int_chan <- 1
  6. string_chan <- "hello"
  7. select {
  8. case value := <-int_chan:
  9. fmt.Println(value)
  10. case value := <-string_chan:
  11. panic(value)
  12. }
  13. }

在线运行
答: 有可能触发异常,是随机事件。
解析
单个chan如果无缓冲时,将会阻塞。但结合 select可以在多个chan间等待执行。有三点原则:

  • select 中只要有一个case能return,则立刻执行。
  • 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
  • 如果没有一个case能return则可以执行”default”块。

此考题中的两个case中的两个chan均能return,则会随机执行某个case块。故在执行程序时,有可能执行第二个case,触发异常。具体参见官方文档

6、下面代码输出什么?

  1. func calc(index string, a, b int) int {
  2. ret := a + b
  3. fmt.Println(index, a, b, ret)
  4. return ret
  5. }
  6. func main() {
  7. a := 1 //line 1
  8. b := 2 //2
  9. defer calc("1", a, calc("10", a, b)) //3
  10. a = 0 //4
  11. defer calc("2", a, calc("20", a, b)) //5
  12. b = 1 //6
  13. }

在线运行
输出结果为:

  1. 10 1 2 3
  2. 20 0 2 2
  3. 2 0 2 2
  4. 1 1 3 4

解析
在解题前需要明确两个概念:

  • defer是在函数末尾的return前执行,先进后执行,具体见问题1。
  • 函数调用时 int 参数发生值拷贝。

不管代码顺序如何,defer calc func中参数b必须先计算,故会在运行到第三行时,执行calc("10",a,b)输出:10 1 2 3得到值3,将cal("1",1,3)存放到延后执执行函数队列中。
执行到第五行时,现行计算calc("20", a, b)calc("20", 0, 2)输出:20 0 2 2得到值2,将cal("2",0,2)存放到延后执行函数队列中。
执行到末尾行,按队列先进后出原则依次执行:cal("2",0,2)cal("1",1,3) ,依次输出:2 0 2 21 1 3 4

7、请写出以下输入内容

  1. func main() {
  2. s := make([]int, 5)
  3. s = append(s, 1, 2, 3)
  4. fmt.Println(s)
  5. }

在线运行
答: 将输出:[0 0 0 0 0 1 2 3]
解析
make可用于初始化数组,第二个可选参数表示数组的长度。数组是不可变的。
当执行make([]int,5)时返回的是一个含义默认值(int的默认值为0)的数组:[0,0,0,0,0]。而append函数是便是在一个数组或slice后面追加新的元素,并返回一个新的数组或slice。
这里append(s,1,2,3)是在数组s的继承上追加三个新元素:1、2、3,故返回的新数组为[0 0 0 0 0 1 2 3]

8、下面的代码有什么问题?

  1. type UserAges struct {
  2. ages map[string]int
  3. sync.Mutex
  4. }
  5. func (ua *UserAges) Add(name string, age int) {
  6. ua.Lock()
  7. defer ua.Unlock()
  8. ua.ages[name] = age
  9. }
  10. func (ua *UserAges) Get(name string) int {
  11. if age, ok := ua.ages[name]; ok {
  12. return age
  13. }
  14. return -1
  15. }

在线运行
答: 在执行 Get方法时可能被panic
解析
虽然有使用sync.Mutex做写锁,但是map是并发读写不安全的。map属于引用类型,并发读写时多个协程见是通过指针访问同一个地址,即访问共享变量,此时同时读写资源存在竞争关系。会报错误信息:“fatal error: concurrent map read and map write”。
可以在在线运行中执行,复现该问题。那么如何改善呢? 当然Go1.9新版本中将提供并发安全的map。首先需要了解两种锁的不同:

  1. sync.Mutex互斥锁
  2. sync.RWMutex读写锁,基于互斥锁的实现,可以加多个读锁或者一个写锁。

利用读写锁可实现对map的安全访问,在线运行改进版 。利用RWutex进行读锁。

  1. type RWMutex
  2. func (rw *RWMutex) Lock()
  3. func (rw *RWMutex) RLock()
  4. func (rw *RWMutex) RLocker() Locker
  5. func (rw *RWMutex) RUnlock()
  6. func (rw *RWMutex) Unlock()

9、下面的迭代会有什么问题?

  1. func (set *threadSafeSet) Iter() <-chan interface{} {
  2. ch := make(chan interface{})
  3. go func() {
  4. set.RLock()
  5. for elem := range set.s {
  6. ch <- elem
  7. }
  8. close(ch)
  9. set.RUnlock()
  10. }()
  11. return ch
  12. }

在线运行
答: 内部迭代出现阻塞。默认初始化时无缓冲区,需要等待接收者读取后才能继续写入。
解析
chan在使用make初始化时可附带一个可选参数来设置缓冲区。默认无缓冲,题目中便初始化的是无缓冲区的chan,这样只有写入的元素直到被读取后才能继续写入,不然就一直阻塞。
设置缓冲区大小后,写入数据时可连续写入到缓冲区中,直到缓冲区被占满。从chan中接收一次便可从缓冲区中释放一次。可以理解为chan是可以设置吞吐量的处理池。
来自社区fiisio的说明

  1. ch := make(chan interface{}) ch := make(chan interface{},1)是不一样的
  2. 无缓冲的 不仅仅是只能向 ch 通道放 一个值 而是一直要有人接收,那么ch <- elem才会继续下去,要不然就一直阻塞着,也就是说有接收者才去放,没有接收者就阻塞。
  3. 而缓冲为1则即使没有接收者也不会阻塞,因为缓冲大小是1只有当 放第二个值的时候 第一个还没被人拿走,这时候才会阻塞

10、以下代码能编译过去吗?为什么?

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type People interface {
  6. Speak(string) string
  7. }
  8. type Stduent struct{}
  9. func (stu *Stduent) Speak(think string) (talk string) {
  10. if think == "bitch" {
  11. talk = "You are a good boy"
  12. } else {
  13. talk = "hi"
  14. }
  15. return
  16. }
  17. func main() {
  18. var peo People = Stduent{}
  19. think := "bitch"
  20. fmt.Println(peo.Speak(think))
  21. }

在线运行
答: 编译失败,值类型 Student{} 未实现接口People的方法,不能定义为 People类型。
解析
考题中的 func (stu *Stduent) Speak(think string) (talk string) 是表示结构类型*Student的指针有提供该方法,但该方法并不属于结构类型Student的方法。因为struct是值类型。
修改方法:

  • 定义为指针 go var peo People = &Stduent{}
  • 方法定义在值类型上,指针类型本身是包含值类型的方法。 go func (stu Stduent) Speak(think string) (talk string) { //... }

11. 在go中,new和make的区别

关于new

image.png

new 内置函数分配内存。第一个参数是类型,而不是值,返回的值是指向该类型新分配的零值的指针

  • new 的作用是初始化一个指向类型的指针(*T)
  • new是内奸函数,函数定义 func new(Type) *Type
  • 使用new来分配空间。
  • 传递给new的是一个类型,不是一个值;返回值是指向这个新分配的`零值的指针。

关于make

image.png

make内置函数分配并初始化类型为slice、map或chan(仅)的对象。
与new一样,第一个参数是类型,而不是值。
与new不同,make的返回类型与其参数的类型相同,而不是指向它的指针。

结果的规格取决于类型:

  • Slice: size指定长度。切片的容量等于它的长度。可以提供第二个整型参数来指定不同的容量;

它必须不小于长度。例如,make([]int, 0,10)分配一个大小为10的底层数组,并返回由这个底层数组支持的长度为0、容量为10的片。

  • Map: 一个空映射被分配了足够的空间来容纳指定数量的元素。大小可以省略,在这种情况下,分配一个小的初始大小。
  • 通道: 通道的缓冲区用指定的缓冲区容量初始化。如果为0,或者省略大小,则通道未缓冲。
  • make的作用是为 slice, map、chan 初始化并返回引用(T)
  • make是内那家函数,函数定义 func make(Type, size IntegerType) Type

1.2 go中 Printf、Sprintf、Fprintf 函数的区别用法是什么