Golang原理剖析(defer、defer面试与分析)

文章目录

  • defer是什么​
  • defer的使用形式
  • defer的底层结构
  • defer的执行过程
    • _defer内存分配
      • 堆上分配
      • 栈上分配
      • 开放编码
    • defer函数执行
  • defer面试与分析
  • 1、defer的底层数据结构是怎样的​
  • 2、循环体中能用defer调用吗? 会有什么问题,为什么?
  • 3、defer能修改返回值吗,defer与return的先后关系是怎样的?
  • 4、多个defer的执行顺序是怎样的?

defer是什么​

defer是go语言的一个关键字,用来修饰函数,其作用是让defer后面跟的函数或者方法调用能够延迟到当前所在函数return或者panic的时候再执行。

defer的使用形式

deferfunc(args)

defer在使用的时候,只需要在其后面加上具体的函数调用即可,这样就会注册一个延迟执行的函数func,并且会把函数名和参数都确定,等到从当前函数退出的时候再执行

defer的底层结构

进行defer 函数调用的时候其实会生成一个_defer结构,一个函数中可能有多次defer调用,所以会生成多个这样的_defer结构,这些_defer结构链式存储构成一个_defer链表,当前goroutine的_defer指向这个链表的头节点

_defer 的结构定义在src/src/runtime/runtime2.go中,源码如下:

// A _defer holds an entry on the list of deferred calls.// If you add a field here, add code to clear it in deferProcStack.// This struct must match the code in cmd/compile/internal/ssagen/ssa.go:deferstruct// and cmd/compile/internal/ssagen/ssa.go:(*state).call.// Some defers will be allocated on the stack and some on the heap.// All defers are logically part of the stack, so write barriers to// initialize them are not required. All defers must be manually scanned,// and for heap defers, marked.type_deferstruct{// 标记位,标志当前defer结构是否是分配在堆上heapboolrangefuncbool// true for rangefunc list// 调用方的sp寄存器指针,即栈指针spuintptr// sp at time of defer// 调用方的程序计数器指针pcuintptr// pc at time of defer// defer注册的延迟执行的函数fnfunc()// can be nil for open-coded defers// defer链表link*_defer// next defer on G; can point to either heap or stack!// If rangefunc is true, *head is the head of the atomic linked list// during a range-over-func execution.head*atomic.Pointer[_defer]}

底层存储如下图:

defer函数在注册的时候,创建的_defer结构会依次插入到_defer链表的表头,在当前函数return的时候,依次从_defer链表的表头取出_defer结构执行里面的fn函数

头插法(push 到链表头),所以 defer 的执行顺序是后进先出 LIFO

defer的执行过程

在探究defer的执行过程之前,先简单看一下go语言程序的编译过程,go语言程序由.go文件编译成最终的二进制机器码主要有以下结果步骤

defer关键字的处理在生成SSA中间代码阶段,编译器遇到defer 语句的时候,会插入两种函数

  1. defer内存分配函数:deferproc(堆分配)或 deferprocStack(栈分配)

  2. 执行函数:deferreturn

下面分别看一下这两种函数的执行过程

defer的处理逻辑在cmd/compile/internal/ssagen/ssa.go文件中的state.stmt()方法中,由于源码过长,这里只贴部分重要代码:

caseir.ODEFER:// 如果节点defer节点n:=n.(*ir.GoDeferStmt)ifbase.Debug.Defer>0{vardefertypestringifs.hasOpenDefers{defertype="open-coded"// 开放编码}elseifn.Esc()==ir.EscNever{defertype="stack-allocated"// 栈分配}else{defertype="heap-allocated"// 堆分配}base.WarnfAt(n.Pos(),"%s defer",defertype)}ifs.hasOpenDefers{// 如果可以开放编码,即内联实现s.openDeferRecord(n.Call.(*ir.CallExpr))// 就使用开放编码这种方式}else{d:=callDefer// 否则先默认使用堆分配的模式ifn.Esc()==ir.EscNever{// 没有内存逃逸,使用栈分配的方式实现d=callDeferStack}s.callResult(n.Call.(*ir.CallExpr),d)}

