我在 Go 的循环里用 defer 关闭打开的文件,自以为每轮都妥妥地释放了,结果批量处理几千个文件时却报了 too many open files,我排查了大半天的复盘

我批量处理文件,在循环里逐个 os.Open 后 defer f.Close(),自以为每轮就关、很安心,少量文件也确实没事。可处理几千个文件时程序跑到一半崩了,报 too many open files。我每个都 defer Close 了啊,怎么会打开过多?深挖才懂我搞错了 defer 的执行时机:defer 是"函数级"延迟,在所在函数即将 return 时才执行,不是循环每轮结束时——循环里的几千个 defer 只是被登记压栈、全部堆积,要等 processAll 函数返回才一股脑(后进先出)执行;循环跑完前所有文件一个没关、句柄累积,超过 ulimit 上限就崩。这篇从 defer 的执行时机与作用域(函数级/LIFO/参数注册时求值)讲起,到把循环体抽成小函数让 defer 每轮返回/匿名函数包裹/手动 Close 的正解、几种写法对比、看似一次性实则累积的资源坑、几行代码看清 defer 何时执行,以及那句最戳心的——越方便的特性越要懂它的边界,真正的熟练不是会用而是懂得它什么时候不该用。

我在 Go 的循环里用 defer 关闭打开的文件,自以为每轮都妥妥地释放了,结果批量处理几千个文件时却报了 too many open files,我排查了大半天的复盘

这是一个让我对 Go 的 defer 真正理解透彻的故事。我写了个函数,要批量处理一大堆文件:在一个循环里,逐个打开文件、处理、然后用 defer file.Close() 来确保关闭。defer 这东西多优雅啊——"打开后随手 defer 一下,就再也不用操心关闭了",我一直这么用,也一直觉得很安心。处理少量文件时,它确实毫无问题。可当我拿它去批量处理几千个文件时,程序跑到一半就崩了,报了一个让我猝不及防的错:too many open files(打开的文件过多)。

我当时非常困惑:我每个文件不都 defer Close() 了吗?用完就该关了啊,怎么会"打开的文件过多"?难道 defer 没生效?我顺着这个现象深挖,才终于揭开真相,补上了我对 Go defer 一个最关键的认知漏洞:问题的核心,是我彻底搞错了 defer 的执行时机。我一直想当然地以为,在循环里写的 defer file.Close(),会在每一轮循环结束时就执行、把当轮的文件关掉;可真相是:defer 的执行时机,是"它所在的函数即将返回时",而不是"它所在的代码块/循环结束时"。也就是说,defer函数级的延迟,不是块级、更不是循环级的。所以,我那个写在循环里的 defer Close(),根本不会在每轮关闭文件;它们只是被一个一个地"登记"了下来,全都堆积着,要等到整个外层函数跑完、即将返回的那一刻,才会(以后进先出的顺序)一股脑地全部执行。这就意味着:在我那个处理几千个文件的循环跑完之前,所有打开过的文件,一个都没关,它们的句柄全都开着、累积着;而操作系统对单个进程能同时打开的文件数,是有限制的(ulimit -n);当我打开的文件数,超过了这个上限,系统就拒绝再打开新文件,抛出了 too many open files我这才痛彻地明白:defer 虽然优雅,但它"延迟到函数返回"的本质,意味着在循环里用它来释放"每轮都需要及时释放"的资源,是一个致命的误用;因为那些资源,会一直累积到函数结束,而不是用完即放defer 的作用域,是函数,而非循环——这个看似微小的区别,在处理大批量资源时,就是"句柄耗尽、程序崩溃"和"稳定运行"之间的天壤之别。

故障现场:循环里的 defer 全堆到函数返回时才执行

我把这个"句柄耗尽"的现场,用代码摊开给你看:

// ✗ 灾难: 循环里 defer Close, 全堆到函数返回时才关, 句柄累积耗尽
func processAll(paths []string) error {
    for _, path := range paths {        // 假设 paths 有几千个
        f, err := os.Open(path)
        if err != nil {
            return err
        }
        defer f.Close()                 // ✗ 不是每轮关! 而是登记下来, 等 processAll 返回才关
        process(f)
    }
    return nil
    // ✗ 直到这里(函数返回), 几千个 defer f.Close() 才一股脑执行。
    //   在此之前, 几千个文件句柄全开着 → too many open files!
}

