我在循环里用 defer 关闭打开的文件,以为每次循环结束就关了,结果它们全堆到函数返回才一起关,跑到一半就 too many open files,我对着 defer 在函数返回时才执行这个坑排查大半天的复盘

一个让我对 Go 的 defer 到底什么时候执行彻底搞清楚的坑。defer 是个优雅方便的特性(确保资源一定释放)我用得很顺手,可我用错了它的时机——以为 defer file.Close() 会在当前这次循环结束时关闭文件,实际它要等整个函数返回时才关。一个函数循环处理几千个文件,每个 os.Open 后 defer f.Close() 确保关闭,看起来是 Go 推荐的惯用法,可跑到一半 too many open files 崩溃。深究 defer 执行时机才明白:defer 注册的函数是在它所在函数 return 时才执行(按 LIFO),不是当前 for 循环迭代结束时也不是当前代码块结束时,而是包含这个 defer 的那个函数整个返回时;所以循环里 defer,循环跑 N 次就注册 N 个 defer 全堆着等函数返回,循环期间 N 个文件全开着没关、句柄耗尽。我犯错是把 defer 当成离开当前作用域就执行(像有些语言的析构 RAII)搞混了,Go 的 defer 是函数级的不是块级的;还有衍生坑 defer 参数在 defer 那刻就求值不是执行时。这篇从故障现场、defer 执行时机真相、正解(把循环体抽成独立函数让 defer 在它返回时及时执行最优雅、用立即执行匿名函数包住循环体、循环里显式 Close)、defer 其他坑(参数立即求值、改命名返回值、循环变量捕获、Close 错误忽略)、defer 时机速查表、defer 适用边界(函数级清理是主场循环要小心)、决策图与铁律,到附上一段用打印顺序亲眼看清 defer 函数级执行循环堆积抽函数及时执行的实验。核心领悟:很多特性的坑集中在时机维度(何时执行何时求值),时机和逻辑同样重要甚至更隐蔽,读写代码要想它什么时候做按什么顺序做用哪个时刻的值;一个特性的作用范围粒度(块级函数级对象级)根本决定它的行为,粒度假设错了对行为的预判就系统性出错,要明确搞清它的边界粒度;用打印顺序把无形的执行时序变成有形证据是确诊一切执行时机问题的万能钥匙;特性优雅不代表到处适用,在对的场景用对的特性(defer 是函数级清理的主场)。

我在循环里用 defer 关闭打开的文件,以为每次循环结束就关了,结果它们全堆到函数返回才一起关,跑到一半就 too many open files,我对着 defer 在函数返回时才执行这个坑排查了大半天的复盘

这是一个让我对 Go 的 defer "到底什么时候执行"彻底搞清楚的坑。它隐蔽在:defer 是个特别优雅、特别方便的特性(用它来"确保资源一定释放"),我用得很顺手;可我用错了它的时机——我以为 defer file.Close() 会在"当前这次循环结束时"关闭文件,实际它要等到"整个函数返回时"才关。

需求很常见:一个函数,要循环处理一大批文件,每个文件打开、处理、关闭。我用 defer 来确保每个文件都被关闭,写得很"优雅":

// 循环处理一批文件(有问题的版本)
func processFiles(paths []string) error {
    for _, path := range paths {        // 假设有几千个文件
        f, err := os.Open(path)
        if err != nil {
            return err
        }
        defer f.Close()                 // ★ 我以为: 每次循环结束就关闭当前文件
        //                                 实际: 所有Close都堆到【整个函数返回时】才执行!

        process(f)                       // 处理文件
    }
    return nil
    // 函数返回时, 才一口气执行所有累积的 defer f.Close() —— 此时早就晚了!
}

这个 defer f.Close() 看起来再正确不过——"打开文件后,defer 一下确保它被关闭",这是 Go 里推荐的惯用法啊!可当 paths 有几千个文件时,这个函数跑到一半就崩了:

报错: too many open files

# 现象:
#   - 文件少时(几个): 正常
#   - 文件多时(几千个): 跑到一半 too many open files 崩溃
#   - 原因: 循环里打开的文件, defer 的 Close 全都【没有在循环中执行】,
#     而是【堆积】着, 等到【整个函数返回时】才一起执行!
#   - → 函数还没返回(还在循环中), 已经打开了几千个文件却一个都没关,
#     很快就超过了进程的文件句柄上限 → too many open files

