延迟语句(defer)是Go 语言里一个非常有用的关键字,它能把资源的释放语句与申请语句放到距离相近的位置,从而减少了资源泄漏的情况发生。

延迟语句是什么

        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 也是有必要的,因为需求总是在变化,代码也总会被修改。 

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

        每次 defer 语句执行的时候,会把函数“压栈”,函数参数会被复制下来;当外层函数(注意不是代码块,如一个 for 循环块并不是外层函数)退出时,defer 函数按照定义的顺序逆序执行;如果 defer 执行的函数为nil,那么会在最终调用函数的时候产生 panic 

        defer 语句并不会马上执行,而是会进入一个栈,函数return 前,会按先进后出的顺序执行。也就是说,最先被定义的defer 语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面先执行了,那后面函数的依赖就没有了,因而可能会出错。
在 defer 函数定义时,对外部变量的引用有两种方式:函数参数、闭包引用前者在 defer 定
义时就把值传递给 defer,并被 cache 起来
后者则会在 defer 函数真正调用时根据整个上下文确
定参数当前的值

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

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

执行结果:

2
1
0

        defer 后面跟的是一个闭包(后面小节会讲到),i 是“引用”类型的变量,for 循环结束后 i的值为 2,因此最后打印了2 1 0。

有了上面的基础,再来看一个例子:

type number intfunc (n number) print()   { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }
func main() {var n numberdefer 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 直接求值,开始的时候 n=0,所以最后是 0。
我们再来看两个延伸情况。例如,下面的例子中,return 之后的 defer 语句会执行吗?

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。 

 如何拆解延迟语句

        如果 defer 像前面介绍的那样简单,这个世界就完美了。但事情总是没这么简单,defer 用得
不好,会陷入泥潭。
避免陷入泥潭的关键是必须深刻理解下面这条语句:

return xxx

        上面这条语句经过编译之后,实际上生成了三条指令:
      1)设置返回值 = xxx。
2)调用 defer 函数。
3)空的 return。讲返回值返回


第 1 和第3 步是 return 语句生成的指令,也就是说return 并不是一条原子指令;第 2 步是
defer 定义的语句,这里可能会操作返回值,从而影响最终结果。
下面来看两个例子,试着将 return 语句和 defer 语句拆解到正确的顺序。
第一个例子: 

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

拆解后:

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

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

        第二个例子:

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

        拆解后:

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

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

        第三个例子:

package mainimport "fmt"func main() {res := deferRun()fmt.Println(res)
}func deferRun() (res int) {num := 1  defer func() {res++}()  return num
}

运行结果:

2

        在本例中,第一步是将result的值设置为num,此时还未执行defernum的值是1,所以result被设置为1,然后再执行defer语句将result+1,最终将result返回,所以会打印出 2

        如果把defer中的res++改成num++

func deferRun() (res int) {num := 1defer func() {num++}()  return num
}

运行结果: 

1

        第一步是将result的值设置为num,此时还未执行defernum的值是1,所以result被设置为1,然后再执行defer 即num+1,要返回的result并没有变最终将result返回,所以会打印出 1。 

如何确定延迟语句的参数

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

func f1() {var err errordefer fmt.Println(err)err = errors.New("defer1 error")return
}
func f2() {var err errordefer func() {fmt.Println(err)}()err = errors.New("defer2 error")return
}
func f3() {var err errordefer func(err error) {fmt.Println(err)}(err)err = errors.New("defer3 error")return
}
func main() {f1()f2()f3()
}

        运行结果: 

<nil>
defer2 error
<nil>

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

func deferrun3() {num := 1defer func() {fmt.Println(num)}()num++return
}

运行结果: 原理同上述第二个例子,也是闭包

2

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

闭包是什么


闭包是由函数及其相关引用环境组合而成的实体,即:闭包=函数+引用环境
一般的函数都有函数名,而匿名函数没有。匿名函数不能独立存在,但可以直接调用或者赋值于某个变量。匿名函数也被称为闭包,一个闭包继承了函数声明时的作用域。在 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 intreturn func(delta int) int {fmt.Printf("(%+v, %+v) - ", &x, x)x += deltareturn x}
}