从上述代码可以看出,defer的是现有三种实现方式,在栈上分配内存,在堆上分配内存以及使用开放编码的方式。
会优先使用内联方式,当内联不满足,且没有发生内存逃逸的情况下,使用栈分配的方式,这两种情况都不符合的情况下在使用堆分配,这样做的好处是提升性能。

_defer内存分配

defer 的实现方式由编译器决定:优先使用 open-coded defer;如果不能开放编码,则根据逃逸分析决定 _defer 记录是栈上分配还是堆上分配。是否堆分配取决于 defer 记录及其捕获的环境/参数是否会逃逸,而不是简单取决于被 defer 的函数“是否简单”或“函数内部是否动态分配”。

在上面的分析中我们可以看出在不同的情况下,_defer结构分配在不同的地方,可能分配在堆上也可能分配在栈上,这两种分配方式调用的函数是不同的,堆上分配实际调用的是 runtime.deferproc 函数,栈上分配内存调用的是 runtime.deferprocStack 函数,下面分别来看看这两个函数都做了些什么工作?

堆上分配

先看deferproc 函数,在堆上分配内存,go 1.13 之前只有这个函数,说明go 1.13 之前,_defer只能在堆上分配。

src/runtime/panic.go

// Create a new deferred function fn, which has no arguments and results.// The compiler turns a defer statement into a call to this.funcdeferproc(fnfunc()){// 获取goroutine,defer在哪个goroutine中执行gp:=getg()ifgp.m.curg!=gp{// go code on the system stack can't deferthrow("defer on system stack")}// 在堆中创建一个_defer对象d:=newdefer()// 将这个新建的defer对象加入到goroutine的defer链表头部d.link=gp._defer gp._defer=d d.fn=fn d.pc=sys.GetCallerPC()// We must not be preempted between calling GetCallerSP and// storing it to d.sp because GetCallerSP's result is a// uintptr stack pointer.d.sp=sys.GetCallerSP()}

重点看一下newdefer()这个函数

// Each P holds a pool for defers.// Allocate a Defer, usually using per-P pool.// Each defer must be released with freedefer. The defer is not// added to any defer chain yet.funcnewdefer()*_defer{vard*_defer mp:=acquirem()// 获取逻辑处理器Ppp:=mp.p.ptr()// p的本地defer缓存池为空且全局defer缓存池不为空,从全局defer缓存池取出一个defer结构加入到p的本地defer缓存池iflen(pp.deferpool)==0&&sched.deferpool!=nil{lock(&sched.deferlock)forlen(pp.deferpool)<cap(pp.deferpool)/2&&sched.deferpool!=nil{d:=sched.deferpool sched.deferpool=d.link d.link=nilpp.deferpool=append(pp.deferpool,d)}unlock(&sched.deferlock)}// p的本地defer缓存池取出一个defer结构ifn:=len(pp.deferpool);n>0{d=pp.deferpool[n-1]pp.deferpool[n-1]=nilpp.deferpool=pp.deferpool[:n-1]}releasem(mp)mp,pp=nil,nil// p的本地defer缓存池和全局defer缓存池都没有可用的defer结构,在堆上创建一个ifd==nil{// Allocate new defer.d=new(_defer)}d.heap=truereturnd}

可以看出堆上defer的创建思想借助了内存复用,用到了内存池的思想,创建defer的过程是:优先在P的本地和全局的defer缓存池里找到一个可用的defer结构返回,找不到在去堆上创建

栈上分配

下面看一下 runtime.deferprocStack 函数,在栈上分配_defer,这个函数是go 1.13 之后引入的,优化defer性能的,显然在栈上分配的效率更高

runtime.deferprocStack 源码如下:

// deferprocStack queues a new deferred function with a defer record on the stack.// The defer record must have its fn field initialized.// All other fields can contain junk.// Nosplit because of the uninitialized pointer fields on the stack.////go:nosplit// 在调用这个函数之前,defer结构已经在栈上创建好,这里只是作为参数传进来赋值funcdeferprocStack(d*_defer){// 获取goroutine,defer在哪个goroutine中执行gp:=getg()ifgp.m.curg!=gp{// go code on the system stack can't deferthrow("defer on system stack")}// fn is already set.// The other fields are junk on entry to deferprocStack and// are initialized here.// 堆上分配为falsed.heap=falsed.rangefunc=falsed.sp=sys.GetCallerSP()d.pc=sys.GetCallerPC()// The lines below implement:// d.panic = nil// d.fd = nil// d.link = gp._defer// d.head = nil// gp._defer = d// But without write barriers. The first three are writes to// the stack so they don't need a write barrier, and furthermore// are to uninitialized memory, so they must not use a write barrier.// The fourth write does not require a write barrier because we// explicitly mark all the defer structures, so we don't need to// keep track of pointers to them with a write barrier.*(*uintptr)(unsafe.Pointer(&d.link))=uintptr(unsafe.Pointer(gp._defer))*(*uintptr)(unsafe.Pointer(&d.head))=0*(*uintptr)(unsafe.Pointer(&gp._defer))=uintptr(unsafe.Pointer(d))}

Go 在编译的时候在 SSA中间代码阶段,如果判断出_defer需要在栈上分配,则编译器会直接在函数调用栈上初始化 _defer 记录,并作为参数传递给 deferprocStack 函数。

开放编码

再看一下defer的第三种实现方式,开放编码。这种方式是在go1.14 引入的继续优化defer实现性能的方式。在go1.14 中通过代码内联优化,使得函数末尾直接对 defer 函数进行调用,减少了函数调用开销。其主要逻辑位于 cmd/compile/internal/walk/stmt.go文件的 walkStmt()函数和 cmd/compile/internal/ssagen/ssa.go 的 buildssa()函数,函数较长,这里看一下关键代码。

walkStmt()函数:

caseir.ODEFER:// 如果节点defer节点n:=n.(*ir.GoDeferStmt)ir.CurFunc.SetHasDefer(true)ir.CurFunc.NumDefers++ifir.CurFunc.NumDefers>maxOpenDefers{// maxOpenDefers = 8// defer函数的个数多余8个时,不能用开放编码模式ir.CurFunc.SetOpenCodedDeferDisallowed(true)}ifn.Esc()!=ir.EscNever{// If n.Esc is not EscNever, then this defer occurs in a loop,// so open-coded defers cannot be used in this function.ir.CurFunc.SetOpenCodedDeferDisallowed(true)}fallthrough

这里分析一下 n.Esc() != ir.EscNever 这个条件:
通过源码注释可以看到,这里其实就是判断defer是否在循环体内,因为 defer 在 for 循环中调用,编译器不确定会执行多少次,会逃逸到堆上,这样defer就只能分配在堆中了。所以在使用defer 延迟调用的时候,尽量不要在循环中使用,否则可能导致性能问题。

buildssa()函数:

// build时候的没有设置-N,允许内联s.hasOpenDefers=base.Flag.N==0&&s.hasdefer&&!s.curfn.OpenCodedDeferDisallowed()switch{casebase.Debug.NoOpenDefer!=0:s.hasOpenDefers=falsecases.hasOpenDefers&&(base.Ctxt.Flag_shared||base.Ctxt.Flag_dynlink)&&base.Ctxt.Arch.Name=="386":// Don't support open-coded defers for 386 ONLY when using shared// libraries, because there is extra code (added by rewriteToUseGot())// preceding the deferreturn/ret code that we don't track correctly.//// TODO this restriction can be removed given adjusted offset in computeDeferReturn in cmd/link/internal/ld/pcln.gos.hasOpenDefers=false}ifs.hasOpenDefers&&s.instrumentEnterExit{// Skip doing open defers if we need to instrument function// returns for the race detector, since we will not generate that// code in the case of the extra deferreturn/ret segment.s.hasOpenDefers=false}ifs.hasOpenDefers{// Similarly, skip if there are any heap-allocated result// parameters that need to be copied back to their stack slots.for_,f:=ranges.curfn.Type().Results(){if!f.Nname.(*ir.Name).OnStack(){s.hasOpenDefers=falsebreak}}}// defer所在函数返回值个数和defer函数个数乘积不能大于15ifs.hasOpenDefers&&s.curfn.NumReturns*s.curfn.NumDefers>15{// Since we are generating defer calls at every exit for// open-coded defers, skip doing open-coded defers if there are// too many returns (especially if there are multiple defers).// Open-coded defers are most important for improving performance// for smaller functions (which don't have many returns).s.hasOpenDefers=false}

总结一下:在go1.14之后,go会优先采用内联的方式处理defer函数调用,但是需要满足以下几个条件:
• build编译的时候没有设置 -N
• defer 函数个数没有超过 8 个
• defer所在函数返回值个数和defer函数个数乘积不超过15
• defer没有出现在循环语句中

defer函数执行

在给defer分配好内存之后,剩下的就是执行了。在函数退出的时候,deferreturn 来执行defer链表上的各个defer函数。函数源码如下:

// deferreturn runs deferred functions for the caller's frame.// The compiler inserts a call to this at the end of any// function which calls defer.funcdeferreturn(){varp _panic p.deferreturn=true// 遍历goroutine的defer链表p.start(sys.GetCallerPC(),unsafe.Pointer(sys.GetCallerSP()))for{fn,ok:=p.nextDefer()if!ok{break}// 执行函数调用fn()}}// start initializes a panic to start unwinding the stack.//// If p.goexit is true, then start may return multiple times.func(p*_panic)start(pcuintptr,sp unsafe.Pointer){gp:=getg()// Record the caller's PC and SP, so recovery can identify panics// that have been recovered. Also, so that if p is from Goexit, we// can restart its defer processing loop if a recovered panic tries// to jump past it.p.startPC=sys.GetCallerPC()p.startSP=unsafe.Pointer(sys.GetCallerSP())ifp.deferreturn{// 获取调用栈的栈顶指针p.sp=sp// 开放编码模式 内联处理ifs:=(*savedOpenDeferState)(gp.param);s!=nil{// recovery saved some state for us, so that we can resume// calling open-coded defers without unwinding the stack.gp.param=nilp.retpc=s.retpc p.deferBitsPtr=(*byte)(add(sp,s.deferBitsOffset))p.slotsPtr=add(sp,s.slotsOffset)}return}p.link=gp._panic gp._panic=(*_panic)(noescape(unsafe.Pointer(p)))// Initialize state machine, and find the first frame with a defer.//// Note: We could use startPC and startSP here, but callers will// never have defer statements themselves. By starting at their// caller instead, we avoid needing to unwind through an extra// frame. It also somewhat simplifies the terminating condition for// deferreturn.p.lr,p.fp=pc,sp p.nextFrame()}// nextDefer returns the next deferred function to invoke, if any.//// Note: The "ok bool" result is necessary to correctly handle when// the deferred function itself was nil (e.g., "defer (func())(nil)").func(p*_panic)nextDefer()(func(),bool){gp:=getg()if!p.deferreturn{ifgp._panic!=p{throw("bad panic stack")}ifp.recovered{mcall(recovery)// does not returnthrow("recovery failed")}}// The assembler adjusts p.argp in wrapper functions that shouldn't// be visible to recover(), so we need to restore it each iteration.p.argp=add(p.startSP,sys.MinFrameSize)for{forp.deferBitsPtr!=nil{bits:=*p.deferBitsPtr// Check whether any open-coded defers are still pending.//// Note: We need to check this upfront (rather than after// clearing the top bit) because it's possible that Goexit// invokes a deferred call, and there were still more pending// open-coded defers in the frame; but then the deferred call// panic and invoked the remaining defers in the frame, before// recovering and restarting the Goexit loop.ifbits==0{p.deferBitsPtr=nilbreak}// Find index of top bit set.i:=7-uintptr(sys.LeadingZeros8(bits))// Clear bit and store it back.bits&^=1<<i*p.deferBitsPtr=bitsreturn*(*func())(add(p.slotsPtr,i*goarch.PtrSize)),true}Recheck:ifd:=gp._defer;d!=nil&&d.sp==uintptr(p.sp){ifd.rangefunc{deferconvert(d)popDefer(gp)gotoRecheck}// 非内联模式// 获取defer的执行函数fn:=d.fn p.retpc=d.pc// Unlink and free.popDefer(gp)returnfn,true}if!p.nextFrame(){returnnil,false}}}// popDefer pops the head of gp's defer list and frees it.funcpopDefer(gp*g){d:=gp._defer// defer上的函数指针置空d.fn=nil// Can in theory point to the stack// We must not copy the stack between the updating gp._defer and setting// d.link to nil. Between these two steps, d is not on any defer list, so// stack copying won't adjust stack pointers in it (namely, d.link). Hence,// if we were to copy the stack, d could then contain a stale pointer.// 遍历下一个defer结构gp._defer=d.link d.link=nil// After this point we can copy the stack.if!d.heap{return}mp:=acquirem()pp:=mp.p.ptr()iflen(pp.deferpool)==cap(pp.deferpool){// Transfer half of local cache to the central cache.varfirst,last*_deferforlen(pp.deferpool)>cap(pp.deferpool)/2{n:=len(pp.deferpool)d:=pp.deferpool[n-1]pp.deferpool[n-1]=nilpp.deferpool=pp.deferpool[:n-1]iffirst==nil{first=d}else{last.link=d}last=d}lock(&sched.deferlock)last.link=sched.deferpool sched.deferpool=firstunlock(&sched.deferlock)}*d=_defer{}// 释放defer结构,优先归还到defer缓冲池中pp.deferpool=append(pp.deferpool,d)releasem(mp)mp,pp=nil,nil}

