Go深入学习延迟语句

news2025/6/9 9:59:58

1 延迟语句是什么

编程的时候,经常会需要申请一些资源,比如数据库连接、文件、锁等,这些资源需要再使用后释放掉,否则会造成内存泄露。但是编程人员经常容易忘记释放这些资源,从而造成一些事故。 Go 语言直接在语言层面提供 defer 关键字,在申请资源语句的下一行,可以直接用 defer 语句来注册函数结束后执行释放资源的操作。因为这样一颗小小的语法糖,忘关闭资源语句的情况就打打地减少了。

defer 是 Go 语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过 return 正常结束或者 panic 导致的异常结束)执行。在需要释放资源的场景非常有用,可以很方便在函数结束前做一些清理操作。在打开资源语句的下一行,直接使用 defer 就可以在函数返回前释放资源,可谓相当的高效。

defer 通常用于一些成对操作的场景:打开连接 / 关闭连接、加锁 / 释放锁、打开文件 / 关闭文件等。使用非常简单:

f, err := os.Open(filename)
if err !=  nil {
    panic(err)
}

if f !=  nil {
    defer f.Close()
}

在打开文件的语句附近,用 defer 语句关闭文件。这样,在函数结束之前,会自动执行 defer 后面的语句来关闭文件。注意,要先判断 f 是否为空,如果 f 不为空,在调用 f.Close() 函数,避免出现异常情况。

当然, defer 会有短暂延迟,对时间要求特别高的程序,可以避免使用它,其他情况一般可以忽略它带来的延迟。特别是 Go 1.14 又对 defer 做了很大幅度的优化,效率提升不少。

下面是一个反面例子:

r.mu.Lock()
rand.Intn(param)
r.mu.Unlock()

上面只有三行代码,看起来这里不用 defer 执行 Unlock 并没有什么问题。其实并不是这样,中间这行代码 rand.Intn(param) 其实是有可能发生 panic 的,更严重的情况是,这段代码很有可能被其他人修改,增加更多的逻辑,而这完全不可控。也就是说,在 Lock 和 Unlock 之间的代码一旦出现异常情况导致 panic ,就会形成死锁。因此这里的逻辑是,即使看起来非常简单的代码,使用 defer 也是有必要的,因为需求总是在变化,代码也总会被修改。

2 延迟语句的执行顺序是什么

先看下官方文档对 defer 的解释:

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.(每次 defer 语句执行的时候,会把函数 “压栈”,函数参数会被复制下来;当外层函数 (注意不是代码块,如一个 for 循环块并不是外层函数) 退出时,defer 函数按照定义 的顺序逆序执行;如果 defer 执行的函数为 nil,那么会在最终调用函数的时候产生 panic。)

defer 语句并不会马上执行,而是会进入一个栈,函数 return 前,会按照先进后出的顺序执行。也就是一说,最先被定义的 defer 语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面先执行了,那后面函数的依赖就没有了,因而可能会出错。

在 defer 函数定义时,对外部变量的引用有两种方式:函数参数、闭包引用。前者再 defer 定义时就把值传递给 defer,并被 cache 起来;后者则会在 defer 函数真正调用时根据整个上下文确定参数当前的值。

defer 后面的函数在执行的时候,函数调用的参数会被保存起来,也就是复制一份。真正执行的时候,实际上用到的是这个复制的变量,因此如果此变量是一个” 值 “,那么就和定义的是一直的。如果此变量是一个” 引用 “,那就可能和定义的不一致。

举个例子:

func  main() {
    var v [3]struct{}
    for i :=  range v {
        defer  func() {
            fmt.Println(i)
        } ()
    }
}

执行结果:

2
2
2

defer 后面跟的是一个闭包,i 是” 引用 “类型的变量, for 循环结束后 i 的值为 2,因此最后打印了 3 个 2。

再看一个例子:

type  num  int

func (n num) print() {fmt.Println(n)}
func (n *num) pprint() {fmt.Println(*n)}
func  main() {
    var n num
    defer n.print()
    defer n.pprint()
    defer  func() {n.print()} ()
    defer  func() {n.pprint()} ()
    n =  3
}

执行结果为:

3
3
3
0

注意,defer 语句的执行顺序和定义顺序相反。

第四个 defer 语句是闭包,引用外部函数的 n ,最终结果为 3;第三个 defer 语句同上;第二个 defer 语句,n 是引用,最终求值是 3;第一个 defer 语句,对 n 直接求值,开始的时候是 0,所以最后是 0。