执行结果是: 

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

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

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

        Go 语言被诟病多次的就是它的 error,实际项目里经常出现各种 error 满天飞,正常的代码逻
辑里有很多 error 处理的代码块。函数总是会返回一个 error,留给调用者处理;而如果是致命的错
误,比如程序执行初始化的时候出问题,最好直接 panic 掉,避免上线运行后出更大的问题。
有些时候,需要从异常中恢复。比如服务器程序遇到严重问题,产生了 panic,这时至少可以
在程序崩溃前做一些“扫尾工作”,比如关闭客户端的连接,防止客户端一直等待等;并且单个请求导致的 panic,也不应该影响整个服务器程序的运行。

recover异常捕获

        异常其实就是指程序运行过程中发生了panic,那么我们为了不让程序报错退出,可以在程序中加入recover机制,将异常捕获,打印出异常,这样也方便我们定位错误。而捕获的方式我们之前在讲defer的时候也提到过,一般是用recoverdefer搭配使用来捕获异常。
下面请看个具体例子:

func main() {         defer func() {                 if error:=recover();error!=nil{   fmt.Println("出现了panic,使用reover获取信息:",error)   }         }()         fmt.Println("11111111111")        panic("出现panic")         fmt.Println("22222222222") }

运行结果:

11111111111
出现了panic,使用reover获取信息: 出现panic

        注意,这里有了recover之后,程序不会在panic出中断,再执行完panic之后,会接下来执行defer recover函数,但是当前函数panic后面的代码不会被执行,但是调用该函数的代码会接着执行。
如果我们在main函数中未加入defer func(){...},当我们的程序运行到底8行时就会panic掉,而通常在我们的业务程序中对于程序panic是不可容忍的,我们需要程序健壮的运行,而不是是不是因为一些panic挂掉又被拉起,所以当发生panic的时候我们要让程序能够继续运行,并且获取到发生panic的具体错误,这就可以用上述方法。

panic传递

        当一个函数发生了panic之后,若在当前函数中没有recover,会一直向外层传递直到主函数,如果迟迟没有recover的话,那么程序将终止。如果在过程中遇到了最近的recover,则将被捕获。
看下面例子:

package mainimport "fmt"func testPanic1(){fmt.Println("testPanic1上半部分")testPanic2()fmt.Println("testPanic1下半部分")
}func testPanic2(){defer func() {recover()}()fmt.Println("testPanic2上半部分")testPanic3()fmt.Println("testPanic2下半部分")
}func testPanic3(){fmt.Println("testPanic3上半部分")panic("在testPanic3出现了panic")fmt.Println("testPanic3下半部分")
}func main() {fmt.Println("程序开始")testPanic1()fmt.Println("程序结束")
}

运行结果:

程序开始
testPanic1上半部分
testPanic2上半部分
testPanic3上半部分
testPanic1下半部分
程序结束

解析:
调用链:main-->testPanic1-->testPanic2-->testPanic3,但是在testPanic3中发现了一个panic,由于testPanic3没有recover,向上找,在testPanic2中找到了recoverpanic被捕获了,程序接着运行,由于testPanic3发生了panic,所以不再继续运行,函数跳出返回到testPanic2testPanic2中捕获到了panic,也不会再继续执行,跳出函数testPanic2,到了testPanic1接着运行。

        所以recoverpanic可以总结为以下两点:

        这里的调用链指的是同一个函数中(如果panic是在另外一个go程中,是捕获不到的。即一个go程是无法捕获到另一个go程中的panic)

  1. recover()只能恢复当前函数级以当前函数为首的调用链中的函数中的panic(),恢复后调用当前函数结束,但是调用此函数的函数继续执行
  2. 函数发生了panic之后会一直向上传递,如果直至main函数都没有recover(),程序将终止,如果是碰见了recover(),将被recover捕获。