我盯着这个 too many open files,起初很困惑——我明明每个文件都 defer Close 了啊,怎么会句柄耗尽?直到我意识到 defer执行时机:我以为 defer f.Close() 是"这次循环结束就关",可它实际是"等整个 processFiles 函数返回时才关";于是循环跑了几千次,打开了几千个文件,每个的 Close 都被"延迟"着、堆积着,一个都没真正执行;函数还在循环里没返回,几千个文件句柄就这么全开着,自然就 too many open files 了。

第一件事:看清真相——defer 是在"函数返回时"执行,不是"当前代码块结束时"

我去深入理解了 Go 的 defer 的执行时机,才彻底明白这个"资源堆积不释放"之谜——defer 注册的函数,是在它所在的那个函数返回时才执行的(按后进先出 LIFO 顺序),而不是在"当前循环迭代结束"或"当前代码块结束"时;所以在循环里 defer,每一次迭代的 defer 都不会立即执行,而是被累积起来,直到整个函数返回时才一口气全部执行——循环期间,这些资源全都没被释放、堆积着

defer 执行时机的真相

# 1. defer 的执行时机: 它注册的函数, 在【所在函数 return 时】才执行。
#    - 不是"当前for循环迭代结束时"!
#    - 不是"当前 { } 代码块结束时"!
#    - 就是"包含这个defer的那个【函数】整个返回(return/panic)时"。

# 2. 多个defer: 按【后进先出(LIFO, 栈)】的顺序, 在函数返回时依次执行。

# 3. 在循环里 defer 发生了什么:
#    for ... {
#        f, _ := os.Open(path)
#        defer f.Close()      // 每次迭代注册一个defer, 但【都不执行】
#    }
#    return  // ← 直到这里(函数返回), 才一口气执行所有累积的 f.Close()
#    - 循环跑N次, 就注册了N个defer, 它们【全堆着】等函数返回;
#    - → 循环期间, N个文件全开着没关 → 资源(句柄)堆积 → too many open files!

# 4. 为什么会错: 我把"defer = 离开当前作用域就执行"(像有些语言的析构/RAII)
#    搞混了。Go的defer是"函数级"的, 不是"块级"的!
#    - "{ }块"结束不触发defer; 只有"函数"返回才触发。

# 5. 衍生坑: defer 的【参数是在defer那一刻就求值的】(不是执行时):
#    for i := 0; i < 3; i++ { defer fmt.Println(i) }
#    → 打印 2,1,0(LIFO, 且i的值在defer时已确定); 这是另一个defer常见坑。

# 核心: defer在"所在函数返回时"才执行(不是循环/代码块结束时), 在循环里defer会让资源全堆积到
#   函数返回才释放, 循环期间不释放导致句柄/资源耗尽; defer是函数级不是块级的。

真相大白,我恍然大悟。原来 defer 注册的函数,是在它所在的那个函数 return 时才执行的(按 LIFO 顺序)——不是"当前 for 循环迭代结束时",也不是"当前 { } 代码块结束时",而是"包含这个 defer 的那个函数整个返回时"所以在循环里 defer:循环跑 N 次就注册了 N 个 defer,它们全堆着等函数返回,循环期间 N 个文件全开着没关、资源堆积,自然 too many open files。我犯错,是因为把"defer = 离开当前作用域就执行"(像有些语言的析构/RAII)搞混了——Go 的 defer 是"函数级"的,不是"块级"的;"{ } 块"结束不触发 defer,只有"函数"返回才触发。还有一个衍生坑:defer 的参数是在 defer 那一刻就求值的(不是执行时)——for i...{ defer fmt.Println(i) } 会打印 2,1,0(LIFO,且 i 的值在 defer 时已确定),这是另一个 defer 常见坑。

第二件事:正解——把循环体抽成函数让 defer 及时执行,或循环里显式关闭

搞懂了原理,正解就清晰了:把循环体抽成一个独立的小函数(defer 在那个小函数返回时就执行,每次迭代都及时释放);或在循环里显式调用 Close()(不依赖 defer)