再看一个延伸例子:

func  main() {
    defer  func() {
        fmt.Println("before return")
    }()
    if  true {
        fmt.Println("during return")
        return
    }

    defer  func() {
        fmt.Println("after return")
    }()
}

运行结果如下:

during return
before return

解析:return 之后的 defer 函数不能被注册,因此不能打印出 after return。

延伸示例则可以视为对 defer 的原理的利用。某些情况下,会故意用到 defer 的” 先求值,在延迟调用 “的性质。想象这样一个场景:在一个函数里,需要打开两个文件进行合并操作,合并完成后,在函数结束前关闭打开的文件句柄:

func  mergeFile() error {
    // 打开文件一
    f, _ := os.Open("file1.txt")
    if f !=  nil {
        defer  func(f  io.Closer) {
            if err := f.Close(); err !=nil {
                fmt.Printf("defer close file1.txt err %v\n", err)
            }
        }(f)
    }

    // 打开文件二
    f, _ := os.Open("file2.txt")
    if f !=  nil {
        defer  func(f  io.Closer) {
            if err := f.Close(); err !=nil {
                fmt.Printf("defer close file2.txt err %v\n", err)
            }
        }(f)
    }
    // ......
    return  nil
}

上面的代码中就用到了 defer 的原理,defer 函数定义的时候,参数就已经复制进去了,之后,真正执行 close() 函数的时候就刚好关闭的是正确的” 文件 “了,很巧妙,如果不这样,将 f 当成函数参数传递进去的话,最后两个语句关闭的就是同一个文件:都是最后打开的文件。

在调用 close() 函数的时候,要注意一点:先判断调用主题是否为空,否则可能会解引用一个空指针,进而 panic。

3 如何拆解延迟语句

如果 defer 像前面介绍的那样简单,这个世界就完美了。但事情总是没那么简单,defer 用得不好,会陷入泥潭。

避免陷入泥潭的关键是必须深刻理解下面这条语句:

return xxx

上面这条语句经过编译之后,实际上生成了三条指令:

  1. 返回值 = xxx
  2. 调用 defer 函数
  3. 空的 return

第一步和第三步是 return 语句生成的指令,也就是说 return 并不是一条原子指令;第二步是 defer 定义的语句,这里可能会操作返回值,从而影响最终的结果。

看两个例子,试着将 return 语句和 defer 语句拆解到正确的顺序。

第一个例子:

func  f()(r  int) {
    t :=  5
    defer  func() {
        t = t +  5
    }()
    return t
}

拆解后:

func  f() (r  int) {
    t :=  5
    // 1. 赋值指令
    r = t
    // 2. defer 被插入到赋值与返回之间执行,这个例子中返回值 r 没被修改过
    func() {
        t = t +  5
    }
    // 3. 空的 return 指令
    return
}

这里第二步实际上并没有操作返回值 r,因此,main 函数中调用 f() 得到 5。

第二个例子:

func  f()(r  int) {
    defer func(r  int) {
        r = r +  5
    }(r)
    return  1
}

拆解后:

func  f()(r  int) {
    // 1. 赋值
    r =  1
    // 2. 这里改的 r 是之前传进去的r,不会改变要返回的那个 r 值
    defer func(r  int) {
        r = r +  5
    }(r)
    // 3. 空的 return
    return  1
}

第二步,改变的是传值进去的 r,是形参的一个复制值,不会影响实参 r。因此,main 函数中需要调用 f() 得到 1。

4 如何确定延迟语句的参数

defer 语句表达式的值在定义时就已经确定了。下面可以通过三个不同的函数来理解:

func  f1() {
    var err error
    defer fmt.Println(err)
    err = errors.New("defer1 error")
    return
}

func  f2() {
    var err error
    defer func() {
        fmt.Println(err)
    }()
    err = errors.New("defer2 error")
    return
}

func  f3() {
    var err error
    defer func(err  error) {
        fmt.Println(err)
    }(err)
    err = errors.New("defer3 error")
    return
}

func  main() {
    f1()
    f2()
    f3()
}

运行结果:

<nil>
defer2 error
<nil>

第一和第三个函数中,因为作为参数,err 在函数定义的时候就会求值,并且定义的时候 err 的值都是 nil,所以最后打印的结果都是 nil;第二个函数的参数其实也会在定义的时候求值,但是第二个例子中是一个闭包,它引用的变量 err 在执行的时候值最终变成 defer2 error 了。

