Golang使用goroutine与channel并发添加链表节点异常问题
问题现象
使用channel与goroutine实现并发向链表追加节点的逻辑时,调用链表打印方法仅重复输出值为8的节点,同一份打印逻辑在其他链表实现中运行正常,无法定位问题根因,原始实现代码如下:
func makeNodes(ctx context.Context, wg *sync.WaitGroup, ch chan Node) { defer wg.Done() for i := 0; i < 9; i++ { tmp := Node{Data: i, Next: nil} ch <- tmp } <-ctx.Done() return } type Node struct { Data int Next *Node } type List struct { Head *Node Length int Last *Node } func (l *List) addToEnd(n *Node) { if l.Head == nil { l.Head = n l.Last = n l.Length++ return } tmp := l.Last tmp.Next = n l.Last = n l.Length++ } func (l List) print() { tmp := l.Head for tmp != nil { fmt.Println(tmp) tmp = tmp.Next } fmt.Println("\n") } func main() { cha := make(chan Node) defer close(cha) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) var wg sync.WaitGroup wg.Add(1) list := List{nil, 0, nil} go makeNodes(ctx, &wg, cha) go func() { for j := range cha { list.addToEnd(&j) } }() cancel() wg.Wait() list.print() }
根因分析
代码一共存在4个核心问题,直接导致异常输出:
- 循环变量地址复用:
for j := range cha中的j是整个循环共用的临时变量,内存地址从循环开始到结束都不会变,每次从channel读到新值就直接覆盖这块内存的内容。所有addToEnd(&j)传入的都是同一个地址,等于链表上所有节点的Next指针最后都指到了这块内存上,而最后一次写入这块内存的值是8,打印的时候顺着指针走就会一直重复读到值为8的节点。 - 协程时序错误:主协程启动消费goroutine后立刻调用
cancel(),随后的wg.Wait()仅等待生产节点的makeNodes协程退出,完全没有等待消费协程完成节点写入,主协程就会直接执行打印逻辑,甚至可能出现消费协程未处理完数据进程就退出的情况。 - 生产协程逻辑错误:
makeNodes发送完9个节点后会阻塞在<-ctx.Done(),由于使用无缓冲channel,如果消费协程未及时接收数据,生产协程会直接卡住;主协程提前调用cancel会直接触发上下文取消,生产协程可能未发送完全部节点就退出。 - 接收者使用不规范:
print方法使用值接收者,调用时会完整拷贝整个链表结构体,虽然不是本次异常的核心诱因,但链表这类包含指针的结构体,方法统一使用指针接收者可以避免不必要的拷贝,也能防止拷贝带来的逻辑异常。
修复方案
针对上述问题逐一修正,完整可运行代码如下:
package main import ( "fmt" "sync" ) type Node struct { Data int Next *Node } type List struct { Head *Node Length int Last *Node } // 统一使用指针接收者 func (l *List) addToEnd(n *Node) { if l.Head == nil { l.Head = n l.Last = n l.Length++ return } l.Last.Next = n l.Last = n l.Length++ } func (l *List) print() { tmp := l.Head for tmp != nil { fmt.Printf("Node value: %d\n", tmp.Data) tmp = tmp.Next } fmt.Printf("Total length: %d\n\n", l.Length) } // 生产协程:发送完所有节点后主动关闭channel,通知消费方数据发送完成 func makeNodes(wg *sync.WaitGroup, ch chan<- Node) { defer wg.Done() for i := 0; i < 9; i++ { ch <- Node{Data: i, Next: nil} } close(ch) } func main() { cha := make(chan Node) var prodWg sync.WaitGroup var consWg sync.WaitGroup list := &List{Head: nil, Length: 0, Last: nil} // 启动生产协程 prodWg.Add(1) go makeNodes(&prodWg, cha) // 启动消费协程 consWg.Add(1) go func() { defer consWg.Done() for j := range cha { // 每次循环创建局部变量拷贝当前节点值,保证每个节点内存地址唯一 node := j list.addToEnd(&node) } }() // 等待生产、消费流程全部完成后再打印链表 prodWg.Wait() consWg.Wait() list.print() }
运行后会按顺序打印0-8共9个节点,链表长度统计为9,符合预期。
内容的提问来源于stack exchange,提问作者Grant
相关产品推荐
相关产品推荐