// 为什么会句柄耗尽?
//   - defer 的执行时机 = "所在函数即将返回时", 不是"循环每轮结束时"。
//   - defer 是"函数级"延迟, 不是"块级/循环级"。
//   - 循环里的 defer 只是被"登记", 全部堆积, 等函数返回才(后进先出)执行。
//   - 所以循环跑完前, 所有文件一个没关, 句柄累积。

// 操作系统限制:
//   - 单个进程能同时打开的文件数有上限(ulimit -n, 常见 1024)。
//   - 累积打开几千个 → 超限 → open 失败: too many open files。

// 验证: 在循环里打印当前打开数, 会看到只增不减(直到函数结束才骤降)。

// 同样的坑: 循环里 defer conn.Close() / defer mu.Unlock() / defer rows.Close()
//   —— 任何"循环里 defer 释放资源"都可能累积。

// 根因: defer 是函数级延迟, 循环里的 defer 全堆到函数返回才执行, 资源累积不及时释放。

看着这段代码,我才算彻底想明白了这场"句柄耗尽"的根源。问题的核心,是我搞错了 defer 的执行时机:defer 的执行,是在"它所在的函数即将返回时",不是"循环每轮结束时";它是"函数级"延迟,不是"块级/循环级"所以,循环里的几千个 defer f.Close(),只是被一个个"登记"了下来、全部堆积着,要等到 processAll 函数返回时,才(以后进先出的顺序)一股脑执行;在循环跑完之前,所有文件一个都没关,句柄不断累积而操作系统对单个进程能同时打开的文件数,是有上限的(ulimit -n,常见 1024);累积打开几千个,就超限了,open 失败、抛出 too many open files这个坑,不只出现在文件:循环里 defer conn.Close()defer mu.Unlock()defer rows.Close()——任何"循环里 defer 释放资源"的写法,都可能累积。归根结底:defer 是函数级延迟,循环里的 defer 全堆到函数返回才执行,导致资源累积、不及时释放——这,就是根源。

第一件事:搞懂 defer 的执行时机与作用域

定位到根源,我必须把 defer 的执行时机和作用域,从根上彻底搞清楚:

defer: 延迟到"所在函数返回时"执行, 是函数级, 不是块级

# defer 的执行时机:
#   - defer 注册一个调用, 它在"当前函数即将 return 时"才执行。
#   - 不是"当前 for/if/{} 代码块结束时"! defer 不认识块, 只认识函数。
#   - 多个 defer: 后进先出(LIFO), 像栈一样, 最后 defer 的最先执行。

# 所以循环里的坑:
#   - for { ... defer x.Close() }
#   - 每轮的 defer 都被"压栈登记", 但都不执行。
#   - 直到整个函数 return, 才把栈里所有 defer 依次弹出执行。
#   - 循环期间资源全开着 → 累积 → 耗尽。

# defer 另一个易错点: 参数在"注册时"就求值!
#   - defer fmt.Println(i)  → i 的值在 defer 这行就定了, 不是函数返回时取。
#   - defer func(){ ... i ... }()  → 闭包里读 i 是函数返回时的值(注意区分)。

# defer 适合什么? 不适合什么?
#   ✓ 适合: 在"单个函数作用域内", 配对地获取/释放资源(打开就 defer 关)。
#     —— 典型: 函数开头 open, 紧跟 defer close, 函数结束自动关。
#   ✗ 不适合: 在"循环里"释放"每轮就该释放"的资源(会累积到函数结束)。

# 关键认知: defer 的作用域是"函数", 不是"循环/块"。
#   - 想"每轮就释放" → 必须让 defer 处在一个"每轮都会返回的函数"里。

# 核心: defer 延迟到所在函数返回时执行(函数级、LIFO、参数注册时求值);
#   循环里 defer 会累积到函数结束, 不适合释放每轮需及时释放的资源。

