非常有意思的面试题, 开始第一反应是 _, ok := <-chan 但是这个答案明显不是面试官想要的答案

因为如果chan带有缓冲区, 且缓冲区中还有数据, ok还是true

	if c.closed != 0 && c.qcount == 0 {
        // closed标识符会在close()的时候标记为1
        // qcount是缓冲区中数据的数据量
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}

        ...

	if c.qcount > 0 {
		// Receive directly from queue
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			racenotify(c, c.recvx, nil)
		}
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		typedmemclr(c.elemtype, qp)
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.qcount--
		unlock(&c.lock)
		return true, true
                // 有数据 这里返回了true
	}

        ...

官方并不推荐业务中使用 _, ok 判断, 但是总不能面试官问的时候你抛出这个答案, 最好的方法应该是你解决了这个问题之后再抛出这个答案, 回答就可以说是无懈可击了

配合ctx的超时, 这种解法总觉得不是那么尽善尽美

我们可以想到, 要准确的判断chan状态, 能拿到他的closed字段值肯定是最完美的解决方案.

那么, 如何拿到源码中的hchan结构呢?

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         // 当前 Channel 能够收发的元素大小
	closed   uint32         // 1-关闭
	elemtype *_type         // element type 当前 Channel 能够收发的元素类型
	sendx    uint           // send index 	// 发送操作处理到的位置
	recvx    uint           // receive index	// 接收操作处理到的位置
	recvq    waitq          // list of recv waiters	// 接收阻塞的g列表
	sendq    waitq          // list of send waiters // 发送阻塞的g列表

	// 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
}

这是源码中的hchan结构, 也是chan的底层结构

获取已知却无法访问的结构中的变量, 是不是马上想到了unsafe包修改私有变量这个常规考点?

我们马上来操作一下

	ch := make(chan int, 11)
	ch <- 1
	close(ch)
	fmt.Println(*(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&ch)) + 28)))
  1. uintptr(unsafe.Pointer(&ch) 将ch的地址变成可以计算的指针
  2. 28 是 closed 字段的相对起始值
    uint在64位机上是8字节, 指针unsafe.Pointer是8字节, uint16是2字节, 算下来不应该是 8 * 2 + 8 + 2 = 26 吗? 这里需要有内存对齐的知识, 结构体在排列的时候需要考虑相对内存位置对每个变量都是能做到整除的 26不能整除4, 所以需要空出 2bytes 的空间, 相对位置到28时才能整除4

我们的期望值是1, 虽然缓冲区还有数据, 但是调用closed后 ch.closed已经被标记为1了

然而上述程序并不能打印出我们期望的1

这是因为go在处理 ch :=make(chan int, 11) 时 返回的是 *hchan

func makechan(t *chantype, size int) *hchan {
    ...
}

所以我们需要取到他的二级地址而不是一级

	ch := make(chan int, 11)
	ch <- 1
	close(ch)
	// 先取到ch的起始位置
	add := (*[1]uintptr)(unsafe.Pointer(&ch))
	// add[0]存放的是二级地址 
	add1 := *(*uint32)(unsafe.Pointer(add[0] + 28))
	fmt.Println(add1)

还有一种直接结构体映射的方法, 原理是一样的 只是转换成了结构体

	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         // 当前 Channel 能够收发的元素大小
		closed   uint32         // 1-关闭
	}
	ch := make(chan int, 11)
	ch <- 1
	close(ch)
	k := (*[2]uintptr)(unsafe.Pointer(&ch))
	h := (*hchan)(unsafe.Pointer(k[0]))
	fmt.Println(h.closed)