// ====== 正解一(推荐): 把循环体抽成一个函数, 让defer在它返回时执行 ======
func processFiles(paths []string) error {
    for _, path := range paths {
        if err := processOne(path); err != nil {   // 每次迭代调用一个独立函数
            return err
        }
    }
    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
}
// → 每次循环调用processOne, 它一返回(处理完一个文件)就执行defer关闭文件;
//   文件用完即关, 不会堆积, 不再 too many open files。这是最优雅的解法。

// ====== 正解二: 循环里显式 Close, 不用 defer ======
func processFiles2(paths []string) error {
    for _, path := range paths {
        f, err := os.Open(path)
        if err != nil {
            return err
        }
        process(f)
        f.Close()          // ★ 显式关闭(注意: 这样如果process panic, 可能漏关)
        // 更稳: 用匿名函数包一层, 让defer在匿名函数内生效:
        //   func() { defer f.Close(); process(f) }()
    }
    return nil
}

// ====== 正解三: 用立即执行的匿名函数包住循环体 ======
for _, path := range paths {
    func() {                       // 匿名函数, 立即执行
        f, _ := os.Open(path)
        defer f.Close()            // defer在【这个匿名函数返回时】执行 = 每次迭代结束就关
        process(f)
    }()                            // 立即调用
}
// → 匿名函数也是"函数", defer在它返回时执行; 每次迭代结束就关, 不堆积。

// ====== 牢记原则: defer是函数级的 ======
// - 想让defer"每次迭代就执行" → 给它一个"每次迭代都返回的函数"(抽函数/匿名函数)
// - 别在长循环里直接defer释放资源(会堆积到函数末尾)

// 核心: 别在循环里直接defer释放资源(会堆积到函数返回); 把循环体抽成独立函数(或用立即执行的
//   匿名函数包住), 让defer在每次迭代的函数返回时及时执行; 或循环里显式Close。

修复的核心,是"给 defer 一个'每次迭代都返回的函数',让它及时执行"正解一(推荐):把循环体抽成一个函数——把"打开-处理-关闭一个文件"抽成 processOne(path),defer f.Close()processOne 返回时执行(= 每处理完一个文件就关),文件用完即关、不堆积,这是最优雅的解法正解二:循环里显式 Close()——不用 defer 直接 f.Close()(但注意 process panic 可能漏关)正解三:用立即执行的匿名函数包住循环体——func() { defer f.Close(); process(f) }(),匿名函数也是"函数",defer 在它返回时执行(= 每次迭代结束就关)牢记原则:defer 是函数级的——想让 defer"每次迭代就执行",就给它一个"每次迭代都返回的函数"(抽函数/匿名函数);别在长循环里直接 defer 释放资源归根结底:别在循环里直接 defer 释放资源(会堆积到函数返回);把循环体抽成独立函数或用立即执行的匿名函数包住,让 defer 在每次迭代的函数返回时及时执行;或循环里显式 Close。

第三件事:defer 相关的其他常见坑

排查后我把 Go 的 defer 相关的其他常见坑也系统梳理了一遍。

defer 相关的其他常见坑

# 1. 循环里defer资源堆积(本文): 到函数返回才释放。→ 抽函数/匿名函数/显式关。

# 2. defer参数立即求值: defer f(x) 的x在defer那一刻就算好了, 不是执行时。
#    for i...{ defer fmt.Println(i) } → 打印的是defer时的i值。
#    → 想用执行时的值, 用闭包: defer func(){ fmt.Println(i) }()(注意闭包捕获变量)。

# 3. defer里改命名返回值: defer func(){ result = ... }() 能改有名返回值(可用于统一处理)。

# 4. defer + 循环变量(pre-1.22): defer func(){ use loopVar }() 捕获的是变量, 可能取到最终值。

# 5. defer 的性能: 极高频路径里defer有微小开销(新版已优化, 多数场景可忽略)。

# 6. 忘了defer导致资源泄漏: 反过来, 该defer关闭的没defer/没关, 也泄漏(见连接泄漏篇)。

# 7. defer的Close错误被忽略: defer f.Close()忽略了Close的返回错误(写文件时可能丢数据)。
#    → 重要场景: defer func(){ if e:=f.Close(); e!=nil {...} }()。