现实中第三个函数比较容易犯错误,在生产环境中,很容易写出这样的错误代码,导致最后 defer 语句没有起到作用,造成一些线上事故,要特别注意。

5 闭包是什么

闭包不是一句两句话可以说清楚的,大家感兴趣的话可以自行搜索这块知识点,可以参考 闭包 和 闭包 这部分内容自己研究。

闭包是由函数及其相关引用环境组合而成的实体,即:闭包 = 函数 + 引用环境。

一般的函数都有函数名,而匿名函数没有。匿名函数不能独立存在,但可以直接调用或者赋值于某个变量。匿名函数也被称为闭包,一个闭包继承了函数声明时的作用域。在 Go 语言中,所有的匿名函数都是闭包。

有个不太恰当的例子:可以把闭包看成是一个类,一个闭包函数调用就是实例化一个类。闭包在运行时可以有多个实例,它会将同一个作用域里的变量和常量捕获下来,无论闭包在什么地方被调用 (实例化) 时,都可以使用这些变量和常量。而且,闭包捕获的变量和常量是引用传递,不是值传递。

举个例子:

func  main() {
    var a =  Accumulator()
    fmt.Printf("%d\n", a(1))
    fmt.Printf("%d\n", a(10))
    fmt.Printf("%d\n", a(100))
    fmt.Println("------------------------")
    var b =  Accumulator()
    fmt.Printf("%d\n", b(1))
    fmt.Printf("%d\n", b(10))
    fmt.Printf("%d\n", b(100))
}

func Accumulator() func(int) int {
    var x int
    return func(delta  int) int {
        fmt.Printf("(%+v, %+v) - ", &x, x)
        x += delta
        return x
    }
}

执行结果是:

(0xc420014070, 0) - 1
(0xc420014070, 1) - 11
(0xc420014070, 11) - 111
------------------------
(0xc4200140b8, 0) - 1
(0xc4200140b8, 1) - 11
(0xc4200140b8, 11) – 111

闭包引用了 x 变量,a,b 可看作 2 个不同的实例,实例之间互不影响。实例内部,x 变量是同一个地址,因此具有 “累加效应”。

6 延迟语句如何配合恢复语句

Go 语言被诟病多次的就是它的 error,实际项目里经常出现各种 error 满天飞,正常的代码逻辑里有很多 error 处理的代码块。函数总是会返回一个 error,留给调用者处理;而如果是致命的错误,比如程序执行初始化的时候出问题,最好直接 panic 掉,避免上线运行后出更大的问题。

有些时候,需要从异常中恢复。比如服务器程序遇到严重问题,产生了 panic,这时至少可以在程序崩溃前做一些 “扫尾工作”,比如关闭客户端的连接,防止客户端一直等待等;并且单个请求导致的 panic,也不应该影响整个服务器程序的运行。

panic 会停掉当前正在执行的程序,而不只是当前线程。在这之前,它会有序地执行完当前线程 defer 列表里的语句,其他协程里定义的 defer 语句不作保证。所以在 defer 里定义一个 recover 语句,防止程序直接挂掉,就可以起到类似 Java 里 try…catch 的效果。

注意,recover() 函数只在 defer 的函数中直接调用才有效。例如:

func  main() {
    defer fmt.Println("defer main")
    var user = os.Getenv("USER_")
    go  func() {
        defer  func() {
            fmt.Println("defer caller")
            if err :=  recover(); err !=  nil {
                fmt.Println("recover success, err: ", err)
            }
        }()

        func() {
            defer  func() {
                fmt.Println("defer here")
            }()
            if user ==  "" {
                panic("should set user env")
            }
            fmt.Println("after panic")
        }()
    }()
    time.Sleep(100)
    fmt.Println("end of main function")
}

执行结果:

defer here
defer caller
recover success. err: should set user env.
end of main function
defer main

代码中的 panic 最终会被 recover 捕获到。这样的处理方式在一个 http server 的主流程常常会被用到。一次偶然的请求可能会触发某个 bug,这时用 recover 捕获 panic,稳住主流程,不影响其他请求。

再看几个延伸的示例。这些例子都与 recover() 函数的调用位置有关。

考虑一下写法,程序是否能正确 recover 吗?如果不能,原因是什么:

第一个例子:

func  main() {
    defer  f()
    panic(404)
}

func  f() {
    if e :=  recover(); err !=  nil {
        fmt.Println("recover")
        return
    }
}

能,在 defer 函数中调用,生效。

