go中的关键字-defer

1. defer的使用

  defer 延迟调用。我们先来看一下,有defer关键字的代码执行顺序:

func main() {
    defer func() {
        fmt.Println("1号输出")
    }()
    defer func() {
        fmt.Println("2号输出")
    }()
}

  输出结果:

2号出来
1号出来

  结论:多个defer的执行顺序是倒序执行(同入栈先进后出)。

  由例子可以看出来,defer有延迟生效的作用,先使用defer的语句延迟到最后执行。

1.1 defer与返回值之间的顺序

func main() {
    fmt.Println("main:", defertest())
}

func defertest() int {
    var i int
    defer func() {
        i++
        fmt.Println("defer2的值:", i)
    }()
    defer func() {
        i++
        fmt.Println("defer1的值:", i)
    }()
    return i
}

 输出结果:

 defer1的值: 1
 defer2的值: 2
 main: 0

  结论:return最先执行->return负责将结果写入返回值中->接着defer开始执行一些收尾工作->最后函数携带当前返回值退出

   return的时候已经先将返回值给定义下来了,就是0,由于i是在函数内部声明所以即使在defer中进行了++操作,也不会影响return的时候做的决定。

func main() {
    fmt.Println("main:", test())
}

func test() (i int) {
    defer func() {
        i++
        fmt.Println("defer2的值:", i)
    }()
    defer func() {
        i++
        fmt.Println("defer1的值:", i)
    }()
    return i
}

  详解:由于返回值提前声明了,所以在return的时候决定的返回值还是0,但是后面两个defer执行后进行了两次++,将i的值变为2,待defer执行完后,函数将i值进行了返回。

2. defer定义和执行

func test(i *int) int {
    return *i
}

func main(){
    var i = 1

    // defer定义的时候test(&i)的值就已经定了,是1,后面就不会变了
    defer fmt.Println("i1 ="  , test(&i))
    i++

    // defer定义的时候test(&i)的值就已经定了,是2,后面就不会变了
    defer fmt.Println("i2 ="  , test(&i))

    // defer定义的时候,i就已经确定了是一个指针类型,地址上的值变了,这里跟着变
    defer func(i *int) {
        fmt.Println("i3 ="  , *i)
    }(&i)

    // defer定义的时候i的值就已经定了,是2,后面就不会变了
    defer func(i int) {
        //defer 在定义的时候就定了
        fmt.Println("i4 ="  , i)
    }(i)

    defer func() {
        // 地址,所以后续跟着变
        var c = &i
        fmt.Println("i5 ="  , *c)
    }()

    // 执行了 i=11 后才调用,此时i值已是11
    defer func() {
        fmt.Println("i6 ="  , i)
    }()

    i = 11
}

  结论:会先将defer后函数的参数部分的值(或者地址)给先下来【你可以理解为()里头的会先确定】,后面函数执行完,才会执行defer后函数的{}中的逻辑。

例题分析

//例子1
func f() (result int) {
    defer func() {
        result++
    }()
    return 0
}
//例子2
func f() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}
//例子3
func f() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return 1
}

  例1的正确答案不是0,例2的正确答案不是10,例3的正确答案不是6......

  这里先说一下返回值。defer是在return之前执行的。这条规则毋庸置疑,但最重要的一点是要明白,return xxx这一条语句并不是一条原子指令!

  函数返回的过程:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。defer表达式可能会在设置函数返回值之后,且在返回到调用函数之前去修改返回值,使最终的函数返回值与你想象的不一致。

  return xxx 可被改写成:

 返回值 = xxx
 调用defer函数
 空的return

  所以例子也可以改写成:

//例1
func f() (result int) {
     result = 0  //return语句不是一条原子调用,return xxx其实是赋值+ret指令
     func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
         result++
     }()
     return
}
//例2
func f() (r int) {
     t := 5
     r = t //赋值指令
     func() {        //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
         t = t + 5
     }
     return        //空的return指令
}
例3
func f() (r int) {
     r = 1  //给返回值赋值
     func(r int) {        //这里改的r是传值传进去的r,不会改变要返回的那个r值
          r = r + 5
     }(r)
     return        //空的return
}

  所以例1的结果是1,例2的结果是5,例3的结果是1.