原理终于清晰了。defer执行时机:它注册一个调用,在"当前函数即将 return 时"才执行——不是"当前 for/if/{} 代码块结束时"!defer 不认识"块",只认识"函数";多个 defer后进先出(LIFO),像栈一样。所以循环里的坑就清楚了:for { ... defer x.Close() },每轮的 defer 都被"压栈登记"但不执行,直到整个函数 return,才把栈里所有 defer 依次弹出执行;循环期间资源全开着、累积、耗尽这里还有 defer 另一个易错点:参数在"注册时"就求值!defer fmt.Println(i)idefer 这行就定了(不是函数返回时取);而 defer func(){ ... i ... }() 闭包里读的 i 是函数返回时的值(注意区分)。defer 适合什么?✓ 适合:在"单个函数作用域内",配对地获取/释放资源(函数开头 open、紧跟 defer close,函数结束自动关);✗ 不适合:在"循环里"释放"每轮就该释放"的资源(会累积到函数结束)。由此,我刻下一个关键认知:defer 的作用域是"函数",不是"循环/块";想"每轮就释放",必须让 defer 处在一个"每轮都会返回的函数"里。归根结底:defer 延迟到所在函数返回时执行(函数级、LIFO、参数注册时求值);循环里 defer 会累积到函数结束,不适合释放每轮需及时释放的资源。

第二件事:正解——把循环体抽成函数,让 defer 每轮返回

搞懂了原理,正解就清晰了:既然 defer 是"函数级"的,那就把循环体,抽成一个独立的小函数——这样,每一轮调用这个小函数、它一返回,defer 就执行了,资源用完即放。

// ✓ 正解一: 把循环体抽成独立函数, defer 随小函数每轮返回(推荐!)
func processAll(paths []string) error {
    for _, path := range paths {
        if err := processOne(path); err != nil {   // ✓ 每轮调用一个小函数
            return err
        }
        // ✓ processOne 一返回, 它里面的 defer f.Close() 就执行了 → 当轮就关!
    }
    return nil
}

func processOne(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()       // ✓ 函数级 defer: processOne 返回时关 = 每轮就关
    process(f)
    return nil
}

// ✓ 正解二: 用匿名函数(闭包)包裹循环体, 立即调用
func processAll2(paths []string) error {
    for _, path := range paths {
        err := func() error {           // ✓ 立即执行的匿名函数, 形成一个函数作用域
            f, err := os.Open(path)
            if err != nil {
                return err
            }
            defer f.Close()             // ✓ 这个匿名函数返回时就关
            process(f)
            return nil
        }()
        if err != nil {
            return err
        }
    }
    return nil
}

// ✓ 正解三: 干脆不用 defer, 在循环里手动、及时地关
func processAll3(paths []string) error {
    for _, path := range paths {
        f, err := os.Open(path)
        if err != nil {
            return err
        }
        process(f)
        f.Close()         // ✓ 用完立刻关(但注意: process 若 panic 就关不掉, 没 defer 安全)
    }
    return nil
}

// 核心: 把循环体抽成函数(或用立即执行的匿名函数), 让 defer 随"每轮的函数返回"执行,
//   资源用完即放; 这是修"循环里 defer 累积"最干净的办法。

修复的思路,统一为"defer 制造一个'每轮都会返回的函数作用域'"。正解一,把循环体抽成独立函数(推荐!):把"打开-处理-关闭"的逻辑,抽成一个 processOne(path) 小函数,循环里每轮调用它;这样,processOne 一返回,它里面的 defer f.Close() 就执行了——也就是每一轮就关一次,资源用完即放,既保留了 defer 的优雅,又避免了累积。这种写法额外的好处是:逻辑更内聚、更易测试正解二,用立即执行的匿名函数(闭包)包裹循环体:在循环里写一个 func() error { ... }(),它形成一个函数作用域,defer 在这个匿名函数返回时就执行——效果一样,适合不想单独抽函数的情况。正解三,干脆不用 defer、手动及时关:在循环里 process(f)立刻 f.Close();但要注意——这没有 defer 的"panic 也能关"的保障(若 process panic,这行就执行不到了),所以不如前两种安全。归根结底:把循环体抽成函数(或用立即执行的匿名函数),让 defer 随"每轮的函数返回"执行、资源用完即放——这是修"循环里 defer 累积"最干净的办法。

第三件事:defer 的另外几个经典坑

这次踩坑后,我顺势把 defer 其他几个同样隐蔽的经典坑,也一并梳理清楚了:

// defer 的其他经典坑:

// 坑1: 参数在"注册时"就求值(不是函数返回时)
func f1() {
    i := 0
    defer fmt.Println("defer:", i)   // ✗ 打印 0! i 在这行就被求值固定了
    i = 100
    // 函数返回时打印的是 "defer: 0", 不是 100
}
//   ✓ 想打印最终值, 用闭包: defer func(){ fmt.Println(i) }()  → 打印 100

