如果上下文被取消,则终止函数执行

Terminating function execution if a context is cancelled

我有这个当前函数,它最初不是上下文感知的。

func (s *Service) ChunkUpload(r *multipart.Reader) error {
    chunk, err := s.parseChunk(r)
    if err != nil {
        return fmt.Errorf("failed parsing chunk %w", err)
    }

    if err := os.MkdirAll(chunk.UploadDir, 02750); err != nil {
        return err
    }

    if err := s.saveChunk(chunk); err != nil {
        return fmt.Errorf("failed saving chunk %w", err)
    }

    return nil
}

我更新了它的方法调用,现在将 context.Context 作为第一个参数。我的主要目标是在上下文被取消后立即终止并 return 函数。

我最初的实现是这样的。

func (s *Service) ChunkUpload(ctx context.Context, r *multipart.Reader) error {
    errCh := make(chan error)

    go func() {
        chunk, err := s.parseChunk(r)
        if err != nil {
            errCh <- fmt.Errorf("failed parsing chunk %w", err)
            return
        }

        if err := os.MkdirAll(chunk.UploadDir, 02750); err != nil {
            errCh <- err
            return
        }

        if err := s.saveChunk(chunk); err != nil {
            errCh <- fmt.Errorf("failed saving chunk %w", err)
            return
        }
    }()

    select {
    case err := <-errCh:
        return err
    case <-ctx.Done():
        return ctx.Err()
    }
}

但是,当我考虑代码的执行时,我意识到这并没有实现我的目标。由于所有函数的逻辑都在一个单独的 go 例程中,即使上下文被取消并且我 return ChunkUpload 提前,go 例程中的代码将继续执行,因此与原始代码没有真正的区别。

下一个虽然可以将上下文传递给所有内部函数,如 s.parseChunks.saveChunk,但这个选项似乎也不正确,因为我需要在每个函数中实现取消。将这个原始函数重构为上下文感知并在上下文被取消后立即终止的正确方法是什么?

函数调用和 goroutines 不能从调用者终止,函数和 goroutines 必须支持取消,通常通过 context.Context 值或 done 通道。

在任何一种情况下,函数都负责检查/监视上下文,如果请求取消(当上下文的完成通道关闭时),return 及早。没有更简单/自动的方法。

如果任务在循环中执行代码,一个方便的解决方案是在每次迭代中检查 done 通道,并 return 如果它已关闭。如果任务是一个 "monolith",实施者负责使用/插入 "checkpoints",如果请求取消,任务可以合理地提前中止。

检查 done 通道是否关闭的简单方法是使用非阻塞 select,例如:

select {
case <-ctx.Done():
    // Abort / return early
    return
default:
}

当任务使用其他通道操作时必须小心,因为它们可能以不确定的方式阻塞。这些选择也应包括 ctx.Done() 频道:

select {
case v := <- someChannel:
    // Do something with v
case <-ctx.Done():
    // Abort / return early
    return
}

还要小心,因为如果上述从 someChannel 接收的信息从不阻塞,则不能保证取消会得到正确处理,因为如果多个通信可以在一个 select 中进行,则随机选择一个(并且不能保证 <-ctx.Done() 会被选中)。在这种情况下,您可以结合以上 2 项:首先对取消进行非阻塞检查,然后使用 select 和您的频道操作 取消监控。

当我们谈到取消时,我们谈到了一个很长的运行函数或者一个重复多次的块,比如http.Serve()

对于您的情况,假设 saveChunk 将花费几秒到 运行,并且您想在保存时取消。所以我们可以把chunk拆分成几块,一块一块保存,每块后。

for i:=0;i<n;i++{
    select {
        case err := <- s.saveChunk(chunk[i]):
        {
             if err != nil {
                  fmt.Errorf("failed saving chunk %w", err)
                  return
              }
        }
        case <-ctx.Done():
              return
    }
}