3. defer内部原理

  从例子开始看:

package main

import()

func main() {
  defer println("这是一个测试")
}

  反编译一下看看:

➜  src $ go build -o test test.go
➜  src $ go tool objdump -s "main\.main" test
TEXT main.main(SB) /Users/tushanshan/go/src/test3.go
  test3.go:5        0x104ea70        65488b0c2530000000      MOVQ GS:0x30, CX
  test3.go:5        0x104ea79        483b6110                CMPQ 0x10(CX), SP
  test3.go:5        0x104ea7d        765f                    JBE 0x104eade
  test3.go:5        0x104ea7f        4883ec28                SUBQ $0x28, SP
  test3.go:5        0x104ea83        48896c2420              MOVQ BP, 0x20(SP)
  test3.go:5        0x104ea88        488d6c2420              LEAQ 0x20(SP), BP
  test3.go:6        0x104ea8d        c7042410000000          MOVL $0x10, 0(SP)
  test3.go:6        0x104ea94        488d05e5290200          LEAQ go.func.*+57(SB), AX
  test3.go:6        0x104ea9b        4889442408              MOVQ AX, 0x8(SP)
  test3.go:6        0x104eaa0        488d05e6e50100          LEAQ go.string.*+173(SB), AX
  test3.go:6        0x104eaa7        4889442410              MOVQ AX, 0x10(SP)
  test3.go:6        0x104eaac        48c744241804000000      MOVQ $0x4, 0x18(SP)
  test3.go:6        0x104eab5        e8b631fdff              CALL runtime.deferproc(SB)
  test3.go:6        0x104eaba        85c0                    TESTL AX, AX
  test3.go:6        0x104eabc        7510                    JNE 0x104eace
  test3.go:7        0x104eabe        90                      NOPL
  test3.go:7        0x104eabf        e83c3afdff              CALL runtime.deferreturn(SB)
  test3.go:7        0x104eac4        488b6c2420              MOVQ 0x20(SP), BP
  test3.go:7        0x104eac9        4883c428                ADDQ $0x28, SP
  test3.go:7        0x104eacd        c3                      RET
  test3.go:6        0x104eace        90                      NOPL
  test3.go:6        0x104eacf        e82c3afdff              CALL runtime.deferreturn(SB)
  test3.go:6        0x104ead4        488b6c2420              MOVQ 0x20(SP), BP
  test3.go:6        0x104ead9        4883c428                ADDQ $0x28, SP
  test3.go:6        0x104eadd        c3                      RET
  test3.go:5        0x104eade        e8cd84ffff              CALL runtime.morestack_noctxt(SB)
  test3.go:5        0x104eae3        eb8b                    JMP main.main(SB)
  :-1               0x104eae5        cc                      INT $0x3
  :-1               0x104eae6        cc                      INT $0x3
  :-1               0x104eae7        cc                      INT $0x3

   编译器将defer处理成两个函数调用 deferproc 定义一个延迟调用对象,然后在函数结束前通过 deferreturn 完成最终调用。在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。

内部结构

//defer
type _defer struct {
   siz     int32   // 参数的大小
   started bool    // 是否执行过了
   sp      uintptr // sp at time of defer
   pc      uintptr
   fn      *funcval 
   _panic  *_panic // defer中的panic
   link    *_defer // defer链表,函数执行流程中的defer,会通过 link这个 属性进行串联
}
//panic
type _panic struct {
   argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
   arg       interface{}    // argument to panic
   link      *_panic        // link to earlier panic
   recovered bool           // whether this panic is over
   aborted   bool           // the panic was aborted
}
//g
type g struct {
   _panic         *_panic // panic组成的链表
   _defer         *_defer // defer组成的先进后出的链表,同栈
}

  因为 defer panic 都是绑定在运行的g上的,这里也说一下g中与 defer panic相关的属性

  再把defer, panic, recover放一起看一下:

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

  反编译结果:

go build -gcflags=all="-N -l" main.go
go tool objdump -s "main.main" main
go tool objdump -s "main\.main" main | grep CALL
  main.go:4             0x4548d0                e81b00fdff              CALL runtime.deferproc(SB)              
  main.go:7             0x4548f2                e8b90cfdff              CALL runtime.gopanic(SB)                
  main.go:4             0x4548fa                e88108fdff              CALL runtime.deferreturn(SB)            
  main.go:3             0x454909                e85282ffff              CALL runtime.morestack_noctxt(SB)       
  main.go:5             0x4549a6                e8d511fdff              CALL runtime.gorecover(SB)              
  main.go:4             0x4549b5                e8a681ffff              CALL runtime.morestack_noctxt(SB)

  defer 关键字首先会调用 runtime.deferproc 定义一个延迟调用对象,然后再函数结束前,调用 runtime.deferreturn 来完成 defer 定义的函数的调用

  panic 函数就会调用 runtime.gopanic 来实现相关的逻辑

  recover 则调用 runtime.gorecover 来实现 recover 的功能

deferproc

  根据 defer 关键字后面定义的函数 fn 以及 参数的size,来创建一个延迟执行的 函数,并将这个延迟函数,挂在到当前g的 _defer 的链表上,下面是deferproc的实现:

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
   sp := getcallersp()
   argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
   callerpc := getcallerpc()
   // 获取一个_defer对象, 并放入g._defer链表的头部
   d := newdefer(siz)
     // 设置defer的fn pc sp等,后面调用
   d.fn = fn
   d.pc = callerpc
   d.sp = sp
   switch siz {
   case 0:
      // Do nothing.
   case sys.PtrSize:
      // _defer 后面的内存 存储 argp的地址信息
      *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
   default:
      // 如果不是指针类型的参数,把参数拷贝到 _defer 的后面的内存空间
      memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
   }
   return0()
}

  通过newproc 获取一个 _defer 的对象,并加入到当前g的 _defer 链表的头部,然后再把参数或参数的指针拷贝到 获取到的 _defer对象的后面的内存空间。再看看newdefer 的实现:

func newdefer(siz int32) *_defer {
   var d *_defer
   // 根据 size 通过deferclass判断应该分配的 sizeclass,就类似于 内存分配预先确定好几个sizeclass,然后根据size确定sizeclass,找对应的缓存的内存块
   sc := deferclass(uintptr(siz))
   gp := getg()
   // 如果sizeclass在既定的sizeclass范围内,去g绑定的p上找
   if sc < uintptr(len(p{}.deferpool)) {
      pp := gp.m.p.ptr()
      if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
         // 当前sizeclass的缓存数量==0,且不为nil,从sched上获取一批缓存
         systemstack(func() {
            lock(&sched.deferlock)
            for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
               d := sched.deferpool[sc]
               sched.deferpool[sc] = d.link
               d.link = nil
               pp.deferpool[sc] = append(pp.deferpool[sc], d)
            }
            unlock(&sched.deferlock)
         })
      }
      // 如果从sched获取之后,sizeclass对应的缓存不为空,分配
      if n := len(pp.deferpool[sc]); n > 0 {
         d = pp.deferpool[sc][n-1]
         pp.deferpool[sc][n-1] = nil
         pp.deferpool[sc] = pp.deferpool[sc][:n-1]
      }
   }
   // p和sched都没有找到 或者 没有对应的sizeclass,直接分配
   if d == nil {
      // Allocate new defer+args.
      systemstack(func() {
         total := roundupsize(totaldefersize(uintptr(siz)))
         d = (*_defer)(mallocgc(total, deferType, true))
      })
   }
   d.siz = siz
   // 插入到g._defer的链表头
   d.link = gp._defer
   gp._defer = d
   return d
}

  newdefer的作用是获取一个_defer对象, 并推入 g._defer链表的头部。根据size获取sizeclass,对sizeclass进行分类缓存,这是内存分配时的思想,先去p上分配,然后批量从全局 sched上获取到本地缓存,这种二级缓存的思想真的在go源码的各个部分都有。

deferreturn