# 共同根源: defer是"函数级、注册时求值参数、LIFO、函数返回时执行"的机制;
#   把它的执行时机(函数级非块级)、求值时机(参数注册时求值)搞错, 就会踩坑。

# 核心: defer是函数级(函数返回时执行)、参数注册时求值、LIFO; 别在循环里直接defer释放资源;
#   想每次迭代释放就抽函数/匿名函数; 注意参数求值时机、Close错误处理、改命名返回值等用法。

排查让我把 defer 的其他坑也梳理清了。一、循环里 defer 资源堆积(本文)。二、defer 参数立即求值(参数在 defer 那刻就算好,想用执行时的值用闭包)。三、defer 里改命名返回值(可用于统一处理)。四、defer+循环变量(pre-1.22 捕获变量取到最终值)。五、defer 的性能(高频路径微小开销)。六、忘了 defer 导致资源泄漏七、defer 的 Close 错误被忽略(写文件可能丢数据)。它们的共同根源是:defer 是"函数级、注册时求值参数、LIFO、函数返回时执行"的机制;把它的执行时机(函数级非块级)、求值时机(参数注册时求值)搞错,就会踩坑核心是:defer 是函数级、参数注册时求值、LIFO;别在循环里直接 defer 释放资源;想每次迭代释放就抽函数/匿名函数下面这张图,是这次 defer 堆积的成因与解法:

第四件事:defer 的"时机"速查表

这次踩坑后,我把 defer 关于"时机"的几个关键点整理成一张表。

问题 答案 说明
defer 何时执行 所在函数 return 时 不是块/循环结束时
多个 defer 顺序 LIFO 后进先出 像栈
defer 参数何时求值 defer 语句执行时(注册时) 不是 defer 函数执行时
循环里 defer 全堆到函数返回才执行 资源堆积的坑(本文)
想每次迭代执行 defer 抽成函数/匿名函数 给它一个会返回的函数
defer 能改命名返回值吗 defer func(){result=...}()

这张表把 defer 的时机钉清了。核心是:defer 的两个"时机"最容易搞错——执行时机是"所在函数返回时"(不是块/循环结束),参数求值时机是"defer 语句执行时(注册时)"(不是 defer 函数被调用时);搞清这两个时机,defer 的所有行为就都能预判了它给我的最大启发是:很多语言特性的"",都集中在"时机"这个维度上——"这段代码/这个求值,到底发生在什么时候";defer 何时执行、参数何时求值、闭包何时取变量值、惰性表达式何时计算、异步何时完成……这些"时机问题",是编程里一类极其普遍、又极易出错的难点这其实是我在整个踩坑系列里反复撞见的主题:从 Python 默认参数(定义时求值)、闭包延迟绑定(调用时取值)、C# LINQ(枚举时执行)、JS 异步(事件循环时机),到 Go 的 defer——"什么时候发生"(时机/求值顺序),和"发生了什么"(逻辑)同样重要,甚至更隐蔽、更易错;读代码、写代码时,不仅要想"这段代码做什么",还要想"什么时候做、按什么顺序做、用的是哪个时刻的值"把"时机/求值顺序"当成一个和"逻辑"同等重要的维度来思考——是避开一大类(含 defer)隐蔽坑的关键。

第五件事:defer 是优雅的, 但要用在对的地方

这次也让我重新认识了 defer 这个特性——它很优雅,但有它适合和不适合的地方。

场景 defer 合适吗 说明
函数级的资源释放 ✓ 非常适合 打开后立刻defer关, 确保任何return都释放
解锁/Unlock ✓ 适合 Lock后defer Unlock, 防忘记
循环里释放每次迭代的资源 ✗ 不直接适合 会堆积, 要抽函数(本文)
需要"块级"释放 ✗ 不适合 defer是函数级, 没有块级defer
极高频热路径 △ 看情况 有微小开销(新版已优化)

