You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 08:51:27