defer...recover

        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(); e != 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("recover")}}()panic(404)
}

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

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

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

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

        不能。多重 defer 嵌套。 

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

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【go 】数组的多种初始化方式与操作

在 Go 语言中&#xff0c;数组是一种固定长度的数据结构&#xff0c;用于存储相同类型的元素。以下是 Go 中数组的多种初始化方式&#xff0c;结合搜索结果整理如下&#xff1a; &#xff08;一&#xff09;使用 var 关键字声明并初始化数组 使用 var 关键字声明数组时&#xf…

基于Java+MySQL 实现(Web)网上商城

悦桔拉拉商城1. 课设目的可以巩固自己之前所学的知识&#xff0c;以及学习更多的新知识。可以掌握业务流程&#xff0c;学习工作的流程。2. 开发环境硬件环境&#xff1a;Window11 电脑、Centos7.6 服务器软件环境&#xff1a;IntelliJ IDEA 2021.1.3 开发工具JDK 16 运行环境M…

高并发抢单系统核心实现详解:Redisson分布式锁实战

一、方法整体流程解析 #mermaid-svg-MROZ2xF7WaNPaztA {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-MROZ2xF7WaNPaztA .error-icon{fill:#552222;}#mermaid-svg-MROZ2xF7WaNPaztA .error-text{fill:#552222;strok…

Android12 User版本开启adb root, adb remount, su, 关闭selinux

开启adb root 直接看adb源码&#xff1a; __android_log_is_debuggable就是判断ro.debuggable属性值&#xff0c;感兴趣可以在 源码下grep下实现看看。auth_required :在adb源码下定义的全局变量&#xff0c;默认等于true,。看名字就是是否需要用户授权的flag, 这里不再继续跟…

金融专业高分简历撰写指南

一、金融求职简历原则&#xff1a;深度与亮点并存在金融行业求职时&#xff0c;一份出色的简历需突出经历深度与亮点。01 教育背景需如实填写毕业院校、专业、GPA及所学课程。金融行业不少公司对求职者学校和学历有严格标准&#xff0c;如“985”“211”院校或硕士以上学历等。…

专题:2025生命科学与生物制药全景报告:产业图谱、投资方向及策略洞察|附130+份报告PDF、原数据表汇总下载

原文链接&#xff1a;https://tecdat.cn/?p43526 过去一年&#xff0c;全球生命科学VC融资回暖至1021.5亿美元&#xff0c;并购交易虽下滑23%却聚焦关键赛道&#xff0c;创新药管线中GLP-1受体激动剂以170亿美元市场规模领跑&#xff0c;AI技术将研发周期缩短60%……这些数据背…

Compose笔记(四十)--ClickableText

这一节主要了解一下Compose中的ClickableText&#xff0c;在Jetpack Compose中&#xff0c;ClickableText是用于创建可点击文本的组件&#xff0c;其核心功能是通过声明式语法将文本设置为交互式元素&#xff0c;用户点击时可触发特定操作。简单总结如下:API含义 text&#xff…

面试必刷的数组三连:原地删除与合并

坚持用 清晰易懂的图解 多语言代码&#xff0c;让每道题变得简单&#xff01; 呆头个人主页详情 呆头个人Gitee代码仓库 呆头详细专栏系列 座右铭&#xff1a; “不患无位&#xff0c;患所以立。” 面试必刷的数组三连&#xff1a;原地删除与合并前言目录1.移除元素2.删除有序…

力扣经典算法篇-41-旋转图像(辅助数组法,原地旋转法)

1、题干 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1&#xff1a;输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]]…

译|用户增长策略如何使用因果机器学习的案例

来自上传文件中的文章《[Causal Machine Learning for Growth: Loyalty Programs, LTV, and What to Do When You Can’t Experiment | by Torty Sivill | Towards AI]》 本文探讨了当 A/B 测试不可行时&#xff0c;如何利用因果推断从历史数据中获取洞察。技术亮点在于通过构建…