// 坑2: defer 改命名返回值(可能是惊喜也可能是惊吓)
func f2() (result int) {
    defer func() { result++ }()      // defer 能修改命名返回值!
    return 10                        // 实际返回 11(return 先赋值, defer 再改)
}

// 坑3: defer 里的 error 没检查(尤其 Close 的错误被忽略)
func f3() {
    f, _ := os.Create("x")
    defer f.Close()                  // ✗ 写文件时, Close 的错误(如刷盘失败)被丢了!
    // ✓ 重要写操作: defer func(){ if err := f.Close(); err != nil { handle } }()
}

// 坑4: 循环里 defer 累积(本文) —— 已讲

// 坑5: defer 有微小性能开销, 极热路径的紧循环里要权衡(一般无需在意)

// 核心: defer 参数注册时求值、能改命名返回值、Close 的 error 别忽略、
//   循环里会累积 —— 这些时机与作用域细节, 用前都要想清楚。

原来 defer 的"脾气"还真不少。坑一,参数在"注册时"就求值:defer fmt.Println(i) 里的 i,defer 那一行就被求值固定了,之后改 i 也没用(想打印最终值,得用闭包 defer func(){ fmt.Println(i) }())。坑二,defer 能修改命名返回值:defer func(){ result++ }() 真的能改返回值(return 10 实际返回 11)——这个特性,用对了是惊喜(比如统一改错误),用错了是惊吓。坑三,defer 里的 error 被忽略:defer f.Close()Close错误丢了;对重要的写操作,Close 可能因刷盘失败而报错,要 defer func(){ if err := f.Close(); err != nil {...} }() 显式处理。(坑四就是本文的循环累积;坑五defer 有微小性能开销,极热的紧循环里需权衡,一般无需在意。)归根结底:defer 参数注册时求值、能改命名返回值、Close 的 error 别忽略、循环里会累积——这些时机与作用域的细节,用前都要想清楚。

下面这张图,是这次"循环 defer 累积"的成因与解法:

第四件事:循环里释放资源,几种写法的对比

这次踩坑后,我把"循环里要打开并释放资源"的几种写法,横向比了一遍,按场景对号入座。

写法 资源何时释放 panic 安全 推荐度
循环里 defer Close ✗ 函数结束才释放, 累积 ✗ 错误用法
抽成小函数 + defer ✓ 每轮函数返回即释放 ★★★ 最推荐
匿名函数包裹 + defer ✓ 每轮匿名函数返回即释放 ★★ 不想抽函数时
循环里手动 Close ✓ 用完立即释放 ✗ panic 则漏关 ★ 简单场景

把它们排在一起,优劣一目了然。最推荐的,是"抽成小函数 + defer":它每轮就释放(不累积)、panic 也能释放(defer 的保障还在)、还顺带让代码更内聚易测,集所有优点于一身。"匿名函数包裹 + defer"效果等同,适合不想单独抽函数的场景;"循环里手动 Close"虽简单,但丢了 defer 的"panic 也能关"的保障(process 一 panic 就漏关),只适合最简单、明确不会 panic 的场景;而"循环里 defer Close",正是本文的错误用法。这张表给我的最大启发是:同样是"释放资源",写法的选择,本质是在"及时性"(每轮就放)和"安全性"(panic 也放)之间求两全——而"抽成小函数 + defer",恰恰是那个鱼与熊掌兼得的最优解;它也再次印证了那个朴素的道理:当一段循环体逻辑变复杂、还涉及资源管理时,把它抽成一个独立的函数,往往能同时解决"正确性"和"可读性"两个问题

第五件事:那些"看似一次性、实则会累积"的资源坑

顺着这次的教训,我把项目里其他"以为用完就放了、其实在悄悄累积"的资源坑,系统排查了一遍。它们和循环 defer 同根同源。

