
Go 语言中向 channel 发送值类型(如结构体、切片、指针等)时,仅执行一次浅拷贝:发送时将值复制到 channel 缓冲区,接收时再从缓冲区复制到接收变量;对于 *list.List 或 []string 等引用类型,实际复制的是指针/头信息,底层数据不会被重复分配。
go 语言中向 channel 发送值类型(如结构体、切片、指针等)时,仅执行一次浅拷贝:发送时将值复制到 channel 缓冲区,接收时再从缓冲区复制到接收变量;对于 *list.list 或 []string 等引用类型,实际复制的是指针/头信息,底层数据不会被重复分配。
在 Go 中,channel 是 goroutine 间通信的核心原语,其数据传递语义明确且高效,但理解其底层复制行为对性能优化和并发安全至关重要。
✅ 浅拷贝的本质:只传“描述符”,不搬“数据体”
当你向 channel 发送一个值时(例如 ch 或 <code>ch ),Go 运行时会<strong>按值复制该变量的内存表示</strong>。关键在于:
- 若发送的是
[]string:复制的是 slice header(含指针、长度、容量共 24 字节),底层数组不会被复制; - 若发送的是
*list.List:复制的是 8 字节(64 位系统)的指针,对象本身仍位于原内存位置; - 若发送的是
list.List(值类型,非指针):复制的是list.List结构体本身(仅两个字段:root和len,共 16 字节),*但链表节点(`Element)未被深拷贝**——所有Element` 仍由原 list 管理。
// 示例:发送 slice 和 list 指针,零额外堆分配
data := []string{"a", "b", "c"}
ch := make(chan []string, 1)
ch <- data // 仅复制 24 字节 header;底层数组未复制
import "container/list"
l := list.New()
l.PushBack("x")
chList := make(chan *list.List, 1)
chList <- l // 仅复制 8 字节指针⚠️ 关键陷阱:浅拷贝 ≠ 线程安全
浅拷贝虽轻量,但绝不意味着并发安全。典型风险如下:
- 多个 goroutine 通过 channel 收到同一
[]string的副本后,若都调用append(),可能触发底层数组扩容并导致数据竞争(因append可能修改共享底层数组); - 若发送的是
*list.List,多个接收方持有同一 list 指针,同时调用PushBack/Remove将引发竞态(race condition),必须加锁或使用sync.Mutex保护; - 即使发送
list.List{}值类型,其内部root字段是指针,复制后多个 goroutine 仍可能操作同一链表节点(root.next等),仍需同步控制。
✅ 正确做法:
- 对共享可变数据,优先发送不可变副本(如
strings.Clone后的字符串切片,或s[:len(s):len(s)]防止 append 扩容); - 使用
sync.RWMutex或sync.Once控制 list 访问; - 更推荐模式:channel 仅传递所有权(如发送
chan ),由接收方独占处理,避免跨 goroutine 共享可变状态。
✅ 性能小结
| 类型 | 复制开销(64 位) | 是否触发 GC 分配 | 并发风险 |
|---|---|---|---|
[]string |
~24 B | ❌ 否 | ✅ 高(append/range 修改底层数组) |
*list.List |
8 B | ❌ 否 | ✅ 高(多 goroutine 修改同一链表) |
list.List(值) |
16 B | ❌ 否 | ✅ 高(内部指针仍指向共享节点) |
总之,Go channel 的通信开销极低——本质是内存块复制,无序列化、无系统调用。真正的成本不在 channel 本身,而在于如何设计数据所有权模型。遵循“共享内存通过通信,通信勿共享内存”的原则,才能兼顾性能与安全。


















