阶段1 第1课:RunTasks 骨架
RunTasks 骨架(填空版)
结构已搭好、可直接编译。唯一要你填的是 goroutine 里的 recover 隔离块(把 panic 转 error + cancel), 这是本课易错点 1 的实战落点。填完用
go test -race验收。
conc.go
package conc
import (
"context"
"errors"
"fmt"
"sync"
)
// RunTasks 并发执行 tasks。任意任务 error 或 panic 即取消其余任务,
// 聚合所有错误一并返回;单个 panic 不会让进程崩溃。
func RunTasks(tasks []func() error) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 必须 defer:释放 timer,且所有 goroutine 结束后关闭 ctx
var (
wg sync.WaitGroup
mu sync.Mutex
errs []error
)
for _, task := range tasks {
// 前面已有任务失败 → ctx 已取消 → 跳过剩余任务(cancel 传播的体现)
if ctx.Err() != nil {
mu.Lock()
errs = append(errs, fmt.Errorf("task skipped: %w", ctx.Err()))
mu.Unlock()
continue
}
wg.Add(1)
go func(t func() error) {
defer wg.Done()
// ===== TODO:在此处补全 recover 隔离 =====
// 要求:
// 1. defer 一个匿名函数,函数体内直接调用 recover()
// 2. 若 r := recover(); r != nil:
// - 把 panic 值转成 error 追加到 errs(加锁)
// - 调用 cancel() 通知取消其他任务
// 3. 注意 recover() 必须在 defer 的函数体内「直接」调用才有效
// =========================================
if err := t(); err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
cancel() // 任一失败取消其余
}
}(task)
}
wg.Wait()
if len(errs) == 0 {
return nil
}
return errors.Join(errs...)
}
conc_test.go
package conc
import (
"strings"
"testing"
)
func TestRunTasks(t *testing.T) {
tasks := []func() error{
func() error { return nil },
func() error { panic("boom") }, // 第 2 个任务 panic
func() error { return nil },
}
err := RunTasks(tasks)
// 断言 1:错误被聚合(panic 转成的 error 也在内)
if err == nil {
t.Fatal("expected aggregated error, got nil")
}
// 断言 2:panic 内容被保留
if !strings.Contains(err.Error(), "boom") {
t.Fatalf("expected panic 'boom' in error, got: %v", err)
}
// 断言 3:能跑到这里,说明进程没崩(recover 生效)
}
填空验收点(填完自测)
- recover 在
defer函数体内直接调用 - panic 被转成 error 并
cancel() -
go test -race通过(无 data race) - 进程不崩溃(测试能跑完)