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

goexpect:SpawnGeneric使用中的竞态条件问题排查

问题分析与解决

你的代码出现随机卡死的核心原因是goroutine调度时序不确定,加上io.Pipe的阻塞特性导致竞态:

  • io.Pipe的Write操作会阻塞,直到对应的Read端读取数据;
  • 自定义goroutine(处理命令读取和响应)和main goroutine(调用Send/Expect)的执行顺序完全由Go调度器决定:
    • 若自定义goroutine先被调度,进入rIn.Read()等待读取,main的Send能顺利写入,流程正常;
    • 若main goroutine先执行e.Send(),此时自定义goroutine还没开始读取rIn,Send会阻塞,而自定义goroutine又在等待调度,最终形成互相阻塞的死锁状态。

另外还有两个潜在问题:

  1. 写完响应后未关闭wOut,可能导致Expect一直等待更多数据;
  2. SpawnGeneric的Wait函数直接返回nil,goexpect会认为"子进程"已退出,可能引发后续异常。

修复后的代码
package main

import (
    "fmt"
    "io"
    "regexp"
    "sync"
    "time"

    expect "github.com/google/goexpect"
)

func main() {
    fmt.Println("Starting execution!")
    e := getSpawner()
    defer e.Close() // 记得关闭资源

    e.Send("random command sent")

    out, match, err := e.Expect(regexp.MustCompile(`>>`), time.Second*10)
    fmt.Println(out, match, err)
}

func getSpawner() *expect.GExpect {
    rIn, wIn := io.Pipe()
    rOut, wOut := io.Pipe()
    var initWg sync.WaitGroup
    var cmdWg sync.WaitGroup

    initWg.Add(1)
    cmdWg.Add(1)

    go func() {
        defer cmdWg.Done()
        defer wOut.Close() // 写完响应后关闭输出管道,避免Expect等待
        defer rIn.Close()  // 关闭输入读端

        // 通知main goroutine:已准备好读取命令
        initWg.Done()

        cmdPayload := make([]byte, 5000)
        n, err := rIn.Read(cmdPayload)
        if err != nil && err != io.EOF {
            fmt.Println("Err while reading command to run", err)
            return
        }

        cmd := string(cmdPayload[:n])
        fmt.Println("Recieved payload ------ ", cmd)
        _, _ = wOut.Write([]byte("This is my response and an ending prompt! >>"))
    }()

    // 等待自定义goroutine准备好读取,再初始化goexpect
    initWg.Wait()

    exp, _, err := expect.SpawnGeneric(&expect.GenOptions{
        In:  wIn,
        Out: rOut,
        Wait: func() error {
            // 等待自定义goroutine执行完成
            cmdWg.Wait()
            return nil
        },
        Close: func() error {
            // 关闭所有管道资源
            _ = wIn.Close()
            _ = rOut.Close()
            return nil
        },
        Check: func() bool { return true },
    }, time.Second*20, expect.SendTimeout(time.Second*5))

    if err != nil {
        fmt.Println("Error spawning SpawnGeneric")
        panic(err)
    }
    return exp
}

关键修改点
  • 添加initWg同步:确保自定义goroutine进入读取状态后,再让main继续执行Send,彻底消除竞态;
  • 添加cmdWg让Wait函数等待自定义goroutine完成,符合goexpect对"子进程"生命周期的预期;
  • 增加管道关闭逻辑:避免资源泄漏,同时让Expect在响应写完后能正确识别数据结束;
  • 优化错误处理:忽略EOF(管道正常关闭的情况),避免不必要的panic。

内容的提问来源于stack exchange,提问作者killerx

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 15:43:11