资源坑 累积的原因 正解
循环里 defer 关文件/连接 defer 到函数结束才执行 抽函数让 defer 每轮返回(本文)
循环里开 DB 连接不及时还 连接占着不还, 连接池耗尽 用完立刻还(defer 在子函数里)
HTTP resp.Body 不 Close 连接无法复用且泄漏 defer resp.Body.Close() 在请求函数里
time.Ticker / Timer 不 Stop 底层 goroutine/资源不释放 defer ticker.Stop()
循环里启 goroutine 不回收 goroutine 泄漏, 越积越多 用 WaitGroup/context 管理生命周期
大量临时对象不复用 GC 压力大 sync.Pool 复用(热路径)

这张表,让我看清了这些坑共同的根它们本质上是同一类问题:某个"有限的、需要显式归还"的资源(文件句柄、连接、goroutine、内存),被持续地获取、却没有及时地归还,于是在某个维度上"只增不减",最终耗尽无论是循环 defer 累积文件(本文)、DB 连接不及时还(连接池耗尽)、HTTP resp.Body 不 Close(连接泄漏)、Ticker 不 Stop、还是goroutine 不回收——它们的解法,也都指向同一件事:为每一次"获取",都明确地、及时地,配上一次对应的"归还"它给我的最大启发是:写程序,要建立一种"资源守恒"的意识:凡是你""来的东西(打开的、连接的、申请的),都必须想清楚"它什么时候、在哪里、被还回去";而尤其要警惕的,是那些在循环、并发等"会重复发生"的场景里的获取——因为一次小小的"忘记归还",在重复成千上万次后,就会从"无关紧要"放大成"致命的耗尽"有借必有还,再借不难——这朴素的道理,在资源管理上,字字千金。

第六件事:在循环里用 defer 前,我现在会怎么决策

现在,每当我准备在循环里写 defer,脑子里都会过一遍这张决策图——核心就一问:这个资源,需要"每轮就释放"吗?

这张图的灵魂,是那个必问的问题:这个资源,需要"每轮就释放"吗?(或者说:循环次数会很多吗?)如果不需要、且循环次数很少(比如就几次),那循环里直接 defer 也勉强能接受;但只要需要每轮就放、或循环次数很多,循环里直接 defer 就会累积,必须改:首选,把循环体抽成小函数、defer 放小函数里(每轮就放、又有 panic 保障);次选,用匿名函数包裹 + defer;简单且确定不会 panic 的,可以手动 Close这套判断,让我以后在循环里写 defer 时,不再无脑用,而是先想清楚那个核心问题——而它的本质,始终是:这个 defer,会等到函数结束才执行;而我能等到那时候吗?

我立下的几条规矩

这场"too many open files"的事故,换来了我写 Go 时,刻进骨子里的几条铁律:

  1. defer 是函数级延迟,不是块级/循环级。它在所在函数 return 时才执行,循环里的 defer 会全部堆积到函数结束。
  2. 循环里要"每轮释放"资源,抽成小函数。让 defer 随小函数每轮返回执行,既及时释放又保留 panic 安全。
  3. 循环次数多时,绝不在循环体里直接 defer 释放资源。否则文件/连接/锁句柄会累积到耗尽。
  4. 记住 defer 的参数注册时就求值。defer f(i) 的 i 在那行就定了;要取最终值用闭包 defer func(){...i...}()。
  5. 重要写操作的 Close 错误别忽略。defer f.Close() 会丢错误;写文件用 defer func(){ if err:=f.Close();... }()。
  6. 建立"资源守恒"意识。凡是借来的(打开/连接/申请),都要想清楚何时何地归还,尤其在循环/并发里。
  7. 循环体复杂就抽函数。它能同时解决资源及时释放、可读性、可测试性多个问题。

附:几行代码看清 defer 到底何时执行

口说无凭。下面这几段,用打印顺序,直观展示 defer "到函数返回才执行" 和循环里"累积"的真面目,跑一遍胜过千言:

package main

import "fmt"

// 实验1: defer 是"函数返回时"执行, 且 LIFO(后进先出)
func demo1() {
    fmt.Println("函数开始")
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")
    fmt.Println("函数结束前")
}
// 输出: 函数开始 / 函数结束前 / defer 3 / defer 2 / defer 1
//   → defer 全在最后执行, 且后注册的先执行(栈)

// 实验2: 循环里的 defer "累积"到函数返回才一起执行
func demo2() {
    for i := 0; i < 3; i++ {
        defer fmt.Println("loop defer:", i)   // 不是每轮执行!
        fmt.Println("loop body:", i)
    }
    fmt.Println("循环已结束")
}
// 输出: loop body:0 / loop body:1 / loop body:2 / 循环已结束
//       / loop defer:2 / loop defer:1 / loop defer:0
//   → 三个 defer 全堆到函数末尾才执行(这就是句柄累积的根源!)