第二个例子:

func  main() {
    recover()
    panic(404)
}

不能。直接调用 recover,返回 nil。

第三个例子:

func  main() {
    defer  recover()
    panic(404)
}

不能。要在 defer 函数中调用 recover。

第四个例子:

func  main() {
    defer  func() {
        if e :=  recover(); e !=  nil {
            fmt.Println(err)
        }
    }()
    panic(404)
}

能,在 defer 函数中调用,生效。

第五个例子:

func  main() {
    defer  func() {
        defer  func() {
            recover()
        }()
    }()
    panic()
}

不能,多重 defer 嵌套。

7 defer 链如果被遍历执行

为了在退出函数前执行一些资源清理的操作,例如关闭文件、释放连接、释放锁资源等,会在函数里写上多个 defer 语句,被 defered 的函数,以 “先进后出” 的顺序,在 RET 指令前得以执行。

在一条函数调用链中,多个函数中会出现多个 defer 语句。例如:a()→b()→c() 中,每个函数里都有 defer 语句,而这些 defer 语句会创建对应个数的 _defer 结构体,这些结构体以链表的形式 “挂” 在 G 结构体下。

多个 _defer 结构体形成一个链表,G 结构体中某个字段指向此链表。

在编译器的 “加持下”,defer 语句会先调用 deferporc 函数,new 一个 _defer 结构体,挂到 G 上。当然,调用 new 之前会优先从当前 G 所绑定的 P 的 defer pool 里取,没取到则会去全局的 defer pool 里取,实在没有的话才新建一个。这是 Go runtime 里非常常见的操作,即设置多级缓存,提升运行效率。

在执行 RET 指令之前 (注意不是 return 之前),调用 deferreturn 函数完成 _defer 链表的遍历,执行完这条链上所有被 defered 的函数 (如关闭文件、释放连接、释放锁资源等)。在 deferreturn 函数的最后,会使用 jmpdefer 跳转到之前被 defered 的函数,这时控制权从 runtime 转移到了用户自定义的函数。这只是执行了一个被 defered 的函数,那这条链上其他的被 defered 的 函数,该如何得到执行?

答案就是控制权会再次交给 runtime,并再次执行 deferreturn 函数,完成 defer 链表的遍历。

8 为什么无法从父 goroutine 恢复子 goroutine 的 panic

对于这个问题,其实更普遍的问题是:为什么无法 recover 其他 goroutine 里产生的 panic?

可能会好奇为什么会有人希望从父 goroutine 中恢复子 goroutine 内产生的 panic。这是因为,如果以下的情况发生在应用程序内,那么整个进程必然退出:

go  func() {
    panic("die die die")
}()

当然,上面的代码是显式的 panic,实际情况下,如果不注意编码规范,极有可能触发一些本可以避免的恐慌错误,例如访问越界:

go  func() {
    a :=  make([]int, 1)
    println(a[1])
}()

发生这种恐慌错误对于服务端开发而言几乎是致命的,因为开发者将无法预测服务的可用性,只能在错误发生时发现该错误,但这时服务不可用的损失已经产生了。

那么,为什么不能从父 goroutine 中恢复子 goroutine 的 panic? 或者一般地说,为什么某个 goroutine 不能捕获其他 goroutine 内产生的 panic?

其实这个问题从 Go 诞生以来就一直被长久地讨论,而答案可以简单地认为是设计使然:因为 goroutine 被设计为一个独立的代码执行单元,拥有自己的执行栈,不与其他 goroutine 共享任何数据。这意味着,无法让 goroutine 拥有返回值、也无法让 goroutine 拥有自身的 ID 编号等。若需要与其他 goroutine 产生交互,要么可以使用 channel 的方式与其他 goroutine 进行通信,要么通过共享内存同步方式对共享的内存添加读写锁。

那一点办法也没有了吗?方法自然有,但并不是完美的方法,这里提供一种思路。例如,如果希望有一个全局的恐慌捕获中心,那么可以通过创建一个恐慌通知 channel,并在产生恐慌时,通过 recover 字段将其恢复,并将发生的错误通过 channel 通知给这个全局的恐慌通知器:

package  main

import (
    "fmt"
    "time"
)

var notifier chan  interface{}
func  startGlobalPanicCapturing() {
    notifier =  make(chan  interface{})
    go  func() {
        for {
            select {
                case r :=  <- notifier:
                    fmt.Println(r)
            }
        }
    }()
}