java~final关键字

final关键字final基本介绍final的使用细节final基本介绍 final是最终的意思&#xff0c;可以修饰类&#xff0c;属性&#xff0c;方法&#xff0c;局部变量什么时候会要使用到final呢&#xff1f; 1.想要类不被继承时 2.不希望类的某个属性的值被改变时 3.不想父类的某个方法被…

Node.js(四)之数据库与身份认证

数据库与身份认证 目录 数据库与身份认证 十三、数据库的基本概念 13.1 什么是数据库 13.2 常见的数据库及分类 13.3 传统型数据库的数据组织结构 1. Excel 的数据组织结构 2. 传统型数据库的数据组织结构 3. 实际开发中库、表、行、字段的关系 十四、安装并配置MySQ…

SpringBoot+SpringMVC常用注解

文章目录发展历程项目创建项目结构入门案例配置文件的两种方式&#xff1a;只能使用一种创建项目二入门案例常用知识及注解Controller:类上面加&#xff0c;SpringMVC的注解GetMapping:方法上面加Spring框架的两项核心功能Component:组件。控制反转&#xff0c;加在业务类上面&…

标准GS相位恢复算法

标准GS相位恢复算法详解与MATLAB实现 Gerchberg-Saxton (GS) 算法是一种经典的相位恢复方法&#xff0c;广泛应用于光学成像、衍射成像和全息技术等领域。该算法通过迭代过程从未知相位的强度测量中恢复相位信息。 算法原理 GS算法的核心思想是利用傅里叶变换关系在空间域和频率…

【Linux网络编程基础--socket地址API】

一、主机字节序和网络字节序主机字节序&#xff08;Host Byte Order&#xff09;&#xff1a;你当前电脑的内存字节顺序&#xff08;比如 x86 是小端&#xff09;网络字节序&#xff08;Network Byte Order&#xff09;&#xff1a;统一规定为大端序&#xff08;高位字节在高位…

Linux路径MTU发现(Path MTU Discovery, PMTU)

Linux路径MTU发现&#xff08;Path MTU Discovery, PMTU&#xff09;机制是TCP/IP协议栈中确保数据包高效传输的核心技术。其核心目标是动态探测源主机到目的主机路径上的最小MTU&#xff08;Maximum Transmission Unit&#xff09;&#xff0c;从而避免IP分片&#xff0c;提升…

【MySQL进阶】------MySQL程序

MySQL程序简介 MySQL安装完成通常会包含如下程序&#xff1a; Linux系统程序⼀般在 /usr/bin⽬录下&#xff0c;可以通过命令查看&#xff1a; windows系统⽬录&#xff1a;你的安装路径\MySQL Server 8.0\bin&#xff0c;可以通过命令查看&#xff1a; 每个 MySQL 程序都有许…

Linux大页内存导致服务内存不足

Linux大页内存导致服务内存不足的解决方法 大页内存&#xff08;Huge Pages&#xff09;是Linux内核提供的一种机制&#xff0c;用于减少TLB&#xff08;转换后备缓冲区&#xff09;的压力&#xff0c;提高内存访问性能。然而&#xff0c;如果配置不当&#xff0c;大页内存可能…

超宽带测距+测角+无线通信一体化模组:智能门锁、智能遥控器、AR头戴、智能穿戴

超宽带测距测角无线通信一体化模组&#xff1a;智能门锁、智能遥控器、AR头戴、智能穿戴UWB测距测角技术&#xff0c;因其高精度、低延迟、抗干扰能力&#xff0c;正广泛应用于“人-物-设备”的空间感知场景&#xff0c;成为构建智能空间和精准互动的重要底层技术。代表厂商与产…

基于单片机空气质量检测/气体检测系统

传送门 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品题目速选一览表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品题目功能速览 概述 随着环境污染问题日益严重&#xff0c;空气质量监测成为社会关注的焦点。基于单片机的空气质量检…