// 实验3: 抽成函数后, defer 每轮就执行
func demo3() {
    for i := 0; i < 3; i++ {
        func(n int) {
            defer fmt.Println("inner defer:", n)   // ✓ 每轮匿名函数返回就执行
            fmt.Println("inner body:", n)
        }(i)
    }
}
// 输出: inner body:0 / inner defer:0 / inner body:1 / inner defer:1 / ...
//   → defer 紧跟在每轮 body 后执行 = 每轮就释放!

// 实验4: defer 参数注册时就求值
func demo4() {
    i := 0
    defer fmt.Println("注册时的 i:", i)   // 打印 0(注册时求值)
    i = 100
}

// 核心: 一跑便知 —— demo2 的 defer 全堆到末尾(累积), demo3 抽函数后每轮就执行;
//   defer 是函数级、LIFO、参数注册时求值, 眼见为实。

这几段代码,把 defer 的行为彻底摊在了阳光下实验 1 展示了 defer 的两个基本性质:它全在函数返回时才执行(先打印"函数结束前",再打印三个 defer)、且后进先出(defer 3 最先执行)。实验 2本文坑的最小复现:循环里的三个 defer,没有一个在循环中执行,而是全堆到"循环已结束"之后、函数末尾,才一起倒序执行——这肉眼可见的"累积",正是句柄耗尽的根源。实验 3 则展示了解法的效果:把循环体包进匿名函数后,defer紧跟在每轮 body 之后执行了(body:0 / defer:0 / body:1 / defer:1...)——这就是"每轮就释放"。实验 4 印证了参数注册时求值(打印的是 0,不是 100)。这,正是我想用这几段代码,留给每一个 Go 开发者的最后一课:对于 defer 这种"执行时机和直觉不完全一致"的特性,与其在文档和记忆里反复纠结,不如写几行最小的代码、把它的执行顺序打印出来亲眼看一遍。当你亲眼见过 demo2 里那三个 defer 是怎么"憋到最后才一起爆发"的,你就再也不会,在循环里随手写下那个会累积成灾难的 defer 了。

写在最后

回头看,这场由"循环里的 defer"引发的、句柄耗尽程序崩溃的事故,真正教给我的,是一个比"defer 是函数级"本身更深的道理:一个语言特性"用起来有多方便",和"它在所有场景下都正确",是两码事;而越是方便的特性,越容易让人放松对它精确语义的警惕,从而在它"方便"的糖衣之下,踩到"语义"的暗刺。defer 实在是太方便、太优雅了——"打开后随手一 defer,就再也不用操心关闭"——正是这份极致的便利,让我把它当成了一个"万能的、随处可用的"自动清理魔法,而彻底忽略了它"延迟到函数返回"这个精确而关键的边界。我享受了它的便利,却没去深究它便利背后的运作机制,于是,在"循环"这个它语义的盲区里,被它反咬了一口。所以,使用任何"方便的抽象"时,都要多一份清醒:不仅要会用它"方便"的那一面,更要搞懂它"到底在何时、何地、做了什么"的精确语义,尤其是它的边界和失效的场景在哪里真正的熟练,不是"会用"一个特性,而是"懂得它什么时候不该用"对越方便的东西,越要懂它的边界——这,是我用一次"句柄耗尽"的崩溃,换来的、关于 Go、也关于"如何使用一切便利抽象"的、最朴素也最深刻的领悟。如果这篇复盘,能让你在下一次于循环里写下 defer 时,心里"咯噔"一下、多问一句"它会等到函数结束才执行,我等得起吗",那我对着那个 too many open files 熬的这大半天,就值了。

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

我把对象的方法当回调传给了 setTimeout,结果一执行就报 this 是 undefined、方法里的属性全访问不到,我对着这个丢了 this 的方法排查了大半天的复盘

2026-6-2 2:29:00

技术教程

我用自定义对象当 HashMap 的 key,两个字段完全一样的对象却被当成了不同的键、get 永远返回 null,我对着这个去重失效的 Map 排查了大半天的复盘

2026-6-2 2:40:47

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