func  main() {
    startGlobalPanicCapturing()
    // 产生恐慌,但该恐慌会被捕获
    Go(func() {
        a :=  make([]int, 1)
        println(a[1])
    })
}

// Go 是一个恐慌安全的 goroutine
func  Go(f  func()) {
    go  func() {
        defer  func() {
            if r :=  recover(); r !=  nil {
                notifer <- i
            }
        }()
    }()
}

上面的 func Go(f func()) 本质上是对 go 关键字进行了一层封装,确保在执行并发单元前插入一个 defer,从而能够保证恢复一些可恢复的错误。

之所以说这个方案并不完美,原因是如果函数 f 内部不再使用 Go 函数来创建 goroutine,而且含有继续产生必然恐慌的代码,那么仍然会出现不可恢复的情况。

go  func() {panic("die die die")}()

有人可能也许会想到,强制某个项目内均使用 Go 函数不就好了?事情也并没有这么简单。因为除了可恢复的错误外,还有一些不可恢复的运行时恐慌 (例如并发读写 map),如果这类恐慌一旦发生,那么任何补救都是徒劳的。解决这类问题的根本途径是提高程序员自身对语言的认识,多进行代码测试,以及多通过运维技术来增强容灾机制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2405215.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【大模型】【推荐系统】LLM在推荐系统中的应用价值

文章目录 A 论文出处B 背景B.1 背景介绍B.2 问题提出B.3 创新点B.4 两大推荐方法 C 模型结构C.1 知识蒸馏&#xff08;训练过程&#xff09;C.2 轻量推理&#xff08;部署过程&#xff09; D 实验设计E 个人总结 A 论文出处 论文题目&#xff1a;SLMRec&#xff1a;Distilling…

uni-app学习笔记二十九--数据缓存

uni.setStorageSync(KEY,DATA) 将 data 存储在本地缓存中指定的 key 中&#xff0c;如果有多个key相同&#xff0c;下面的会覆盖掉原上面的该 key 对应的内容&#xff0c;这是一个同步接口。数据可以是字符串&#xff0c;可以是数组。 <script setup>uni.setStorageSyn…

工作邮箱收到钓鱼邮件,点了链接进去无法访问,会有什么问题吗?

没事的&#xff0c;很可能是被安全网关拦截了。最近做勒索实验&#xff0c;有感而发&#xff0c;不要乱点击邮箱中的附件。 最初我们采用钓鱼邮件投递恶意载荷&#xff0c;发现邮件网关把我们的 exe/bat 程序直接拦截了&#xff0c;换成压缩包也一样拦截了&#xff0c;载荷始终…

基于安卓的线上考试APP源码数据库文档

摘 要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存…

【数据结构】顺序表和链表详解(下)

前言&#xff1a;上期我们从顺序表开始讲到了单链表的概念&#xff0c;分类&#xff0c;和实现&#xff0c;而这期我们来将相较于单链表没那么常用的双向链表。 文章目录 一、双向链表二&#xff0c;双向链表的实现一&#xff0c;增1&#xff0c;头插2&#xff0c;尾插3&#x…

【系统架构设计师】绪论-系统架构概述

目录 绪论 系统架构概述 单选题 绪论 系统架构概述 单选题 1、软件方法学是以软件开发方法为研究对象的学科。其中&#xff0c;&#xff08;&#xff09;是先对最高居次中的问题进行定义、设计、编程和测试&#xff0c;而将其中未解决的问题作为一个子任务放到下一层次中去…

SQL-事务(2025.6.6-2025.6.7学习篇)

1、简介 事务是一组操作的集合&#xff0c;它是一个不可分割的工作单位&#xff0c;事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 默认MySQL的事务是自动提交的&#xff0c;也就是说&#xff0…

Virtex II 系列FPGA的配置原理

对FPGA 芯片的配置&#xff0c;本质上是将根据设计生成的包含配置命令和配置数据的比特流文件写入到配置存储器中。 1 配置模式 Virtex II 系列FPGA 一共有五种配置模式&#xff0c;配置模式的选择是根据管脚M[2:0]来决定。 &#xff08;1&#xff09;串行配置模式 串行配置模…

蓝桥杯 国赛2024python(b组)题目(1-3)

第一题 试卷答题页 - 蓝桥云课 问题描述 在今年蓝桥杯的决赛中&#xff0c;一共有 1010 道题目&#xff0c;每道题目的分数依次为 55 分&#xff0c;55 分&#xff0c;1010 分&#xff0c;1010 分&#xff0c;1515 分&#xff0c;1515 分&#xff0c;2020 分&#xff0c;2020 分…