当 go函数的 return 关键字执行的时候,触发 call 调用 deferreturn 函数,deferreturn函数的执行逻辑也很简单,就是遍历goroutine上的defer链表,从表头开始遍历,依次取出defer结构执行defer结构中的函数执行。

总结:

  1. 遇到defer关键字,编译器会在编译阶段注册defer函数的时候插入 deferproc() 函数或者 deferprocStack 函数,在return之前插入deferreturn()函数

  2. defer函数的执行顺序是LIFO的,因为每次创建的defer结构都是插入到goroutine的defer链表表头

  3. defer结构的有三种实现方式,堆上分配,栈上分配还有内联实现

非内联模式defer结构上的fn字段就是对应的defer执行函数 ​ 而内联模式直接将defer函数展开在函数里面 ​ 你可以看到非内联模式调用的是defer结构里面的fn函数 ​ 而内联模式是直接运行的栈帧某段代码(defer函数展开的内容)

defer面试与分析

1、defer的底层数据结构是怎样的​

回顾这个图:

每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单链表,保存在 goroutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果

2、循环体中能用defer调用吗? 会有什么问题,为什么?

循环体中不要使用defer调用语句,一方面是会影响性能,另一方面是可能会发生一些意想不到的结果

首先,在循环中使用defer可能会发生内存逃逸(逃逸就是“编译器不敢放栈上,只能放堆上”,代价通常是更多分配和 GC,性能会变差。),这样defer有可能分配到堆上,相比于栈上分配和内联方式,是性能最差的一种内存分配方式,会导致程序的性能问题

另外,可能会带来一些系统问题。比如在一个循环中,用defer函数来操作文件,如下:

for_,filename:=rangefilenames{f,err:=os.Open(filename)iferr!=nil{returnerr}deferf.Close()}

这段代码很可能会用尽所有文件描述符。因为 defer 语句不到函数的最后一刻是不会执行的,也就是说文件始终得不到关闭

3、defer能修改返回值吗,defer与return的先后关系是怎样的?

defer可以修改返回值,当函数的返回值是非匿名的,有显示返回值的时候,defer可以修改返回值。函数的返回其实不是一个原子操作,可以理解为三个步骤:

  1. 设置返回值

  2. 执行defer语句

  3. 将结果返回

4、多个defer的执行顺序是怎样的?

后进先出,类似于栈,先调用的defer语句后执行

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

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

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

相关文章

攻防世界backup

1.进入发现什么都没有2.根据提示查看其备份文件名 常见备份后缀名有:.git .svn .swp .svn .~ .bak .bash_history .php 一. .git 【最高危 - 源码泄露】 含义:Git 版本控制系统的核心目录,项目的「所有版本历…

[python] python-docx-template模板化Word文档生成指北

python-docx库的核心功能是程序化创建全新的Word文档,但在基于已有模板替换其部分内容时,其操作会非常繁琐。用户需要先解析文档结构、定位具体位置、手动替换内容,并维护原有格式与布局,导致开发效率较低。相关使…

gitee分支

核心操作命令与步骤总结 一、分支创建与推送 1. 基于已有分支创建并切换新分支 # 切换到基础分支A git checkout 分支A名称 # 拉取分支A最新远端代码 git pull origin 分支A名称 # 创建并切换到新分支B git checkout -…

Manus官方揭秘Sandbox云计算机:智能体的云端 AI 助手与智能计算环境

Understanding Manus Sandbox - Your Cloud Computer摘要Manus现已成为Meta的一部分&#xff0c;推出革命性的Manus Sandbox云虚拟机服务。作为AI Agent的"手"&#xff0c;Sandbox为每个任务提供完全独立的云计算环境&#xff0c;具备完整的网络、文件系统和软件工具…

【数字信号去噪】基于matlab吕佩尔狐算法优化变分模态分解RFO-VMD数字信号去噪(优化K值 alpha值 综合指标 适应度函数包络熵)【含Matlab源码 14994期】

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到海神之光博客之家&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49…

day08-工作流和智能体发布

今日内容 1 12306出行建议工作流 # 1 出行之前先查天气,天气合适,查询车次,找到有座的车次,做推荐1.1 步骤 # 1 开始节点-出行时间-出发地-目的地# 2 查询天气插件-只查询出发地天气-如果同学想查询两个地的天气-…

线段树的构建与使用

线段树的构建与使用线段树是指如下图所示的数据结构:其中,对于每个标号为n,左端点是l,右端点是r的节点有:子树 标号 左端点 右端点左子树 2*n l floor((l+r)/2)右子树 2*n+1 floor((l+r)/2)+1 r使用线段树,我们可…

炒股别太努力:量化交易正在“收割”最认真的投资者?

当勤奋成为亏损的陷阱在多数领域&#xff0c;深入研究和勤奋分析是通往成功的不二法门。我们从小就被教导&#xff0c;付出越多&#xff0c;收获越大。然而&#xff0c;在当前的A股市场&#xff0c;这个看似颠扑不破的逻辑可能正在失效&#xff0c;甚至会适得其反。当下的市场主…

LP3716CK隔离型10W/12W极简化自供电原边反馈控制芯片解析

LP3716CK是芯茂微推出的“极简型”隔离型原边反馈&#xff08;PSR&#xff09;PWM功率开关&#xff0c;单颗SOP8L即可实现10W/12W适配器或LED驱动电源。它把高压启动、功率BJT、CV/CC环路、线损补偿、全套保护全部集成&#xff0c;外围仅需10颗元件&#xff0c;BOM成本比传统方…

