
文档教程【免费下载链接】go-questions Go 程序员面试笔试宝典 | 从问题切入串连 Go 语言相关的所有知识融会贯通。 https://golang.design/go-questions项目地址https://gitcode.com/gh_mirrors/go/go-questions点击查看免费下载导读本文围绕《Go 程序员面试笔试宝典》go-questions中 channel 专题的核心命题展开channel 的发送与接收本质上都是“值的拷贝”copy of value。文章先给出结论与官方论述再用一个指针型结构体的完整示例剖析拷贝路径并对照 channel 底层源码 中的hchan、chansend、sendDirect、chanrecv、recvDirect等实现说明“拷贝”在缓冲型、非缓冲型、直接交接三种路径下分别如何发生帮助读者真正理解share memory by communicating的底层含义。在 Go 的并发世界里channel 是 goroutine 之间传递数据的主要通道。很多人会用“发送数据”“接收数据”来描述 channel 上的操作但更深一层的问题在于channel 发送和接收元素的本质到底是什么go-questions 项目在 content/channel/9-channel 发送和接收元素的本质是什么.md 中给出了明确答案引用的是 Go 语言设计者对 channel 的经典论述All transfer of value on the go channels happens with the copy of value.也就是说channel 的发送和接收操作本质上都是“值的拷贝”。无论数据从发送方 goroutine 的栈拷贝到 channel 的缓冲区chan buf还是从 chan buf 拷贝到接收方 goroutine抑或是在非缓冲场景下直接从发送方 goroutine 拷贝到接收方 goroutine底层完成的工作都是同一件事——拷贝一份值而不是“搬移”引用或“传递”对象本身。一句话理解channel 里流动的是“值”不是“对象”这里的“值”是 Go 语言层面的概念。对于基础类型int、string 等拷贝的当然是数据本身对于指针、切片、map、channel 这类引用类型拷贝的是指针/引用值而不是指针所指向的堆对象内容。换句话说channel 从不帮你“传递所有权”它只是忠实地复制一份你放进来的东西。这一点对面试和实际编码都至关重要发送int、string等值类型接收方拿到的是发送时刻值的快照之后修改发送方的局部变量不影响已发送的数据发送*user这类指针channel 里保存的是指针值一个地址发送后发送方若把指针变量重新赋值指向新对象channel 里那个旧地址仍指向旧对象接收方读到的依然是发送瞬间拷贝的指针值所指向的数据。用一段代码看清“拷贝”的本质文档中给出了一个非常经典的例子它同时演示了“值的拷贝”和 Go 并发哲学share memory by communicatingpackage main import ( fmt time ) type user struct { name string age int8 } var u user{name: Ankur, age: 25} var g u func modifyUser(pu *user) { fmt.Println(modifyUser Received Vaule, pu) pu.name Anand } func printUser(u -chan *user) { time.Sleep(2 * time.Second) fmt.Println(printUser goRoutine called, -u) } func main() { c : make(chan *user, 5) c - g fmt.Println(g) // modify g g user{name: Ankur Anand, age: 100} go printUser(c) go modifyUser(g) time.Sleep(5 * time.Second) fmt.Println(g) }运行结果{Ankur 25} modifyUser Received Vaule {Ankur Anand 100} printUser goRoutine called {Ankur 25} {Anand 100}这个输出非常有意思我们来逐步拆解c - g此时g指向变量u{Ankur 25}。根据“拷贝值”的本质被放进 chan buf 的是g的值——即指向u的地址0x56420示例中 u 的地址而不是g本身更不是u的内容的某种引用关系。fmt.Println(g)输出{Ankur 25}此刻g仍指向u一切如常。g user{name: Ankur Anand, age: 100}发送方把g重新指向了一个全新的user对象。此时 chan buf 里保存的地址0x56420依然指向原来的u。go printUser(c)接收方从 channel 里取出的正是 chan buf 中保存的指针值0x56420因此打印出{Ankur 25}——它看到的依然是发送瞬间被拷贝的那个值而不是发送方后来重新赋值的g。go modifyUser(g)这个 goroutine 拿到的是发送方当前最新的g指向{Ankur Anand 100}通过指针把name改成Anand所以最终fmt.Println(g)输出{Anand 100}。这里最重要的结论是“发送到 channel 里的不是指针 g只是 g 的值一个地址的拷贝”。chan buf 中保存的0x56420并不会因为发送方把g指向新对象而改变。而modifyUser之所以能修改到对象内容是因为它拿到的拷贝是同一个地址通过地址解引用自然能改到同一块内存——这与 channel 的拷贝本质并不矛盾拷贝的是地址值而地址指向的对象是共享的。这正是share memory by communicating通过通信来共享内存的生动写照我们通过 channel 传递的是地址值的拷贝通信双方由此“共享”了同一块内存却不需要显式加锁。文档中还有一张示意图content/channel/assets/12.png直观展示了上述过程上方定义user结构体、指针g及容量为 5 的chan *user中间展示发送时的内存拷贝动作下方展示发送完成后 chan buf 中仍保留原指针值而g已重新指向新对象。文档最后再次强调这条铁律Remember all transfer of value on the go channels happens with the copy of value.从源码看“拷贝”如何发生“拷贝值”这一本质在 Go runtime 源码里有非常具体的实现。以 Go 1.9.2 版本为例channel 的底层结构hchan定义在src/runtime/chan.go中go-questions 的 channel 底层数据结构 一节有完整注释type hchan struct { qcount uint // chan 里元素数量 dataqsiz uint // chan 底层循环数组的长度 buf unsafe.Pointer // 指向底层循环数组的指针只针对有缓冲的 channel elemsize uint16 // chan 中元素大小 closed uint32 // chan 是否被关闭的标志 elemtype *_type // chan 中元素类型 sendx uint // 已发送元素在循环数组中的索引 recvx uint // 已接收元素在循环数组中的索引 recvq waitq // 等待接收的 goroutine 队列 sendq waitq // 等待发送的 goroutine 队列 lock mutex // 保护 hchan 中所有字段 }注意buf指向的就是那块“拷贝目的地”——有缓冲 channel 的循环数组。无论qcount如何变化进入 buf 的每一个元素都是通过一次内存拷贝落地的。sendx/recvx则分别记录下一次发送/接收应该落在 buf 的哪个槽位。路径一有缓冲 channel数据先拷贝进 buf向 channel 发送最终由chansend函数完成。当 channel 还有缓冲空间时c.qcount c.dataqsiz关键代码是qp : chanbuf(c, c.sendx) // qp 指向 buf 的 sendx 位置 // …… typedmemmove(c.elemtype, qp, ep) // 将数据从 ep 处拷贝到 qp c.sendx if c.sendx c.dataqsiz { c.sendx 0 } c.qcounttypedmemmove是一次按元素类型进行的内存拷贝把发送方提供的元素ep拷贝进循环数组的槽位。接收侧在chanrecv里对应的是qp : chanbuf(c, c.recvx) if ep ! nil { typedmemmove(c.elemtype, ep, qp) // 从 buf 拷贝到接收方 } typedmemclr(c.elemtype, qp) // 清理 buf 槽位 c.recvx if c.recvx c.dataqsiz { c.recvx 0 } c.qcount--可以看到接收操作同样是typedmemmove——从 buf 拷贝到接收方的变量地址。一来一回全是拷贝没有任何“移动”语义。对于*user这种元素类型拷贝的就是一个指针值unsafe.Sizeof大小与文档示例中的行为完全吻合。路径二非缓冲直接交接跳过 buf 的直连拷贝当发送方发现接收队列recvq里有等待的 goroutine 时典型场景非缓冲 channel或有缓冲但当前为空且有 receiver 在等会走send函数由sendDirect完成“直连拷贝”func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) { // src 在当前 goroutine 的栈上dst 是另一个 goroutine 的栈 dst : sg.elem typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size) memmove(dst, src, t.size) }接收侧对称的实现是recvDirectfunc recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) { src : sg.elem // src 在另一个 goroutine 的栈上 typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size) memmove(dst, src, t.size) }这仍是“值的拷贝”只是把目的地从 chan buf 变成了对方 goroutine 栈上的变量地址省掉了一次中间缓冲区的往返。由于这里出现一个 goroutine 直接写另一个 goroutine 栈的情况runtime 特意加上了写屏障typeBitsBulkBarrier以符合 GC 对栈写操作的假设。文档在 向 channel 发送数据的过程 和 从 channel 接收数据的过程 两节中对这两条路径有非常详细的源码级拆解包括非阻塞发送的快速失败判断、缓冲区满时构造sudog入队sendq并goparkunlock挂起等完整流程。路径三缓冲满时的“队首出、队尾进”当缓冲型 channel 已满sendq非空时接收方会执行recv函数先把 buf 队首recvx处的元素拷贝给接收者再把发送者 sudog 中的待发元素拷贝进这个刚刚空出的槽位。同样两次typedmemmove同样全是拷贝。把“拷贝”放进 channel 操作的完整图景理解了“一切皆拷贝”再看 channel 的各种操作结果就更有把握了。go-questions 的 操作 channel 的情况总结 给出了全景表操作nil channelclosed channelnot nil, not closed channelclosepanicpanic正常关闭读 - ch阻塞读到对应类型的零值阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞写 ch -阻塞panic阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞由此也归纳出 panic 的三种情形向已关闭的 channel 写、关闭 nil channel、重复关闭 channel而对 nil channel 的读写都会永久阻塞。关闭行为在 关闭一个 channel 的过程 中有源码级说明closechan会把挂在recvq上的接收者全部唤醒并让他们收到类型零值而挂在sendq上的发送者被唤醒后会在chansend中检测到 channel 已关闭而 panic——这也解释了为什么文档强调“在不了解 channel 还有没有接收者的情况下不能贸然关闭 channel”。此外从一个关闭的 channel 仍然能读出数据吗 则验证了关闭后缓冲型 channel 中残留的元素依然可以正常读出直到ok变为false。总结channel 发送和接收的本质是值的拷贝发送方放进 channel 的是值的副本接收方拿到的同样是副本channel 自始至终没有“移动”任何对象。对于指针、切片等引用类型拷贝的是引用值地址因此发送后双方仍可能通过该地址共享同一块内存——这正是share memory by communicating的实现机制。runtime 源码chansend/chanrecv中的typedmemmove、sendDirect/recvDirect中的memmove证实无论数据走 chan buf 中转还是非缓冲直接交接落地的动作都是按元素类型的内存拷贝。理解这一点就能解释为何发送方修改指针变量不影响已发送数据、为何缓冲 channel 关闭后仍能读出残留元素等一系实例也能更从容地设计基于 channel 的数据传递方案。本文核心命题的原始论述与示例见 content/channel/9-channel 发送和接收元素的本质是什么.md如需继续深入可对照阅读同专题下的 channel 底层的数据结构、发送过程、接收过程、关闭过程 以及 操作情况总结。赞分享文档教程【免费下载链接】go-questions Go 程序员面试笔试宝典 | 从问题切入串连 Go 语言相关的所有知识融会贯通。 https://golang.design/go-questions项目地址https://gitcode.com/gh_mirrors/go/go-questions点击查看免费下载相关推荐如何在5分钟内集成FancyToast-Android新手必备的快速上手指南如何在5分钟内集成FancyToast Android新手必备的快速上手指南 FancyToast Android是一款能让原生Android Toast变得Fluent Bit 内嵌 nghttp2 的零拷贝发送nghttp2_session_callbacks_set_send_data_callback 完整解析Fluent Bit 内嵌 nghttp2 的零拷贝发送nghttp2_session_callbacks_set_send_data_callback 完整可观测性日志分析云原生流处理RT-Thread 中 Microchip SAM E70 USART 异步驱动usart_async详解环形缓冲接收、零拷贝发送与流程控制RT Thread 中 Microchip SAM E70 USART 异步驱动usart_async详解环形缓冲接收、零拷贝发送与流程控制 本篇技术指南操作系统嵌入式物联网嵌入式OSRTOS上一篇认识Nex-N2.5-Pro开源AI智能体多模态大模型完整概览——它会操作电脑、浏览网页的10大能力下一篇从文件级审计到可辩护的合规声明vscode-gitlens 的 WCAG Compliance Rollup 方法论与实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考