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

Golang API使用goroutine与context取消时出现阻塞问题

问题诊断与修复

核心问题分析

你的代码出现阻塞的根本原因是错误处理逻辑不完整,加上通道使用不当导致goroutine泄漏:

  1. 错误发生后未终止函数执行:在StoreFiles中,当某个步骤出错时,你向errorChan发送错误并调用cancel(),但没有立即return退出函数。这会导致后续循环继续执行,可能多次向errorChan发送错误——而errorChan的缓冲只有1,第二次发送时会直接阻塞,导致goroutine无法退出。
  2. 上下文传递错误:StoreFiles的第一个参数是*context.Context,但Go的上下文是值类型,不需要传递指针,这种写法不仅冗余,还可能引发意外问题。
  3. 潜在的资源泄漏:即使错误发送成功,后续代码依然会执行到attachmentChan <- attachmentPaths,如果main函数已经通过errorChan接收错误并返回,虽然缓冲通道不会阻塞,但多次调用后会积累未退出的goroutine,最终导致资源耗尽。

是否推荐传递两个通道?

传递两个通道(结果+错误)是可行的,但不是最简洁的方案。更常用的方式是定义一个包含结果和错误的结构体,用单个通道传递:

type StoreResult struct {
    Paths []string
    Err   error
}

不过两个通道的写法本身没有问题,只要处理逻辑正确就可以正常工作。

修复后的代码

修复main函数

func main() {
    attachmentChan := make(chan []string, 1)
    errorChan := make(chan error, 1)
    var attachmentPaths []string
    // 使用context.Background()创建根上下文
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 确保上下文最终被取消,避免泄漏

    go StoreFiles(ctx, cancel, body, tenant, bucketName, attachmentChan, errorChan)
    select {
    case err := <-errorChan:
        if err != nil {
            return response, err
        }
    case attachmentPaths = <-attachmentChan:
    }
}

修复StoreFiles函数

func StoreFiles(ctx context.Context, cancel context.CancelFunc, body EmailDto, tenant string, bucketName string, attachmentChan chan []string, errorChan chan error) {
    defer cancel() // 函数退出时取消上下文,释放资源
    currentDate := GetFormattedDate()
    saveFolder := bucketName + "/" + tenant + "/" + currentDate

    extensions := strings.Split(constants.AllowedExtensions, ",")
    allowedExtensions := make(map[string]struct{})
    for _, ext := range extensions {
        allowedExtensions[ext] = struct{}{}
    }

    var attachmentPaths []string

    for _, attachment := range body.Attachments {
        // 提前检查上下文是否已取消,避免无效操作
        select {
        case <-ctx.Done():
            return
        default:
        }

        // Decode base64 content
        decodedContent, err := base64.StdEncoding.DecodeString(attachment.Content)
        if err != nil {
            logger.GetLogger().Errorln(err)
            errorChan <- err
            return // 发送错误后立即退出
        }

        ext := GetFileExtension(attachment.Filename)
        trimmedAttachmentFileName := strings.TrimLeft(attachment.Filename, ".")
        fileNameParts := strings.Split(trimmedAttachmentFileName, ".")
        if len(fileNameParts) < 2 {
            err := utils.BadRequestError("Invalid file name: " + attachment.Filename)
            errorChan <- err
            return
        }

        if _, ok := allowedExtensions[ext]; !ok {
            err := utils.BadRequestError("Invalid file extension: " + ext)
            errorChan <- err
            return
        }

        randomString := random18DigitGenerator()
        fileNameString := strings.TrimSpace(attachment.Filename) + constants.FileNameSeperator + randomString
        filePath := filepath.Join(saveFolder, fileNameString)

        err = WriteToGCS(ctx, filePath, decodedContent)
        if err != nil {
            logger.GetLogger().Errorln(err)
            errorChan <- errors.New("Error while saving file")
            return
        }

        logger.GetLogger().Infoln("File stored on GCS :", filePath)
        attachmentPaths = append(attachmentPaths, filePath)
    }
    attachmentChan <- attachmentPaths
}

关键修复点说明

  1. 错误后立即return:每次发送错误到errorChan后,立即退出函数,避免后续代码执行导致的重复发送错误或无效逻辑。
  2. 上下文传递改为值类型:将*context.Context改为context.Context,符合Go的最佳实践。
  3. 添加defer cancel():确保函数无论正常还是异常退出,都会取消上下文,释放相关资源。
  4. 循环中检查上下文状态:在每次循环开始前检查上下文是否已取消,避免在上下文取消后继续执行无效操作。
  5. 变量名优化:将fileName改为fileNameParts,避免语义混淆。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 04:36:12