手把手搭建本地RAG知识库!实现文档秒检索。

文章详细介绍如何使用开源模型nomic-embed-text搭建本地RAG知识库&#xff0c;实现高效文档检索。内容包括模型基本信息、特性对比和应用场景&#xff0c;以及完整搭建步骤&#xff1a;下载模型、创建工作区、上传文档、向量化存储和检索测试。同时提供了两种使用方式&#xff…

VP引导定位软件-定位纠偏(带角度)

VP引导定位软件-定位纠偏&#xff08;带角度&#xff09;/// <summary>/// 计算物理旋转之后点xy的变化/// </summary>/// <param name"x0">图像物体上一点的x</param>/// <param name"y0">图像物体上一点的y</param>…

使用MCP执行代码:让Agent效率提升98.7%

Anthropic推出的Model Context Protocol (MCP)面临大规模工具连接的性能瓶颈。通过将MCP服务器呈现为代码API&#xff0c;实现了98.7%的token使用率降低。这一创新架构实现了五大优势&#xff1a;渐进式工具披露、高效数据处理、强大控制流、隐私保护和状态持久化&#xff0c;使…

PL3327系列(PL3327CD/CS/CE/CF) 18W AC/DC反激式开关电源芯片方案

PL3327是聚元微推出的「原边控制 内置650V MOSFET」反激式功率开关系列&#xff0c;涵盖DIP7、SOP7、SOP8四种封装&#xff0c;单颗芯片即可输出5V-24V/18W以内电源。它把传统方案中的光耦、TL431、高压MOSFET、启动电阻全部省掉&#xff0c;BOM从25颗压缩到12颗&#xff0c;峰…

基于YOLOv8的交通事故车辆损伤检测与事故严重程度分级项目识别项目

基于YOLOv8的交通事故车辆损伤检测与事故严重程度分级项目识别项目&#xff5c;完整源码数据集PyQt5界面完整训练流程开箱即用&#xff01; 基本功能演示 https://www.bilibili.com/video/BV1yakuB6EJt/ 项目摘要 本项目围绕 交通事故车辆损伤检测与事故严重程度分级 这一典…

具备这5大潜质的人,天生就是卖货王者

电商行业竞争日益激烈&#xff0c;80%的中小企业在招聘电商人才时面临"看走眼"的困境。传统面试主观性强、评价标准模糊&#xff0c;导致企业招错人后平均试错成本高达3-6个月薪资&#xff0c;这对资源有限的中小企业而言是难以承受的代价。如何科学识别真正具备电商…

Uniapp苹果内购支付全流程指南:从集成到配置的完整复盘

引言在移动应用开发中&#xff0c;虚拟商品支付是核心功能之一&#xff0c;而针对iOS平台&#xff0c;苹果App Store强制要求虚拟商品必须通过其官方内购渠道完成交易&#xff0c;这使得Uniapp项目集成苹果内购成为iOS端开发的必备技能。本文将全面复盘Uniapp苹果内购支付的完整…

哈尔滨特色美食口碑大赏!对青烤鹅力断层领先,成游客必打卡爆款 - 资讯焦点

哈尔滨特色美食口碑大赏!对青烤鹅力断层领先,成游客必打卡爆款 基于近期市场消费数据、游客到店打卡率、本地口碑反馈及主流生活平台综合评价,现对哈尔滨市域内具有广泛影响力的特色熟食品牌进行梳理与评估。本排名…

深入解析:基于非官方接口的企业微信外部群批量创建与效率重构

深入解析:基于非官方接口的企业微信外部群批量创建与效率重构pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Con…

【数据分析】基于matlab辅导功能和ISSR-MDF模型的综合预警指标【含Matlab源码 14993期】

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到海神之光博客之家&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49…

git 如何切换到123分支?

git branch -rorigin/HEAD -> origin/masterorigin/123git checkout -b pci origin/123branch 123 set up to track origin/123.Switched to a new branch 123git branchmaster123