Golang API使用goroutine与context取消时出现阻塞问题
问题诊断与修复
核心问题分析
你的代码出现阻塞的根本原因是错误处理逻辑不完整,加上通道使用不当导致goroutine泄漏:
- 错误发生后未终止函数执行:在
StoreFiles中,当某个步骤出错时,你向errorChan发送错误并调用cancel(),但没有立即return退出函数。这会导致后续循环继续执行,可能多次向errorChan发送错误——而errorChan的缓冲只有1,第二次发送时会直接阻塞,导致goroutine无法退出。 - 上下文传递错误:
StoreFiles的第一个参数是*context.Context,但Go的上下文是值类型,不需要传递指针,这种写法不仅冗余,还可能引发意外问题。 - 潜在的资源泄漏:即使错误发送成功,后续代码依然会执行到
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 }
关键修复点说明
- 错误后立即return:每次发送错误到
errorChan后,立即退出函数,避免后续代码执行导致的重复发送错误或无效逻辑。 - 上下文传递改为值类型:将
*context.Context改为context.Context,符合Go的最佳实践。 - 添加defer cancel():确保函数无论正常还是异常退出,都会取消上下文,释放相关资源。
- 循环中检查上下文状态:在每次循环开始前检查上下文是否已取消,避免在上下文取消后继续执行无效操作。
- 变量名优化:将
fileName改为fileNameParts,避免语义混淆。
内容的提问来源于stack exchange,提问作者Ameya Salagre
相关产品推荐
相关产品推荐