这张表道出了 defer 的"适用边界"。核心是:defer 在"函数级的资源释放"(打开文件/加锁后,defer 关闭/解锁,确保函数任何路径返回都释放)这个场景下极其优雅、强烈推荐;但它不适合"循环里的、每次迭代的"资源释放(会堆积),也没有"块级"defer;用对了它是利器,用错了地方就成了坑。它给我的深刻启发是:一个特性"优雅、好用",不代表它"到处都适用";每个特性都有它被设计来解决的场景(defer 是为"函数级的、配对的清理"设计的),和它不擅长/不适合的场景(循环里逐次释放、块级清理);把一个好特性用在它不适合的场景,它的优雅就会变成陷阱(就像我把函数级的 defer 用在了循环里)这让我对"用特性"有了更成熟的认识:用一个特性时,不仅要知道"它怎么用",还要理解"它是为什么场景设计的、它的语义边界在哪、什么场景下它不适合";"在对的场景用对的特性",才能发挥它的优雅;不分场景地滥用一个好特性,反而会踩它的坑理解 defer 的设计场景(函数级清理)与边界(非块级、循环要小心)、在对的场景用它——是这个 defer 坑,带给我的关于"如何用好一个特性"的认知。

第六件事:用 defer 时,我现在的判断习惯

现在每当我写 defer,我都会按这张图先想清楚:

这张图的精髓,是"函数级 defer 放心用,循环里 defer 要让它每次迭代就释放"不在循环里的函数级 defer 放心用(打开后立刻 defer 关);在循环里就警惕它堆积——循环多/资源宝贵时必须让它每次迭代就释放:把循环体抽成独立函数(推荐)、用立即执行匿名函数包住、或显式 Close这套习惯,让我用 defer 时,从"循环里随手 defer"变成了"先想这 defer 啥时候执行、会不会堆积"——核心始终是:defer 是函数级、函数返回时才执行;循环里直接 defer 会堆积,要抽函数让它及时释放。

我立下的几条规矩

这场"循环里 defer 资源堆积"的事故,换来了我写 Go 时,刻进骨子里的几条铁律:

  1. defer 在所在函数返回时才执行。不是循环/代码块结束时,是函数级的。
  2. 循环里直接 defer 会堆积。所有资源到函数返回才一起释放,可能耗尽。
  3. 想每次迭代释放就抽成函数。defer 在那个小函数返回时及时执行。
  4. 或用立即执行的匿名函数包住循环体。defer 在匿名函数返回时执行。
  5. defer 参数在注册时就求值。不是 defer 函数执行时,想用执行时的值用闭包。
  6. 函数级资源释放才是 defer 的主场。打开后立刻 defer 关,确保任何路径释放。
  7. 把"时机/求值顺序"当成和逻辑同等重要的维度。defer 的坑多在时机。

附:一段亲眼看清 defer 执行时机的实验

口说无凭。下面这段代码,把"defer 函数级执行、参数注册时求值、循环堆积"一次演示清楚:

package main

import "fmt"

func main() {
    fmt.Println("=== 1. defer在函数返回时执行(LIFO) ===")
    demo1()

    fmt.Println("\n=== 2. 循环里defer全堆到函数返回 ===")
    demo2()

    fmt.Println("\n=== 3. 抽成函数, defer每次迭代就执行 ===")
    demo3()
}

func demo1() {
    fmt.Println("  函数开始")
    defer fmt.Println("  defer A")   // 后执行
    defer fmt.Println("  defer B")   // 先执行(LIFO)
    fmt.Println("  函数体")
    // 输出顺序: 函数开始 -> 函数体 -> defer B -> defer A
    // (defer 在函数返回时, 按后进先出执行)
}

func demo2() {
    for i := 0; i < 3; i++ {
        defer fmt.Printf("  循环defer i=%d\n", i)   // ★ 参数i在defer时就求值
    }
    fmt.Println("  循环结束(但defer还没执行!)")
    // 输出: 循环结束(但defer还没执行!) -> 循环defer i=2 -> i=1 -> i=0
    // → 看! 三个defer都堆到demo2返回时才执行, 且i是注册时的值(2,1,0 LIFO)
}

func demo3() {
    for i := 0; i < 3; i++ {
        func(n int) {                 // 抽成(匿名)函数
            defer fmt.Printf("  迭代%d的defer\n", n)   // defer在这个匿名函数返回时执行
            fmt.Printf("  处理 %d\n", n)
        }(i)
    }
    // 输出: 处理0 -> 迭代0的defer -> 处理1 -> 迭代1的defer -> 处理2 -> 迭代2的defer
    // → 每次迭代结束(匿名函数返回)就执行defer, 不堆积!
}

