我在 Go 里用 == 判断一个错误是不是记录不存在,一直好好的,直到某层代码用 %w 把这个错误包装了一下再往上抛,我的 == 判断就突然失效了,把记录不存在当成了未知错误的深度复盘

我要区分记录不存在(返回 404)和真的出错了(返回 500),写成了 if err == sql.ErrNoRows。一直跑得好好的,直到某次重构、中间一层 DAO 在错误往上传时用 fmt.Errorf("query user failed: %w", err) 把原始错误包装了一层。从那以后我的 == 判断突然失效:明明底层就是 sql.ErrNoRows,== 却返回 False,记录不存在被当成未知错误、该 404 的接口返回了 500。复盘才搞懂:Go 1.13 的错误包装 fmt.Errorf("...: %w", err) 会创建一个新 error 包裹原始 err、保留错误链;而 == 比较的是最外层那个 error 对象,包装后最外层是新造的 error、当然不等于里面的 sql.ErrNoRows;我想判断的是这个错误链里本质上是不是 sql.ErrNoRows,可 == 只看最外层、不穿透包装。正解是用 errors.Is(err, sql.ErrNoRows),它会沿包装链一层层往里查、穿透包装看本质。这篇复盘从故障现场讲到错误包装、== 为何失效、errors.Is/As/Unwrap,再到判哨兵用 errors.Is、提取类型用 errors.As、包装用 %w 保留链、定义可判断的哨兵错误的完整正解,以及其他只看表层没穿透包装的坑,和事物在传递中会被层层包装、本质藏在内层、判断要穿透包装追溯根源而非只看最外层的认知。

我在 Go 里用 == 判断一个错误是不是"记录不存在",一直好好的,直到某层代码用 %w 把这个错误包装了一下再往上抛,我的 == 判断就突然失效了,把"记录不存在"当成了未知错误:一次没用 errors.Is 穿透错误包装链、只看最外层身份的深度复盘

那个"明明是记录不存在,却被当成系统错误返回 500"的 bug,根子在我对 Go 错误处理的一个盲区。我有段代码,要区分"记录不存在(正常,返回 404/空)"和"真的出错了(返回 500)",写成了 if err == sql.ErrNoRows { ... }。一直跑得好好的。直到某次重构,中间一层 DAO 代码在错误往上传时,用 fmt.Errorf("query user failed: %w", err) 把原始错误包装(wrap)了一层再返回。从那以后:我那个 err == sql.ErrNoRows 的判断突然失效了——明明底层就是 sql.ErrNoRows,可 == 返回 False,于是"记录不存在"被当成了"未知错误",该返回 404 的接口返回了 500。复盘 Go 的错误包装,我才彻底搞懂,后背发凉:问题出在我用 == 直接比较错误,但错误%w 包装后,最外层已经是一个新的 error 了,== 比不上里面被包裹的原始错误Go 1.13 引入了"错误包装(error wrapping)":fmt.Errorf("...: %w", err) 会创建一个新的 error,它"包裹"着原始的 err(像套了一层壳),保留了错误链;== 比较的是最外层那个 error 对象——包装后,最外层是 fmt.Errorf 新造的 error,它当然不等于里面那个 sql.ErrNoRows;我想判断的是"这个错误链里,本质上是不是 sql.ErrNoRows",可 == 只看最外层、不会穿透包装去看里面;正确的做法,是用 errors.Is(err, sql.ErrNoRows):它会沿着错误包装链一层层往里查,只要链上任何一层是目标错误,就返回 true——它能穿透包装看到本质。根本原因是:错误被 fmt.Errorf 的 %w 包装后,最外层是新 error,== 只比最外层、比不上被包裹的原始哨兵错误;判断错误本质应该用 errors.Is(沿包装链查找)、用 == 会在错误被包装后失效。问题的根,是用 == 比较可能被包装的错误——== 只看最外层身份,错误一旦被 %w 包装就比不上里面的原始错误;应该用 errors.Is 穿透包装链查找。这篇就把这次"错误包装与 errors.Is"的坑,从头到尾复盘一遍。

故障现场:错误被包装后,== 比不上了

问题在于 == 只比最外层,穿不透包装链:

// 我的写法: 用 == 直接比较错误
row := db.QueryRow("SELECT ... WHERE id=?", id)
err := row.Scan(&user)
if err == sql.ErrNoRows {     // 一开始能用; 但错误被包装后就失效!
    return nil, ErrNotFound   // 记录不存在, 返回404
}
if err != nil {
    return nil, err           // 其他错误, 返回500
}

// 某次重构, 中间一层这样包装了错误:
func (d *DAO) GetUser(id int) (*User, error) {
    user, err := d.query(id)
    if err != nil {
        return nil, fmt.Errorf("GetUser(%d) failed: %w", id, err)  // %w 包装! 加了上下文
    }
    return user, nil
}

/*
为什么 == 失效了:
  - fmt.Errorf("...: %w", err) 创建了一个【新的error】, 它"包裹"着原始的err(保留错误链);
  - 包装后, 最外层是 fmt.Errorf 造的新error(类型是 *fmt.wrapError), 不再是 sql.ErrNoRows;
  - == 比较的是【最外层那个error对象】 → 新error == sql.ErrNoRows → false!
  - 所以底层明明是 sql.ErrNoRows, 我的 == 判断却返回false → "记录不存在"被当成未知错误。

错误包装(Go 1.13+)是怎么回事:
  - fmt.Errorf 里用 %w (而非 %v) 包装一个error → 创建保留错误链的新error;
  - 目的: 在错误往上传时, 逐层加上下文("在哪一步失败的"), 又不丢失原始错误;
  - 形成一条"错误链": 最外层(新加的上下文) → ... → 最内层(原始错误如sql.ErrNoRows)。

  判断包装后的错误, 要用 errors 包(能穿透包装链):
  - errors.Is(err, target): 沿链查找, 链上任一层 == target(或其Is方法匹配) → true;
  - errors.As(err, &target): 沿链查找, 找到能赋给target类型的error → 提取出来;
  - errors.Unwrap(err): 取出被包装的下一层error。

★ 核心: == 只比【最外层】error; 错误被 %w 包装后, 最外层是新error, == 比不上里面的原始错误;
  判断错误本质(是不是某个哨兵错误)用 errors.Is(穿透包装链查找), 别用 ==。

看着 errors.Is(err, sql.ErrNoRows) 是 True、而 err == sql.ErrNoRows 是 False 的现场,我又懊恼又恍然:"我一直用 == 比错误,因为之前没人包装它、最外层就是原始错误,碰巧一直对……谁知道有人用 %w 包了一层,最外层就变了,== 只看最外层、根本不知道里面裹着的还是那个错误。"这个坑最隐蔽的地方在于:在错误"没被包装"时一直正确(最外层就是原始错误,==errors.Is 表现一样);它只在错误链上某处被 %w 包装后才失效;而"给错误加包装"是 Go 推荐的好习惯(加上下文便于排查),所以越是规范地包装错误的代码库,这个 == 的坑越容易被踩——你的好习惯反而触发了别处的隐藏 bug下面就来拆解,Go 的错误判断到底该怎么写。

第一件事:搞懂错误包装与 errors.Is/As

我顺着这次事故,把 Go 的错误包装和判断方式彻底理清了。

Go 错误包装是怎么回事? 怎么正确判断错误?

【核心: fmt.Errorf("%w",err)包装出新error、保留错误链; ==只比最外层、包装后比不上里面的原始错误;
   判哨兵错误用errors.Is(穿透链查找)、提取类型用errors.As、包装用%w保留链; 别用==比可能被包装的错误】

1. 错误包装(error wrapping, Go 1.13+):
   - fmt.Errorf("上下文: %w", err): 用 %w 创建一个"包裹着err"的新error, 保留错误链;
   - 目的: 错误往上传时逐层加"在哪步失败"的上下文, 又不丢原始错误;
   - 形成链: 最外层(新上下文) → 中间层 → 最内层(原始错误)。

2. == 为什么不够用:
   - == 比较的是"最外层那个error对象本身";
   - 错误被包装后, 最外层是新造的error, 不再 == 里面的原始哨兵错误;
   - 所以 == 只在"错误没被包装"时碰巧对, 被包装后就失效。

3. 正确的判断工具(errors包, 能穿透包装链):
   - errors.Is(err, target): 沿错误链逐层查找, 任一层等于target(或其Is匹配)就返回true;
     → 判断"是不是某个哨兵错误"(sql.ErrNoRows、io.EOF、context.Canceled、自定义哨兵)用它;
   - errors.As(err, &targetVar): 沿链查找能赋给targetVar类型的error, 找到就提取出来;
     → 判断"是不是某个错误类型"并取出它(如 *os.PathError、自定义错误结构)用它;
   - errors.Unwrap(err): 取出被包装的下一层(一般不直接用, Is/As内部用它遍历)。

4. 怎么写规范的错误:
   - 往上传时加上下文用 %w 包装(保留链, 便于上层用Is/As判断、便于排查);
     return fmt.Errorf("read config %s: %w", path, err)
   - 定义可被判断的哨兵错误: var ErrNotFound = errors.New("not found"); 上层用 errors.Is 判;
   - 定义可携带信息的错误类型: type MyErr struct{...}; 实现Error(); 上层用 errors.As 取;
   - 不想暴露内部错误链给调用方时, 用 %v(不保留链, 只格式化文本)——但就没法Is/As了, 看需求。

5. 本质: 判断一个"可能被层层包装的东西"的本质, 不能只看最外层, 要能穿透包装
   - 错误被包装是为了加上下文, 但它的"本质身份"藏在链的内层;
   - == 只看最外层(表面身份), errors.Is/As 才穿透包装看本质——判断本质要用后者。

一句话: fmt.Errorf的%w包装出保留链的新error、==只比最外层、包装后比不上里面的原始错误; 判哨兵错误用
   errors.Is、提取类型用errors.As(都穿透包装链)、包装用%w保留链; 别用==比可能被包装的错误。

这套认知,是整个坑的根。错误包装:fmt.Errorf("...: %w", err) 创建包裹原始 err 的新 error、保留错误链,目的是逐层加上下文又不丢原始错误== 为什么不够:== 比最外层 error 对象,包装后最外层是新 error、不再等于里面的原始哨兵错误,只在没被包装时碰巧对正确工具:errors.Is(沿链查找哨兵错误)、errors.As(沿链查找并提取错误类型)、errors.Unwrap(取下一层)怎么写规范错误:往上传用 %w 包装加上下文、定义哨兵错误(errors.New)供 Is 判、定义错误类型供 As 取本质:判断"可能被层层包装的东西"的本质,不能只看最外层,要能穿透包装——== 看表面身份,errors.Is/As 穿透看本质一句话:fmt.Errorf 的 %w 包装出保留链的新 error、== 只比最外层、包装后比不上里面的原始错误;判哨兵错误用 errors.Is、提取类型用 errors.As(都穿透包装链)、包装用 %w 保留链;别用 == 比可能被包装的错误。

第二件事:正解——用 errors.Is/As 穿透包装链判断

知道了 == 穿不透包装,正解就清楚了:判断错误本质用 errors.Is/As,包装用 %w。

// 正解1: 判断"是不是某个哨兵错误"用 errors.Is(穿透包装链)——本次该做的
err := row.Scan(&user)
if errors.Is(err, sql.ErrNoRows) {     // ✓ 沿链查找, 不管包装几层都能识别
    return nil, ErrNotFound            // 记录不存在, 404
}
if err != nil {
    return nil, err                    // 其他错误, 500
}

// 正解2: 提取"某个错误类型"并取出它用 errors.As
var pathErr *os.PathError
if errors.As(err, &pathErr) {          // ✓ 沿链找到 *os.PathError 类型, 提取出来
    log.Printf("路径出错: %s", pathErr.Path)
}

// 正解3: 往上传错误时用 %w 包装, 保留链(便于上层Is/As + 排查)
func (d *DAO) GetUser(id int) (*User, error) {
    user, err := d.query(id)
    if err != nil {
        return nil, fmt.Errorf("GetUser(%d): %w", id, err)  // ✓ %w保留链, 上层仍能Is出sql.ErrNoRows
    }
    return user, nil
}

// 正解4: 定义可被判断的哨兵错误 / 错误类型
var ErrNotFound = errors.New("not found")            // 哨兵: 上层用 errors.Is(err, ErrNotFound)
type ValidationError struct{ Field string }          // 错误类型: 上层用 errors.As 取出 Field
func (e *ValidationError) Error() string { return "invalid: " + e.Field }

// 正解5: 什么时候用 %v 而非 %w
//   - %w: 保留错误链(上层能Is/As、能看到完整链), 默认推荐;
//   - %v: 只格式化文本、不保留链(故意不暴露内部错误给调用方时用); 但上层就没法Is/As了。

// 反例(别这样):
// if err == sql.ErrNoRows { }     // ✗ 错误一旦被%w包装就失效
// if err.Error() == "not found" { } // ✗ 比较错误字符串! 脆弱(文案一改就失效), 别这么干

// 核心: 判哨兵错误用 errors.Is、提取错误类型用 errors.As(都穿透包装链); 往上传用 %w 保留链;
//   定义哨兵错误(errors.New)和错误类型供上层判断; 别用 == 或比字符串判可能被包装的错误。

这套正解的关键,是用能"穿透包装链"的 errors.Is/As 判断错误本质,而非只看最外层的 == 判哨兵错误用 errors.Is:沿链查找,不管包装几层都能识别 sql.ErrNoRows——这正是本次该改的。提取错误类型用 errors.As:沿链找到目标类型并取出来用。往上传用 %w 包装:保留链、加上下文,上层仍能 Is/As、也便于排查。定义哨兵错误和错误类型:供上层判断。%w vs %v:%w 保留链(推荐),%v 不保留(故意不暴露内部错误时用,但上层就没法 Is/As)。而要避开的反例,是 err == sql.ErrNoRows(被包装就失效)和 err.Error() == "..."(比字符串,文案一改就崩)。

第三件事:其他几个"只看表层、没穿透包装"的坑

顺着这次错误包装,我把"对被包装/嵌套的东西只看表层"的几类坑也一并理了:

几类"只看表层、没穿透包装/嵌套"的坑:

坑1: 比较错误字符串(err.Error() == "xxx")——文案一改就失效, 还可能被包装的上下文污染;
   正解: 用哨兵错误 + errors.Is, 别依赖错误文本。

坑2: panic/recover 跨包装——recover拿到的可能是被包装/重新抛的, 要看清原始;
   正解: 规范地传递错误信息, 别丢失原始。

坑3: 类型断言只断最外层——接口值被装饰器/包装类型套了一层, 直接断言原始类型失败;
   正解: 沿装饰链解包, 或用支持解包的接口。

坑4: HTTP/RPC错误码只看最外层——下游真实错误被网关/中间层包了一层通用错误码, 丢了原因;
   正解: 透传或保留原始错误码/原因, 别一律包成500。

坑5: 异常被catch后重新抛、丢了cause(Java的 throw new XxxException(e) vs 不传e)——
   正解: 包装异常时带上cause(初始化cause), 保留异常链, 便于定位根因。

坑6: 日志/监控只记最外层错误信息——丢了内层根因, 排查时只看到"GetUser failed"看不到sql.ErrNoRows;
   正解: 用 %+v / 打印完整错误链, 记录根因。

共同的根: 一个东西在传递/处理过程中常被"包装、装饰、转换"层层套上外壳(加上下文、转格式、换类型);
   它的"本质/根因"被藏在了内层; 如果判断/处理时只看"最外层的表面", 就会错判、丢失根因;
   要用"能穿透包装、沿链/沿层查找本质"的方式去判断和处理, 而非只认最外面那一层。

这些坑看似不同,根却是同一个:一个东西在传递/处理过程中常被"包装、装饰、转换"层层套上外壳(加上下文、转格式、换类型),它的本质/根因被藏在了内层;判断/处理时只看"最外层的表面",就会错判、丢失根因认清这个根("对被包装的东西要穿透外壳看本质/根因"),才不会被层层包装迷惑。

第四件事:错误判断方式 / 包装动词——两张对照表

我把几种错误判断方式、以及格式化动词,整理成对照表,贴在了团队的 Go 规范里:

方式 能穿透包装链吗 用途
err == target ✗ 只比最外层 只适合确定没被包装时
errors.Is(err, target) ✓ 沿链查找 判是不是某哨兵错误
errors.As(err, &v) ✓ 沿链查找 判类型并提取出来
errors.Unwrap(err) 取下一层 手动遍历(少用)
err.Error() == "xxx" ✗ 且脆弱 别用(比字符串)
fmt.Errorf 动词 是否保留错误链 影响
%w 保留(包装) 上层可 Is/As,推荐
%v 不保留(只格式化) 上层无法 Is/As,故意隐藏内部时用
%s 不保留 同 %v(取 Error() 文本)

这两张表的核心,第一张是只有 errors.Is/As 能穿透包装链,== 和比字符串都只看表层、错误一旦被包装就失效;第二张是往上传错误想让上层能判断,就用 %w 保留链。记住一条:判可能经过多层传递的错误,一律用 errors.Is/As;包装错误一律用 %w

第五件事:关于 Go 错误处理的几组容易想当然的认知

这次事故也让我厘清了几组关于错误判断的、容易想当然的概念:

直觉以为 实际上
err == sql.ErrNoRows 一直可靠 错误被 %w 包装后就比不上了
== 和 errors.Is 差不多 == 只比最外层,Is 穿透整条链
包装错误是多此一举 加上下文便于排查,是好习惯
%w 和 %v 包装效果一样 %w 保留链可 Is/As,%v 不保留
比 err.Error() 字符串也行 脆弱,文案/上下文一变就失效
== 一直没出错说明它对 是错误恰好没被包装,被包装就错
判错误只看最外层就够 本质/根因藏在链的内层,要穿透

这张表里,我栽的是第一行和第六行:== 比 sql.ErrNoRows、且"一直没出错"让我以为它可靠,没意识到那只是因为错误一直没被包装;一旦有人规范地用 %w 加了上下文,== 就失效了厘清这些,核心是一个意识:Go 的错误可能被层层 %w 包装(为了加上下文,这是好习惯);判断一个错误"本质上是什么",必须用能穿透包装链的 errors.Is/As,而非只看最外层的 ==——别让自己(或同事)规范的包装,触发了 == 的隐藏失效。

第六件事:判断 / 传递错误时,我现在的自检习惯

现在每当我要判断或往上传一个 error,我都会先按这张图问自己:

这张图的精髓,是"判错误用 errors.Is/As 别用==、传错误用 %w 保留链"先分判断还是传递、判断就用 Is/As 穿透包装、传递就用 %w 保留链这套习惯,让我从"随手 == 比错误"变成了"用 errors.Is 穿透包装判本质"——核心始终是:fmt.Errorf 的 %w 包装出保留链的新 error、== 只比最外层、包装后比不上原始错误;判哨兵用 errors.Is、提取类型用 errors.As、包装用 %w 保留链;别用 == 比可能被包装的错误。

我立下的几条规矩

这场"错误被包装后 == 失效、404 变 500"的事故,换来了我写 Go 错误处理时,刻进骨子里的几条铁律:

  1. fmt.Errorf 的 %w 会创建包裹原始错误的新 error、保留错误链;== 只比最外层。
  2. 错误被 %w 包装后,== 就比不上里面的原始哨兵错误了(只在没被包装时碰巧对)。
  3. 判断"是不是某个哨兵错误"用 errors.Is(穿透包装链查找),别用 ==。
  4. 判断"是不是某错误类型"并取出它用 errors.As。
  5. 往上传错误加上下文用 %w 包装,保留链(便于上层 Is/As 和排查)。
  6. 定义哨兵错误(errors.New)和错误类型供上层判断;别用比 err.Error() 字符串(脆弱)。
  7. %w 保留链(推荐)、%v 不保留(故意隐藏内部错误时用,但上层就没法 Is/As)。

附:打印完整错误链、定义可判断的错误

借这次的坑,我整理了团队"错误处理"的一套小约定:用 %+v 打印完整链便于排查,定义清晰的哨兵错误和错误类型供上层判断。

// 1. 定义可被 errors.Is 判断的哨兵错误(包级别导出)
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)

// 2. 定义可被 errors.As 提取信息的错误类型
type ValidationError struct {
    Field string
    Msg   string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Msg)
}

// 3. 往上传时用 %w 包装、加上下文(保留链)
func getUser(id int) (*User, error) {
    u, err := repo.Find(id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, fmt.Errorf("getUser(%d): %w", id, ErrNotFound)  // 转成业务哨兵, 仍可Is
    }
    if err != nil {
        return nil, fmt.Errorf("getUser(%d): %w", id, err)          // 其他错误包装上下文
    }
    return u, nil
}

// 4. 上层统一判断 + 记录完整链
func handler(id int) {
    u, err := getUser(id)
    switch {
    case errors.Is(err, ErrNotFound):
        respond(404)                                    // 穿透包装, 识别业务错误
    case err != nil:
        log.Printf("internal error: %+v", err)          // %+v(配合带堆栈的错误库)打印完整链/根因
        respond(500)
    default:
        respond(200, u)
    }
}

// 原则: 定义清晰的哨兵错误/错误类型供上层 Is/As 判断; 传递用 %w 保留链;
//   排查时打印完整错误链(根因), 而非只看最外层的一句上下文 —— 让"判断"和"排查"都能直达本质。

这套约定的核心,是让错误从"产生"到"判断"到"排查"全程都能直达本质:产生时定义清晰的哨兵/类型,传递时用 %w 保留链,判断时用 errors.Is/As 穿透包装,排查时用 %+v 打印完整链看根因。无论中间被包装了多少层上下文,我永远有办法穿透它们、看到那个最里面的、真正的错误。

写在最后

回头看,这场由"错误被包装后 == 失效"引发的、把记录不存在误判成系统错误的事故,真正教给我的,远不止"判错误用 errors.Is"这一个技巧。它让我对"一个东西在传递的过程中, 常常会被'层层包装'(加上外壳、加上上下文); 而它'本质上是什么', 被藏在了这些外壳的里面; 如果你判断它时只看最外面那层壳, 就会被外壳骗了, 看不到里面真正的它",有了一次刻骨的体会。我栽跟头,是因为我用"看最外层"的方式(==), 去判断一个"本质藏在内层"的东西——错误被 %w 包了一层上下文后, 它最外面那层壳已经不是 sql.ErrNoRows 了, 而是 "GetUser failed" 这个新壳;我的 == 只认这层壳, 一看"不是 sql.ErrNoRows 啊", 就判它不是——可它的'内核'明明还是 sql.ErrNoRows, 只是被一层善意的上下文给包住了;我把"表面的那层包装"当成了"它的全部身份", 而没有去穿透包装、看它内在的本质这让我领悟到一个关于"包装与本质、表层与内核"的深刻认知:一个事物在被传递、转述、处理的过程中, 往往会被不断地包装、转换、加上外层的修饰和上下文(这常常是善意且有用的——比如给错误加上"在哪一步出错"的线索); 但这也意味着, 它的"本质/根源"会被一层层外壳包裹起来、离表面越来越远;所以判断一个'可能被层层包装过的东西'到底是什么, 绝不能只看它最外层的样子, 而要有能力'穿透包装、层层解开、直达内核';这对一切"经过多手传递/封装"的东西都成立——错误的根因、一条转述了好几手的消息、一个被多层中间商加价的商品、一个被层层汇报粉饰过的真实情况: 看表层会被层层包装误导, 看本质要有穿透包装、追溯根源的耐心和方法这给了我一种面对"被包装之物"时的清醒:每当我要判断或依赖一个"可能经过多层传递/包装"的东西时,要意识到"我看到的最外层, 可能只是层层包装中最新的那一层, 不是它的本质"——用"能穿透包装、沿着链条追溯到根源"的方式(errors.Is/As 之于错误)去看它的本质, 而不是只认最外面那层壳;"对被层层包装的东西, 穿透外壳判本质、沿链追溯根因, 而非只看最外层",是避免被表层包装误导的关键认清事物在传递中会被层层包装、本质藏在内层、判断要穿透包装追溯根源而非只看最外层——这,是我用一次 Go 错误包装的事故,换来的、关于 Go、也关于如何看透包装直达本质的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次判断一个 error 时,把那个 == 换成 errors.Is、让判断能穿透同事善意加上的那层包装,那我对着那个"记录不存在却返回 500"的 bug 排查的这段时间,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我在 JavaScript 里用 setTimeout 0 想让一段逻辑稍后立即执行来排个时序,结果它总是排在所有 Promise 回调的后面执行,输出顺序跟我写的顺序完全对不上,因为微任务永远先于宏任务的深度复盘

2026-6-3 1:48:29

技术教程

我在 Java 的 finally 块里写了个 return 做收尾,结果 try 块里本该抛出的异常凭空消失了、本该返回的值也被悄悄换掉了,一个本该报错的方法竟然正常返回:一次 finally 里 return 吞掉了 try 结果的深度复盘

2026-6-3 1:59:27

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索