func deferreturn(arg0 uintptr) {
   gp := getg()
   // 获取g defer链表的第一个defer,也是最后一个声明的defer
   d := gp._defer
   // 没有defer,就不需要干什么事了
   if d == nil {
      return
   }
   sp := getcallersp()
   // 如果defer的sp与callersp不匹配,说明defer不对应,有可能是调用了其他栈帧的延迟函数
   if d.sp != sp {
      return
   }
   // 根据d.siz,把原先存储的参数信息获取并存储到arg0里面
   switch d.siz {
   case 0:
      // Do nothing.
   case sys.PtrSize:
      *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
   default:
      memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
   }
   fn := d.fn
   d.fn = nil
   // defer用过了就释放了,
   gp._defer = d.link
   freedefer(d)
   // 跳转到执行defer
   jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

freedefer

  释放defer用到的函数,应该跟调度器、内存分配的思想是一样的。

func freedefer(d *_defer) {
   // 判断defer的sizeclass
   sc := deferclass(uintptr(d.siz))
   // 超出既定的sizeclass范围的话,就是直接分配的内存,那就不管了
   if sc >= uintptr(len(p{}.deferpool)) {
      return
   }
   pp := getg().m.p.ptr()
   // p本地sizeclass对应的缓冲区满了,批量转移一半到全局sched
   if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
      // 使用g0来转移
      systemstack(func() {
         var first, last *_defer
         for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {
            n := len(pp.deferpool[sc])
            d := pp.deferpool[sc][n-1]
            pp.deferpool[sc][n-1] = nil
            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
            // 先将需要转移的那批defer对象串成一个链表
            if first == nil {
               first = d
            } else {
               last.link = d
            }
            last = d
         }
         lock(&sched.deferlock)
         // 把这个链表放到sched.deferpool对应sizeclass的链表头
         last.link = sched.deferpool[sc]
         sched.deferpool[sc] = first
         unlock(&sched.deferlock)
      })
   }
   // 清空当前要释放的defer的属性
   d.siz = 0
   d.started = false
   d.sp = 0
   d.pc = 0
   d.link = nil

   pp.deferpool[sc] = append(pp.deferpool[sc], d)
}

gopanic

func gopanic(e interface{}) {
   gp := getg()

   var p _panic
   p.arg = e
   p.link = gp._panic
   gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

   atomic.Xadd(&runningPanicDefers, 1)
   // 依次执行 g._defer链表的defer对象
   for {
      d := gp._defer
      if d == nil {
         break
      }

      // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
      // take defer off list. The earlier panic or Goexit will not continue running.
      // 正常情况下,defer执行完成之后都会被移除,既然这个defer没有移除,原因只有两种: 1. 这个defer里面引发了panic 2. 这个defer里面引发了 runtime.Goexit,但是这个defer已经执行过了,需要移除,如果引发这个defer没有被移除是第一个原因,那么这个panic也需要移除,因为这个panic也执行过了,这里给panic增加标志位,以待后续移除
      if d.started {
         if d._panic != nil {
            d._panic.aborted = true
         }
         d._panic = nil
         d.fn = nil
         gp._defer = d.link
         freedefer(d)
         continue
      }
      d.started = true

      // Record the panic that is running the defer.
      // If there is a new panic during the deferred call, that panic
      // will find d in the list and will mark d._panic (this panic) aborted.
      // 把当前的panic 绑定到这个defer上面,defer里面有可能panic,这种情况下就会进入到 上面d.started 的逻辑里面,然后把当前的panic终止掉,因为已经执行过了 
      d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
      // 执行defer.fn
      p.argp = unsafe.Pointer(getargp(0))
      reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
      p.argp = nil

      // reflectcall did not panic. Remove d.
      if gp._defer != d {
         throw("bad defer entry in panic")
      }
      // 解决defer与panic的绑定关系,因为 defer函数已经执行完了,如果有panic或Goexit就不会执行到这里了
      d._panic = nil
      d.fn = nil
      gp._defer = d.link

      // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
      //GC()

      pc := d.pc
      sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
      freedefer(d)
      // panic被recover了,就不需要继续panic了,继续执行剩余的代码
      if p.recovered {
         atomic.Xadd(&runningPanicDefers, -1)

         gp._panic = p.link
         // Aborted panics are marked but remain on the g.panic list.
         // Remove them from the list.
         // 从panic链表中移除aborted的panic,下面解释
         for gp._panic != nil && gp._panic.aborted {
            gp._panic = gp._panic.link
         }
         if gp._panic == nil { // must be done with signal
            gp.sig = 0
         }
         // Pass information about recovering frame to recovery.
         gp.sigcode0 = uintptr(sp)
         gp.sigcode1 = pc
         // 调用recovery, 恢复当前g的调度执行
         mcall(recovery)
         throw("recovery failed") // mcall should not return
      }
   }
     // 打印panic信息
   preprintpanics(gp._panic)
     // panic
   fatalpanic(gp._panic) // should not return
   *(*int)(nil) = 0      // not reached
}

  看下里面gp._panic.aborted 的作用:

func main() {
   defer func() { // defer1
      recover()
   }()
   panic1()
}

func panic1() {
   defer func() {  // defer2
      panic("error1") // panic2
   }()
   panic("error")  // panic1
}

  执行顺序详解:

  • 当执行到 panic("error") 时

  g._defer链表: g._defer->defer2->defer1

  g._panic链表:g._panic->panic1 

  • 当执行到 panic("error1") 时 

  g._defer链表: g._defer->defer2->defer1

  g._panic链表:g._panic->panic2->panic1

  • 继续执行到 defer1 函数内部,进行recover()
    此时会去恢复 panic2 引起的 panic, panic2.recovered = true,应该顺着g._panic链表继续处理下一个panic了,但是我们可以发现 panic1 已经执行过了,这也就是下面的代码的逻辑了,去掉已经执行过的panic
 for gp._panic != nil && gp._panic.aborted {
    gp._panic = gp._panic.link
 }

panic的逻辑:

  程序在遇到panic的时候,就不再继续执行下去了,先把当前panic 挂载到 g._panic 链表上,开始遍历当前g的g._defer链表,然后执行_defer对象定义的函数等,如果 defer函数在调用过程中又发生了 panic,则又执行到了 gopanic函数,最后,循环打印所有panic的信息,并退出当前g。然而,如果调用defer的过程中,遇到了recover,则继续进行调度(mcall(recovery))。

recovery

func recovery(gp *g) {
   // Info about defer passed in G struct.
   sp := gp.sigcode0
   pc := gp.sigcode1
   // Make the deferproc for this d return again,
   // this time returning 1.  The calling function will
   // jump to the standard return epilogue.
   // 记录defer返回的sp pc
   gp.sched.sp = sp
   gp.sched.pc = pc
   gp.sched.lr = 0
   gp.sched.ret = 1
   // 重新恢复执行调度
   gogo(&gp.sched)
}

gorecover

  gorecovery 仅仅只是设置了 g._panic.recovered 的标志位

func gorecover(argp uintptr) interface{} {
   gp := getg()
   p := gp._panic
   // 需要根据 argp的地址,判断是否在defer函数中被调用
   if p != nil && !p.recovered && argp == uintptr(p.argp) {
      // 设置标志位,上面gopanic中会对这个标志位做判断
      p.recovered = true
      return p.arg
   }
   return nil
}

goexit

  当手动调用 runtime.Goexit() 退出的时候,defer函数也会执行:

func Goexit() {
    // Run all deferred functions for the current goroutine.
    // This code is similar to gopanic, see that implementation
    // for detailed comments.
    gp := getg()
  // 遍历defer链表
    for {
        d := gp._defer
        if d == nil {
            break
        }
    // 如果 defer已经执行过了,与defer绑定的panic 终止掉
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
                d._panic = nil
            }
            d.fn = nil
      // 从defer链表中移除
            gp._defer = d.link
      // 释放defer
            freedefer(d)
            continue
        }
    // 调用defer内部函数
        d.started = true
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
        if gp._defer != d {
            throw("bad defer entry in Goexit")
        }
        d._panic = nil
        d.fn = nil
        gp._defer = d.link
        freedefer(d)
        // Note: we ignore recovers here because Goexit isn't a panic
    }
  // 调用goexit0,清除当前g的属性,重新进入调度
    goexit1()
}

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值