// 核心: 跑一遍, 亲眼看到 demo2的"循环结束"先打印、defer后打印(堆到函数返回)、
//   而demo3每次迭代就执行defer——defer函数级执行、循环堆积、抽函数及时执行一目了然。

这段实验代码,是我这次踩坑后写下的"defer 时机显形器"。它用三个对比鲜明的小函数,把 defer 的行为彻底摊开:demo1 让你看到 defer 在函数返回时、按 LIFO 顺序执行;demo2 是关键——它先打印"循环结束(但 defer 还没执行!)",然后才打印三个 defer,用打印顺序铁证了"循环里的 defer 全堆到函数返回才执行";demo3 则展示抽成(匿名)函数后,defer 在每次迭代就及时执行(处理和 defer 交替出现)。这正是我想用这段代码,留给每个学 Go 的人的核心方法:要搞清楚"某段代码到底什么时候执行、按什么顺序执行"(执行时机/控制流),最直接的办法,就是在关键位置插入带标识的打印语句,然后看打印出来的顺序——打印的先后顺序,会把"无形的执行时序"变成"有形的、确凿的证据"因为"执行时机/顺序"是一种看不见的、容易在脑子里推演错的东西(我就把 defer 的时机推演错了);而"插桩打印、看输出顺序"能把它直接呈现给你——你不用猜"defer 到底先于还是后于'循环结束'",一跑就看到了;这是我整个踩坑系列里,搞清一切"执行时机/控制流"问题(defer、闭包、异步、惰性求值)最朴素、最通用、最可靠的法门用"插桩打印、看输出顺序"把无形的执行时序变成有形的证据——这份习惯,是我确诊一切"它到底什么时候执行"问题的万能钥匙。

写在最后

回头看,这场由"循环里 defer 堆积"引发的、句柄耗尽的事故,真正教给我的,远不止"循环里别 defer、要抽函数"这一个技巧。它让我对"一个特性的'作用范围/粒度',决定了它的行为",有了一次深刻的体会。我栽跟头,是因为我对 defer 的"作用范围"有一个错误的假设。我下意识地以为 defer 是"块级"的——以为它会在"离开当前这个代码块(比如 for 循环的一次迭代)"时就执行,就像有些语言里"变量离开作用域就析构"那样。可 Go 的 defer 实际是"函数级"的——它的"作用范围"是整个函数,只有函数返回这一个时刻才会触发。我用"块级粒度"的假设,去使用一个"函数级粒度"的特性,于是对"它什么时候执行"的判断,整整差了一个量级——我以为是"每次迭代",实际是"整个函数末尾"。这让我领悟到一个深刻的认知:很多特性/机制,都有一个"作用范围/粒度"(块级?函数级?对象级?进程级?)——这个粒度,根本性地决定了它的行为(何时生效、何时失效、作用于谁);如果你对一个特性的"粒度"假设错了,你对它行为的预判就会系统性地出错(我把函数级当块级,于是对执行时机的预判全错了)这其实是理解很多机制的一把钥匙:用一个特性时,要明确地搞清楚它的"作用范围/粒度"——defer 是函数级、变量有它的作用域、锁有它保护的范围、事务有它的边界、缓存有它的层级;"它的边界/粒度到底是什么",和"它做什么"同样关键;搞清了粒度,你才能准确预判"它在什么范围内、什么时机上起作用"明确一个特性的作用范围/粒度(defer 是函数级)、用对它粒度的方式去使用它——这,是我用一次 defer 堆积的事故,换来的、关于 Go、也关于如何准确理解任何特性行为的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次想在循环里写 defer 时,先想一句"它要等整个函数返回才执行哦"、转而把循环体抽成函数,那我对着那个 too many open files 排查的这大半天,就值了。

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

我做金额计算,0.1 加 0.2 居然不等于 0.3,累加几笔钱后总额还对不上、比较相等也失败,我对着 JavaScript 浮点数无法精确表示小数这个坑排查了大半天的复盘

2026-6-2 13:27:09

技术教程

静态共享一个 SimpleDateFormat 给所有线程,高并发下偶发日期错乱甚至抛异常:一次线程不安全的深度排查与 DateTimeFormatter 正解

2026-6-2 13:40:58

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