golang流式数据处理与实时传输:解决IO数据丢失的避雷指南

368 阅读5分钟

golang流式数据处理与实时传输:解决IO数据丢失的避雷指南

在开发一个 Go 应用程序时,处理流式数据并实时传输到客户端是一项常见但具有挑战性的任务。尽管 Go 的并发机制强大,但在实现过程中,我们可能会遇到一些棘手的问题,比如本地打印数据时出现数据丢失,以及 sync: negative WaitGroup counter 错误。本文将介绍我们如何一步步解决这些问题,最终实现一个稳定的流式数据处理与实时传输系统。

背景

在这个项目中,我们需要从服务器接收流式数据,并实时地将其传输到客户端。同时,我们还需要对数据进行处理,并保存处理后的结果。在初期的实现中,我们遇到了两个主要问题:

  1. 本地打印数据丢失:我们在读取数据并打印时,发现部分数据没有被完整打印,导致信息不全。
  2. sync: negative WaitGroup counter 错误:在并发处理数据时,WaitGroup 的计数管理出现错误,导致这个计数器减少到负值并引发 panic

问题一:数据丢失的原因与解决

最初,我们通过逐行读取数据并打印,但发现部分数据未被完整地打印出来。数据丢失的原因主要在于数据读取的不完整性,特别是在流式数据传输中,数据可能是分块到达的。如果我们按行读取数据,可能会在某些情况下截断或遗漏部分数据。

为了解决这个问题,我们决定改为逐字节读取数据,并使用一个缓冲区来拼接完整的行。这确保了即使数据是分块到达的,我们也能正确地处理每一行数据,并避免数据丢失。

解决方案:

go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Println("Recovered in reading goroutine:", r)
        }
        close(dataQueue)
        close(transmitQueue)
        wg.Done()
    }()

    buf := make([]byte, 1) // 一次读取一个字节
    for {
        n, err := response.Body.Read(buf)
        if n > 0 {
            char := string(buf[:n])
            buffer += char
            if char == "\n" || char == " " || char == "" {
                dataQueue <- buffer
                transmitQueue <- buffer
                fmt.Println("Read and transmitted line:", buffer)
                buffer = ""
            }
        }
        if err != nil {
            if err != io.EOF {
                log.Println("Error reading from response body:", err)
            }
            break
        }
    }

    if buffer != "" { // 处理最后未发送的内容
        dataQueue <- buffer
        transmitQueue <- buffer
        fmt.Println("Read and transmitted line:", buffer)
    }
}()

这个方法确保每个字符都被处理,最终解决了数据丢失的问题。

问题二:sync: negative WaitGroup counter 错误

在解决了数据丢失的问题后,我们又遇到了 sync: negative WaitGroup counter 错误。这是因为 WaitGroup 的计数器管理不当,导致 Done() 被多次调用或 Add() 的调用次数不匹配。

这个问题通常发生在以下情况下:

  • 过早或多次调用 Done():在某些情况下,可能会多次调用 Done() 或者 Done() 被调用的次数多于 Add() 的次数。
  • WaitGroup 的计数器管理错误:启动了多个 Goroutine,但 Add() 没有正确匹配启动的数量。

解决方案:

为了解决这个问题,我们确保在启动所有 Goroutine 之前,正确地调用 wg.Add(),并在每个 Goroutine 结束时只调用一次 Done()

wg.Add(3) // 启动三个 goroutine

go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Println("Recovered in reading goroutine:", r)
        }
        close(dataQueue)
        close(transmitQueue)
        wg.Done()
    }()

    // 读取数据的逻辑...
}()

go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Println("Recovered in transmitting goroutine:", r)
        }
        wg.Done()
    }()

    // 传输数据的逻辑...
}()

go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Println("Recovered in processing goroutine:", r)
        }
        wg.Done()
    }()

    // 处理数据的逻辑...
}()

wg.Wait() // 等待所有 goroutine 完成

流式处理与实时传输的实现

结合以上两个问题的解决方案,我们最终实现了一个能够流式处理数据、实时传输到客户端,并对数据进行并发处理的系统。这个系统不仅解决了数据丢失和 WaitGroup 管理问题,还保证了高效的实时数据传输。

完整的代码实现:

var wg sync.WaitGroup
var mu sync.Mutex

// 创建一个通道用于数据队列
dataQueue := make(chan string)
transmitQueue := make(chan string)
// 在外部定义 lines
var lines []string

proxy.ModifyResponse = func(response *http.Response) error {
    log.Println("ModifyResponse started")

    var buffer string

    wg.Add(3) // 启动三个 goroutine

    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Println("Recovered in reading goroutine:", r)
            }
            close(dataQueue)
            close(transmitQueue)
            wg.Done()
        }()

        buf := make([]byte, 1) // 一次读取一个字节
        for {
            n, err := response.Body.Read(buf)
            if n > 0 {
                char := string(buf[:n])
                buffer += char
                if char == "\n" || char == " " || char == "" {
                    dataQueue <- buffer
                    transmitQueue <- buffer
                    fmt.Println("Read and transmitted line:", buffer)
                    buffer = ""
                }
            }
            if err != nil {
                if err != io.EOF {
                    log.Println("Error reading from response body:", err)
                }
                break
            }
        }

        if buffer != "" { // 处理最后未发送的内容
            dataQueue <- buffer
            transmitQueue <- buffer
            fmt.Println("Read and transmitted line:", buffer)
        }
    }()

    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Println("Recovered in transmitting goroutine:", r)
            }
            wg.Done()
        }()

        for line := range transmitQueue {
            if _, err := c.Writer.Write([]byte(line)); err != nil {
                log.Println("Error writing to client:", err)
                return
            }
            c.Writer.Flush() // 确保数据立即发送到客户端
            fmt.Println("Real-time transmitted line:", line)
        }
    }()

    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Println("Recovered in processing goroutine:", r)
            }
            wg.Done()
        }()

        for line := range dataQueue {
            processedData := processLine(line)
            mu.Lock()
            lines = append(lines, processedData)
            mu.Unlock()
            fmt.Println("Processed and appended line:", processedData)
        }
    }()

    wg.Wait() // 等待所有 goroutine 完成

    return nil
}

// 实际发送请求到目标服务器
proxy.ServeHTTP(c.Writer, c.Request)

// 将完整的数据拼接为一个字符串
mu.Lock() // 确保在拼接前没有其他 Goroutine 在写入
completeData := strings.Join(lines, "")
fmt.Println("Complete data:", completeData)
mu.Unlock()

总结

通过解决数据丢失和 sync: negative WaitGroup counter 的问题,我们成功实现了一个流式数据处理与实时传输的系统。这个过程不仅帮助我们加深了对 Go 并发编程的理解,也为未来处理类似问题提供了宝贵的经验。如果你在开发中遇到了类似问题,希望这篇文章能为你提供一些帮助。

如果你有任何问题或建议,欢迎在评论区留言讨论。