算法题(165):汉诺塔问题

审题&#xff1a; 本题需要我们找到最优的汉诺塔搬法然后将移动路径输出 思路&#xff1a; 方法一&#xff1a;递归 我们先分析题目 n为2的情况&#xff0c;我们先将第一个盘子移动到三号柱子上&#xff0c;然后再将二号盘子移动到二号柱子上 n为3的情况&#xff0c;我们先将前…

玄机——某次行业攻防应急响应(带镜像)

今天给大家带来一次攻防实战演练复现的过程。 文章目录 简介靶机简介1.根据流量包分析首个进行扫描攻击的IP是2.根据流量包分析第二个扫描攻击的IP和漏扫工具&#xff0c;以flag{x.x.x.x&工具名}3.提交频繁爆破密钥的IP及爆破次数&#xff0c;以flag{ip&次数}提交4. 提…

低代码逻辑引擎配置化实战:三步穿透审批记录查询

在堆积如山的报销单中埋头寻找某笔特殊费用的审批轨迹在跨部门协作时被追问"这个合同到底卡在哪个环节" 在快节奏的办公自动化场景中&#xff0c;这些场景是很常见的&#xff0c;传统OA系统中分散的审批记录查询方式往往太繁琐。 为破解这一痛点&#xff0c;在JVS低…

【Zephyr 系列 15】构建企业级 BLE 模块通用框架:驱动 + 事件 + 状态机 + 低功耗全栈设计

🧠关键词:Zephyr、BLE 模块、架构设计、驱动封装、事件机制、状态机、低功耗、可维护框架 📌面向读者:希望将 BLE 项目从“Demo 工程”升级为“企业可复用框架”的研发人员与技术负责人 📊预计字数:5500+ 字 🧭 前言:从 Demo 到产品化,架构该如何升级? 多数 BLE…

Docker构建Vite项目内存溢出:从Heap Limit报错到完美解决的剖析

问题现象:诡异的"消失的index.html" 最近在CI/CD流水线中遇到诡异现象:使用Docker构建Vite项目时,dist目录中缺少关键的index.html文件,但本地构建完全正常。报错截图显示关键信息: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out…

Android端口转发

如上图所示&#xff0c;有一个Android设备&#xff0c;Android设备里面有主板&#xff0c;主板上有网络接口和Wi-Fi&#xff0c;网络接口通过网线连接了一个网络摄像头&#xff0c;这就跟电脑一样&#xff0c;电脑即可以通过网线接入一个网络&#xff0c;也可以同时用Wi-Fi接入…

PHP环境极速搭建

一、为什么选择phpStudy VS Code&#xff1f; 作为一名初次接触PHP的开发者&#xff0c;我深知环境配置往往是学习路上的第一道门槛。传统PHP环境搭建需要手动配置Apache/Nginx、PHP解释器、MySQL等多重组件&#xff0c;光是处理版本兼容性和依赖问题就可能耗费半天时间——这…

建造者模式深度解析与实战应用

作者简介 我是摘星&#xff0c;一名全栈开发者&#xff0c;专注 Java后端开发、AI工程化 与 云计算架构 领域&#xff0c;擅长Python技术栈。热衷于探索前沿技术&#xff0c;包括大模型应用、云原生解决方案及自动化工具开发。日常深耕技术实践&#xff0c;乐于分享实战经验与…

代码中文抽取工具并替换工具(以ts为例)

文章目录 基本思路目录结构配置文件AST解析替换代码中文生成Excel启动脚本 基本思路 通过对应语言的AST解析出中文相关信息&#xff08;文件、所在行列等&#xff09;存到临时文件通过相关信息&#xff0c;逐个文件位置替换掉中文基于临时文件&#xff0c;通过py脚本生成Excel…

pgsql batch insert optimization (reWriteBatchedInserts )

reWriteBatchedInserts 是 PostgreSQL JDBC 驱动 提供的一个优化选项&#xff0c;它可以 重写批量插入语句&#xff0c;从而提高插入性能。 作用 当 reWriteBatchedInsertstrue 时&#xff0c;PostgreSQL JDBC 驱动会将 多个单独的 INSERT 语句 转换为 一个多行 INSERT 语句&a…

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(上)

概述 在 Swift 开发语言中&#xff0c;各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过&#xff0c;在涉及到多个子类派生于基类进行多态模拟的场景下&#xff0c;…