面试题

这是Go Quiz系列的第2篇,关于channelselect的特性。

这道题比较简单,但是通过这道题可以加深我们对channelselect的理解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

func main() {
	c := make(chan int, 1)
	for done := false; !done; {
		select {
		default:
			print(1)
			done = true
		case <-c:
			print(2)
			c = nil
		case c <- 1:
			print(3)
		}
	}
}
  • A: 321
  • B: 21
  • C: 1
  • D: 31

这道题主要考察以下知识点:

  • channel的数据收发在什么情况会阻塞?

  • select的运行机制是怎样的?

  • nil channel收发数据是什么结果?

解析

  1. 对于无缓冲区的channel,往channel发送数据和从channel接收数据都会阻塞。

  2. 对于nil channel和有缓冲区的channel,收发数据的机制如下表所示:

    channel nil 空的 非空非满 满了
    往channel发送数据 阻塞 发送成功 发送成功 阻塞
    从channel接收数据 阻塞 阻塞 接收成功 接收成功
    关闭channel panic 关闭成功 关闭成功 关闭成功
  3. channel被关闭后:

    • 往被关闭的channel发送数据会触发panic。

    • 从被关闭的channel接收数据,会先读完channel里的数据。如果数据读完了,继续从channel读数据会拿到channel里存储的元素类型的零值。

      1
      
      data, ok := <- c 
      

      对于上面的代码,如果channel c关闭了,继续从c里读数据,当c里还有数据时,data就是对应读到的值,ok的值是true。如果c的数据已经读完了,那data就是零值,ok的值是false

    • channel被关闭后,如果再次关闭,会引发panic。

  4. select的运行机制如下:

    • 选取一个可执行不阻塞的case分支,如果多个case分支都不阻塞,会随机选一个case分支执行,和case分支在代码里写的顺序没关系。
    • 如果所有case分支都阻塞,会进入default分支执行。
    • 如果没有default分支,那select会阻塞,直到有一个case分支不阻塞。

根据以上规则,本文最开始的题目,在运行的时候

  • 第1次for循环,只有c <- 1是不阻塞的,所以最后一个case分支执行,打印3。
  • 第2次for循环,只有<-c是不阻塞的,所以第1个case分支执行,打印2,同时channel被赋值为nil
  • 第3次for循环,因为channelnil对nil channel的读和写都阻塞,所以进入default分支,打印1,done设置为true,for循环退出,程序运行结束。

因此打印结果是321,答案是A

加餐:channel函数传参是引用传递?

网上有些文章写Go的slicemapchannel作为函数参数是传引用,这是错误的,Go语言里只有值传递,没有引用传递。

既然channel是值传递,那为什么channel作为函数形参时,函数内部对channel的读写对外部的实参channel是可见的呢?

对于这个问题,看Go的源码就一目了然了。channel定义在src/runtime/chan.go第33行,源码地址:https://github.com/golang/go/blob/master/src/runtime/chan.go#L33

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func makechan(t *chantype, size int) *hchan

// channel结构体
type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

我们通过make函数来创建channel时,Go会调用运行时的makechan函数。

从上面的代码可以看出makechan返回的是指向channel的指针。

因此channel作为函数参数时,实参channel和形参channel都指向同一个channel结构体的内存空间,所以在函数内部对channel形参的修改对外部channel实参是可见的,反之亦然。

开源地址

文章和示例代码开源地址在GitHub: https://github.com/jincheng9/go-tutorial

公众号:coding进阶

个人网站:https://jincheng9.github.io/

知乎:https://www.zhihu.com/people/thucuhkwuji

好文推荐

  1. 被defer的函数一定会执行么?
  2. Go有引用变量和引用传递么?map,channel和slice作为函数参数是引用传递么?
  3. new和make的使用区别是什么?
  4. 一文读懂Go匿名结构体的使用场景
  5. 官方教程:Go泛型入门
  6. 一文读懂Go泛型设计和使用场景
  7. Go Quiz: 从Go面试题看slice的底层原理和注意事项

References