文章

阶段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)
  • 进程不崩溃(测试能跑完)