当前位置: 首页 > news >正文

go slice源码探索(切片、copy、扩容)和go编译源码分析

文章目录

    • 概要
    • 一、数据结构
    • 二、初始化
        • 2.1、字面量
        • 2.2、下标截取
            • 2.2.1、截取原理
        • 2.3、make关键字
            • 2.3.1、编译时
    • 三、复制
        • 3.1、copy源码
    • 四、扩容
        • 4.1、append源码
    • 五:切片的GC
    • 六:切片使用注意事项
    • 七:参考

概要

Go语言的切片(slice)是对数组的扩展,类似C语言常见的简单动态字符串(典型应用如Redis的string类型),动态扩容是其相对数组的最大优势。
本人在工作过程中,对slice的使用与底层原理有了较为全面的理解,特在这里针对其初始化、扩容、复制等机制进行源码分析。

PS: go V1.20.6

一、数据结构

slice的数据结构非常简单,其提供了和数组一样的下标访问任意元素方式。在运行时,其结构由一个数组字段,一个长度字段,一个容量字段组成。
最初是在runtime/slice.go文件中:

type slice struct {array unsafe.Pointerlen   intcap   int
}

但是2018年10月份的一次优化cmd/compile: move slice construction to callers of makeslice,如下:
 [Go]move slice construction to callers of makeslice

本次优化运行时结构迁移到reflect/value.go文件中:

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
//
// In new code, use unsafe.Slice or unsafe.SliceData instead.
type SliceHeader struct {Data uintptrLen  intCap  int
}

至今未改,其中Data字段是指向底层数组的指针,Len是当前底层数组使用的长度,Cap是当前底层数组的总长度。

二、初始化

切片有三种初始化方式:

  • 使用字面量初始化新的切片;
  • 通过下标的方式获得数组或截取切片的一部分;
  • 使用关键字 make 创建切片。
2.1、字面量

示例如下:

a := []int64{4, 8, 9, 6, 4}
2.2、下标截取

数组转切片:

a := [5]int64{4, 8, 9, 6, 4}
b := a[:]

从切片截取(截取是遵循左闭右开原则):

a := []int64{4, 8, 9, 6, 4}
//删除第一个元素
b1 := a[1:] //[8,9,6,4],此时a的值不变
//删除最后一个元素
b2 := a[:len(a)-1]//[4,8,9,6],此时a的值不变
//删除中间一个元素
n := len(a)/2
b3 := append(a[:n],a[n+1:]...)//[4,8,6,4],此时a的值是[4,8,6,4,4],

这种操作非常高效,不会申请内存,相比b1和b2,b3还会涉及到元素的移动,进而改变了a的内容。

2.2.1、截取原理
a := []int64{4, 8, 9, 6, 4}
b1 := a[1:4]//仅指定长度
b2 := a[1:2:3]//指定长度为1(2-1),容量为2(3-1)。【1标识索引下标1、2标识索引下标2,决定长度、3表示索引下标3,决定容量】

长度和容量变化如下:
go slice 截取变化
如图所示,虽说a、b1、b2的值不是同一个,但底层数组还共用同一段连续的内存块,所以在编码中要注意,这一点我们可以从Go SSA过程中一窥究竟:
【在go的源码和汇编码之间,其实编译器做了很多事情,而ssa(是一种中间代码的表示形式)就是查看编译器优化行为的利器】
先设置下环境变量。

# windows
$env:GOSSAFUNC="main"
# linux
export GOSSAFUNC="main"

再运行以下代码:

package mainimport "fmt"func main() {a := []int64{4, 8, 9, 6, 4}b1 := a[2:4]fmt.Println(b1, len(b1), cap(b1))b2 := a[1:2:3]fmt.Println(b2, len(b2), cap(b2))
}

执行go build main.go就可以得到ssa.html文件,读者可自行试验下,内容太多,我们是看start和opt阶段的就可以:
go-ssa我们只探究b1 := a[2:4]即可,关键两处如下:
start阶段:
go-ssa-start
opt阶段:
go-ssa-opt
对比可以看到,从start阶段到opt阶段,中间码已经简化很多。
从中可以看到:

  • 变量v39表示源码中的变量a;
  • 变量v56表示源码中的变量b1;

那么b1 := a[2:4]如何变化的呢?
在opt阶段可以看到v40=v39,

  1. v49表示源码中的变量b1的长度,已经计算出来真实数值 2(在start阶段还不是呢),v50表示源码中的变量b1的容量,已经计算出来真实数值 3。
  2. v55 通过对变量v40进行OffPtr操作得到一个地址,就是一个指针运算,我们知道a,b1元素是int64的,一个元素8字节。b1相对a是右移了两个元素,就是16字节了。即对a的底层数组指针加16字节,就是b1的底层数组的指针了。
  3. v56 就是整合v49,v50,v55这几个变量到一起了。通过SliceMake 操作会接受四个参数创建新的切片,依次元素类型([]int64)、底层数组指针(v55)、长度(v49)和容量(v50),这也是我们在数据结构一节中提到的切片的几个字段 。

可以看到整个过程并没有重新申请新的内存段,是基于a的底层数组,进行指针运算,调整切片长度和容量的值等操作得到b1,
所以需要注意的是修改新切片b1的数据也会改变原切片a的数据。

所以说b2 := a[:2:3]操作只是改小了切片容量,并不会释放a申请的内存段,这种缩容是伪缩容

2.3、make关键字

提到make的源码,我们第一时间想到的就是Go SDK下的src/runtime/slice.go文件中的makeslice函数,但该函数目前只是申请了一块连续内存(见第一章节2018年10月份的一次优化相关),那么什么地方调用了该函数呢?这就要去看一下Go编译器的源码了。

2.3.1、编译时

Go编译器的执行流程有多个阶段:

  1. 经过词法分析和语法分析得到抽象语法树AST;
  2. 类型检查,包含检查常量、类型和函数名等类型,变量捕获与赋值,函数内联、逃逸分析、闭包重写、遍历函数(有些会导入内建的运行时函数,如runtime.makeslice,runtime.makechan等);
  3. SSA生成;
  4. 机器码生成。

分析何处调用runtime.makeslice函数我们只要分析类型检查阶段

编译器入口文件src/cmd/compile/main.go,代码如下:

func main() {// disable timestamps for reproducible outputlog.SetFlags(0)log.SetPrefix("compile: ")buildcfg.Check()archInit, ok := archInits[buildcfg.GOARCH]if !ok {fmt.Fprintf(os.Stderr, "compile: unknown architecture %q\n", buildcfg.GOARCH)os.Exit(2)}gc.Main(archInit)//注意此处gc是go compiler的缩写,与垃圾回收的GC(garbage collection)区分开base.Exit(0)
}

进入gc.Main函数:

func Main(archInit func(*ssagen.ArchInfo)) {//此处省略若干代码...// Prepare for backend processing. This must happen before pkginit,// because it generates itabs for initializing global variables.ssagen.InitConfig()//ssa初始化// 词法解析、语法解析、类型检查工作noder.LoadPackage(flag.Args())//此处省略若干代码...// 逃逸分析escape.Funcs(typecheck.Target.Decls)//遍历函数工作base.Timer.Start("be", "compilefuncs")fcount := int64(0)for i := 0; i < len(typecheck.Target.Decls); i++ {if fn, ok := typecheck.Target.Decls[i].(*ir.Func); ok {// Don't try compiling dead hidden closure.if fn.IsDeadcodeClosure() {continue}enqueueFunc(fn)fcount++}}base.Timer.AddEvent(fcount, "funcs")//ssa生成、机器码生成工作compileFunctions()// Write object data to disk.base.Timer.Start("be", "dumpobj")dumpdata()base.Ctxt.NumberSyms()dumpobj()if base.Flag.AsmHdr != "" {dumpasmhdr()}
}

进入noder.LoadPackage函数:
该函数位于src/cmd/compile/internal/noder/目录下,

func LoadPackage(filenames []string) {//只摘抄了部分关键代码// Limit the number of simultaneously open files.sem := make(chan struct{}, runtime.GOMAXPROCS(0)+10)noders := make([]*noder, len(filenames))//...// 词法解析、语法解析工作p.file, _ = syntax.Parse(fbase, f, p.error, p.pragma, syntax.CheckBranches) // 类型检查相关check2(noders)
}

check2函数会在某个节点调用typecheck.Expr,typecheck.Stmt,typecheck.Call等函数进行类型检查,即转入typecheck.typecheck函数。

func typecheck(n ir.Node, top int) (res ir.Node) {//省略...n.SetTypecheck(2)n = typecheck1(n, top)n.SetTypecheck(1)//省略...
}
// typecheck1 should ONLY be called from typecheck.
func typecheck1(n ir.Node, top int) ir.Node {switch n.Op() {case ir.OMAKE://make操作n := n.(*ir.CallExpr)return tcMake(n)}
}
// tcMake typechecks an OMAKE node.
func tcMake(n *ir.CallExpr) ir.Node {args := n.Argsl := args[0]l = typecheck(l, ctxType)t := l.Type()var nn ir.Nodeswitch t.Kind() {case types.TSLICE://...,设置为ir.OMAKESLICE操作nn = ir.NewMakeExpr(n.Pos(), ir.OMAKESLICE, l, r)}//省略...return nn
}
func NewMakeExpr(pos src.XPos, op Op, len, cap Node) *MakeExpr {n := &MakeExpr{Len: len, Cap: cap}n.pos = posn.SetOp(op)return n
}

至此获取了make([]int,0,10)之类操作的类型,稍后进入遍历函数操作,即gc.Main函数中的enqueueFunc

func enqueueFunc(fn *ir.Func) {//...todo := []*ir.Func{fn}for len(todo) > 0 {next := todo[len(todo)-1]todo = todo[:len(todo)-1]prepareFunc(next)todo = append(todo, next.Closures...)}//...
}
// prepareFunc handles any remaining frontend compilation tasks that
// aren't yet safe to perform concurrently.
func prepareFunc(fn *ir.Func) {walk.Walk(fn)//进入遍历函数核心逻辑
}

调用链:Walk->walkStmtList->walkStmt->walkExpr->walkExpr1

func walkExpr1(n ir.Node, init *ir.Nodes) ir.Node {switch n.Op() {case ir.OMAKESLICE:n := n.(*ir.MakeExpr)return walkMakeSlice(n, init)case ir.OSLICEHEADER:n := n.(*ir.SliceHeaderExpr)return walkSliceHeader(n, init)}
}
// walkMakeSlice walks an OMAKESLICE node.
func walkMakeSlice(n *ir.MakeExpr, init *ir.Nodes) ir.Node {l := n.Lenr := n.Capif n.Esc() == ir.EscNone {//不发生逃逸,分配栈内内存,注意这里由gc.Main函数中的escape.Funcs函数分析得到t = types.NewArray(t.Elem(), i) // [r]Tvar_ := typecheck.Temp(t)appendWalkStmt(init, ir.NewAssignStmt(base.Pos, var_, nil))  // zero tempr := ir.NewSliceExpr(base.Pos, ir.OSLICE, var_, nil, l, nil) // arr[:l]// The conv is necessary in case n.Type is named.return walkExpr(typecheck.Expr(typecheck.Conv(r, n.Type())), init)}len, cap := l, rfnname := "makeslice64"//声明要调用runtime.makeslice64函数argtype := types.Types[types.TINT64]if (len.Type().IsKind(types.TIDEAL) || len.Type().Size() <= types.Types[types.TUINT].Size()) &&(cap.Type().IsKind(types.TIDEAL) || cap.Type().Size() <= types.Types[types.TUINT].Size()) {fnname = "makeslice"//声明要调用runtime.makeslice函数argtype = types.Types[types.TINT]}fn := typecheck.LookupRuntime(fnname)//调用得到一块连续内存的头指针ptr := mkcall1(fn, types.Types[types.TUNSAFEPTR], init, reflectdata.MakeSliceElemRType(base.Pos, n),   typecheck.Conv(len, argtype), typecheck.Conv(cap, argtype))ptr.MarkNonNil()//修正slice长度和容量len = typecheck.Conv(len, types.Types[types.TINT])cap = typecheck.Conv(cap, types.Types[types.TINT])//这里转化为ir.OSLICEHEADER操作sh := ir.NewSliceHeaderExpr(base.Pos, t, ptr, len, cap)//执行ir.OSLICEHEADER操作return walkExpr(typecheck.Expr(sh), init)
}
// 转化为ir.SliceHeaderExpr,在程序启动后,就会变成反射库中的SliceHeader 结构体
func walkSliceHeader(n *ir.SliceHeaderExpr, init *ir.Nodes) ir.Node {n.Ptr = walkExpr(n.Ptr, init)n.Len = walkExpr(n.Len, init)n.Cap = walkExpr(n.Cap, init)return n
}

至此把编译阶段如何调用makeslice基本解释清楚了,也顺便了解了Go编译相关的知识

至于makeslice函数就很简单了

func makeslice(et *_type, len, cap int) unsafe.Pointer {mem, overflow := math.MulUintptr(et.size, uintptr(cap))if overflow || mem > maxAlloc || len < 0 || len > cap {//参数自动修正mem, overflow := math.MulUintptr(et.size, uintptr(len))if overflow || mem > maxAlloc || len < 0 {panicmakeslicelen()}panicmakeslicecap()}return mallocgc(mem, et, true)//申请一块连续的内存
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {//...if size == 0 {//这时mallocgc函数有意思的地方,此时会返回一个固定指针,我们常用的struct{}{}就是因此而来return unsafe.Pointer(&zerobase)}//...
}

PS:ir.SliceHeaderExpr是如何在程序启动后转化为reflect.SliceHeader 的呢?有兴趣的大佬可在评论区解释下

三、复制

func main() {s1 := []string{"aaa", "sss", "ddd"}s2 := make([]string, 2, 6)copy(s2, s1)s2 = append(s2, "yyy")printSlice(s2)//output: len=3 cap=6 slice=[aaa sss yyy]s3 := make([]string, 4, 6)copy(s3, s1)s3 = append(s3, "xxx")printSlice(s3)//output: len=5 cap=6 slice=[aaa sss ddd  xxx]
}
func printSlice(x []string) {fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

根据s2,s3的打印结果可知,若想将源slice的内容全部复制到目的slice,那么目的slice的长度必须大于等于源slice的长度。

3.1、copy源码

编译时源码可见2.3.1小节,关键词是src/cmd/compile/internal/ir/node.go中的OCOPY ,搜索可知其遍历函数是walkCopy

// Lower copy(a, b) to a memmove call or a runtime call.
// Also works if b is a string.
func walkCopy(n *ir.BinaryExpr, init *ir.Nodes, runtimecall bool) ir.Node {if n.X.Type().Elem().HasPointers() {//slice在堆上的话调用runtime.typedslicecopyfn := writebarrierfn("typedslicecopy", n.X.Type().Elem(), n.Y.Type().Elem())return mkcall1(fn, n.Type(), init, reflectdata.CopyElemRType(base.Pos, n), ptrL, lenL, ptrR, lenR)}if runtimecall {//某些特殊情况,比如编译时开启竞态检查(-race),调用runtime.slicecopyfn := typecheck.LookupRuntime("slicecopy")fn = typecheck.SubstArgTypes(fn, ptrL.Type().Elem(), ptrR.Type().Elem())return mkcall1(fn, n.Type(), init, ptrL, lenL, ptrR, lenR, ir.NewInt(n.X.Type().Elem().Size()))}//排除以上两种情况,都走runtime.memmovenlen := typecheck.Temp(types.Types[types.TINT])// n = len(to)l = append(l, ir.NewAssignStmt(base.Pos, nlen, ir.NewUnaryExpr(base.Pos, ir.OLEN, nl)))fn := typecheck.LookupRuntime("memmove")fn = typecheck.SubstArgTypes(fn, nl.Type().Elem(), nl.Type().Elem())call := mkcall1(fn, nil, init, nto, nfrm, nwid)ne.Body.Append(call)return nlen
}

进入runtime.typedslicecopy和runtime.slicecopy函数,其最后也是调用的runtime.memmove函数。
该函数与C语言的memmove作用是一样的,时间复杂度是O(N),所以面对较多元素的切片时,使用copy操作应当慎重。

四、扩容

func main() {r := make([]int, 0, 3)fmt.Printf("len=%d cap=%d slice=%v,r addr:%p,addr:%p\n", len(r), cap(r), r, &r, r) //初始化,但可以看出r本质为*SliceHeader的指针类型,所以在传参时就是指针传递r = append(r, 5, 6)fmt.Printf("len=%d cap=%d slice=%v,r addr:%p,addr:%p,r[0] addr:%p\n", len(r), cap(r), r, &r, r, &r[0]) //第一个元素地址没变r = append(r, 11)fmt.Printf("len=%d cap=%d slice=%v,r addr:%p,addr:%p,r[0] addr:%p\n", len(r), cap(r), r, &r, r, &r[0]) //第一个元素地址没变r = append(r, 22)fmt.Printf("扩容:len=%d cap=%d slice=%v,r addr:%p,addr:%p,r[0] addr:%p\n", len(r), cap(r), r, &r, r, &r[0]) //扩容后地址发生变化,即底层数组发生变化,但变量的地址不变fmt.Printf("r addr:%p,addr:%p,r[0] addr:%p,r[1] addr:%p,\n", &r, r, &r[0], &r[1])                         //r值的地址也变为扩容后第一个元素的地址r = append(r, []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110}...)fmt.Printf("扩容 len=%d cap=%d slice=%v,r addr:%p,r[0] addr:%p\n", len(r), cap(r), r, &r, &r[0]) //扩容后地址发生变化,即底层数组发生变化,但变量的地址不变。扩容后newCap本应该是15,但实际是16,因为做了内存对齐
}

运行代码输出如下:

len=0 cap=3 slice=[],r addr:0xc000008570,addr:0xc000017698
len=2 cap=3 slice=[5 6],r addr:0xc000008570,addr:0xc000017698,r[0] addr:0xc000017698
len=3 cap=3 slice=[5 6 11],r addr:0xc000008570,addr:0xc000017698,r[0] addr:0xc000017698
扩容:len=4 cap=6 slice=[5 6 11 22],r addr:0xc000008570,addr:0xc00000eba0,r[0] addr:0xc00000eba0
r addr:0xc000008570,addr:0xc00000eba0,r[0] addr:0xc00000eba0,r[1] addr:0xc00000eba8,
扩容 len=15 cap=16 slice=[5 6 11 22 10 20 30 40 50 60 70 80 90 100 110],r addr:0xc000008570,r[0] addr:0xc000078f00

其扩容流程图如下:
假设有一切片,其长度为oldLen,容量为oldCap,现要增加num个元素。则有newLen=oldLen+num,doublecap=oldCap+oldCap。
go slice扩容

4.1、append源码

编译时源码可见2.3.1小节,关键词是src/cmd/compile/internal/ir/node.go中的OAPPEND ,注意walkExpr1函数源码中OAPPEND已废弃,而是走OAS搜索可知其遍历函数是walkAssign

func walkAssign(init *ir.Nodes, n ir.Node) ir.Node {//...as := n.(*ir.AssignStmt)switch as.Y.Op() {case ir.OAPPEND:var r ir.Nodeswitch {case isAppendOfMake(call):// x = append(y, make([]T, y)...)r = extendSlice(call, init)case call.IsDDD:r = appendSlice(call, init) // also works for append(slice, string).default:r = walkAppend(call, init, as)}}
}
func walkAppend(n *ir.CallExpr, init *ir.Nodes, dst ir.Node) ir.Node {var l []ir.Node// s = slice to append tos := typecheck.Temp(nsrc.Type())l = append(l, ir.NewAssignStmt(base.Pos, s, nsrc))// num = number of things to appendnum := ir.NewInt(int64(argc))// newLen := s.len + numnewLen := typecheck.Temp(types.Types[types.TINT])l = append(l, ir.NewAssignStmt(base.Pos, newLen, ir.NewBinaryExpr(base.Pos, ir.OADD, ir.NewUnaryExpr(base.Pos, ir.OLEN, s), num)))//调用runtime.growslice函数fn := typecheck.LookupRuntime("growslice") //   growslice(ptr *T, newLen, oldCap, num int, <type>) (ret []T)fn = typecheck.SubstArgTypes(fn, s.Type().Elem(), s.Type().Elem())nif.Else = []ir.Node{ir.NewAssignStmt(base.Pos, s, mkcall1(fn, s.Type(), nif.PtrInit(),ir.NewUnaryExpr(base.Pos, ir.OSPTR, s),//要扩容切片的地址newLen,//新切片元素个数ir.NewUnaryExpr(base.Pos, ir.OCAP, s),//要扩容切片的容量num,//要追加的元素个数reflectdata.TypePtr(s.Type().Elem()))),//要扩容切片的类型}
}

再看看runtime.growslice函数

func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {oldLen := newLen - numif et.size == 0 {return slice{unsafe.Pointer(&zerobase), newLen, newLen}//扩容的运行时竟然用的是runtime.slice结构体}//扩容逻辑newcap := oldCapdoublecap := newcap + newcapif newLen > doublecap {newcap = newLen} else {const threshold = 256if oldCap < threshold {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < newLen {// Transition from growing 2x for small slices// to growing 1.25x for large slices. This formula// gives a smooth-ish transition between the two.newcap += (newcap + 3*threshold) / 4}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = newLen}}}var overflow boolvar lenmem, newlenmem, capmem uintptr//进行内存对齐switch {case et.size == 1:lenmem = uintptr(oldLen)newlenmem = uintptr(newLen)capmem = roundupsize(uintptr(newcap))overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.size == goarch.PtrSize: //goarch.PtrSize is 4 on 32-bit systems, 8 on 64-bit systems。lenmem = uintptr(oldLen) * goarch.PtrSizenewlenmem = uintptr(newLen) * goarch.PtrSizecapmem = roundupsize(uintptr(newcap) * goarch.PtrSize)//内存对齐,overflow = uintptr(newcap) > maxAlloc/goarch.PtrSizenewcap = int(capmem / goarch.PtrSize)case isPowerOfTwo(et.size):var shift uintptrif goarch.PtrSize == 8 {// Mask shift for better code generation.shift = uintptr(sys.TrailingZeros64(uint64(et.size))) & 63} else {shift = uintptr(sys.TrailingZeros32(uint32(et.size))) & 31}lenmem = uintptr(oldLen) << shiftnewlenmem = uintptr(newLen) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)capmem = uintptr(newcap) << shiftdefault:lenmem = uintptr(oldLen) * et.sizenewlenmem = uintptr(newLen) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)capmem = uintptr(newcap) * et.size}if overflow || capmem > maxAlloc {panic(errorString("growslice: len out of range"))}//申请新切片所需的内存var p unsafe.Pointerif et.ptrdata == 0 {p = mallocgc(capmem, nil, false)memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)} else {p = mallocgc(capmem, et, true)if lenmem > 0 && writeBarrier.enabled {bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.size+et.ptrdata)}}//旧切片中的内容复制到新切片中memmove(p, oldPtr, lenmem)return slice{p, newLen, newcap}
}

接着一起看下内存对齐函数roundupsize,窥视下Go的内存管理,我们以案例中的r := make([]int, 0, 3)第二次发生扩容为例,
已知测试环境是64位的,那么et.size=8,goarch.PtrSize=8,在内存对齐前的newcap=15
则有capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)转化为capmem = roundupsize(120)

const (_MaxSmallSize   = 32768smallSizeDiv    = 8smallSizeMax    = 1024largeSizeDiv    = 128_NumSizeClasses = 68_PageShift      = 13
)
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {if size < _MaxSmallSize {if size <= smallSizeMax-8 {return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])} else {return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])}}if size+_PageSize < size {return size}return alignUp(size, _PageSize)
}
// divRoundUp returns ceil(n / a).
func divRoundUp(n, a uintptr) uintptr {return (n + a - 1) / a
}

按照代码逻辑计算:

  1. divRoundUp(size, smallSizeDiv)得到15;
  2. size_to_class8[15]得到10;
  3. class_to_size[10]得到128;
  4. newcap = int(capmem / goarch.PtrSize)得到16。
    所以经过内存对齐后的容量为16,所以本次扩容申请了16B的内存块。

我们观察到两个全局变量class_to_sizesize_to_class8 。Go为了方便对内存进行管理,将内存划分成了68个级别的span,最小为8B,最大为32KB,大于32KB的类型都为0。
class_to_size通过 spanClass获取 span划分的 object大小,而 size_to_class8 表示通过 size 获取它的 spanClass。

五:切片的GC

Go 的GC算法是三色(黑、灰、白)标记算法,白色的是要被回收的。Go在GC时从根对象开始扫描,根对象包含全局变量、goroutine上的栈对象,span中的finalizer对象(所以runtime.SetFinalizer要慎用,可能会引起内存泄漏)。

那么我们要想slice被GC,就要slice不被全局变量,栈对象一直使用即可(finalizer对象编码时一般不涉及)。

1:切片下标截取引起的内存泄漏

func PrintMemory() {bToMb := func(b uint64) uint64 {return b}var m runtime.MemStatsruntime.ReadMemStats(&m)// For info on each, see: https://golang.org/pkg/runtime/#MemStatsfmt.Printf("Alloc = %vB", bToMb(m.Alloc))fmt.Printf("\tTotalAlloc = %vB", bToMb(m.TotalAlloc))fmt.Printf("\tSys = %vB", bToMb(m.Sys))fmt.Printf("\tHeapSys = %vB", bToMb(m.HeapSys))fmt.Printf("\tHeapReleased = %vB", bToMb(m.HeapReleased))fmt.Printf("\tHeapInuse = %vB", bToMb(m.HeapInuse))fmt.Printf("\tHeapAlloc = %vB", bToMb(m.HeapAlloc))fmt.Printf("\tHeapIdle = %vB", bToMb(m.HeapIdle))fmt.Printf("\tNumGC = %v\n", m.NumGC)
}
func main() {PrintMemory()a := make([]int64, 1024*1024) //FIXME:申请内存块 APrintMemory()time.Sleep(time.Second)for i := 0; i < 1024*1024; i++ {a[i] = rand.Int63()}time.Sleep(time.Second)PrintMemory()runtime.GC()time.Sleep(time.Second)PrintMemory()b := a[:100:100] //FIXME:所以说这样操作并不会释放已申请的内存块 Aruntime.GC()time.Sleep(time.Second)PrintMemory()b = append(b, []int64{5, 5, 6, 6}...) //FIXME:此时发生了扩容,已经与内存块 A无关了,后续发生GC,就会回收内存块 Aruntime.GC()time.Sleep(time.Second)PrintMemory() //这里从HeapIdle可以看出来,Go中及时某些对象被释放了,但并不会将内存立即归还给OS,而是标记为free,后续可能会被重新利用,提高性能fmt.Println(b[100])
}

2:指针切片引起的内存泄漏

func main() {PrintMemory()s := make([]*string, 5)s[0] = strPtr("世纪东方收款方就开始")s[1] = strPtr("速度进房撒克服恐惧的")s[2] = strPtr("畜牧场辛苦费几十块大飞机刷卡")s[3] = strPtr("摩卡壶亚体育前三个哈哈的")runtime.GC()time.Sleep(time.Second)PrintMemory()s1 := &strings.Builder{}for i := 0; i < 1024*1024; i++ { //变量s1申请内存s1.WriteString("看见对方收款方几十块")}s[4] = strPtr(s1.String())runtime.GC()time.Sleep(time.Second)PrintMemory()p := s[:3] //并不会释放变量s1申请的内存runtime.GC()time.Sleep(time.Second)PrintMemory()s[4] = nil //会释放变量s1申请的内存runtime.GC()time.Sleep(time.Second)PrintMemory()fmt.Println(*p[0])
}

六:切片使用注意事项

  1. 切片初始化时尽量确定容量,避免频繁扩容,因为要重新申请内存并copy旧切片内容;
  2. 大切片尽量少copy,而是复用,copy的时间复杂度是O(N);
  3. 切片采用下标截取时不会申请新内存块,所以修改截取后的切片内容会改变源切片的内容;
  4. 切片变量本身是个结构体指针,切片发生扩容时会改变指针值,指向一个新的地址
func main() {s := []int64{4, 8, 9}fmt.Printf("len=%d cap=%d slice=%v,s addr:%p,s:%p,s[0] addr:%p\n", len(s), cap(s), s, &s, s, &s[0])//len=3 cap=3 slice=[4 8 9],s addr:0xc000008078,s:0xc000016180,s[0] addr:0xc000016180s = append(s, 6)fmt.Printf("len=%d cap=%d slice=%v,s addr:%p,s:%p,s[0] addr:%p\n", len(s), cap(s), s, &s, s, &s[0])//len=4 cap=6 slice=[4 8 9 6],s addr:0xc000008078,s:0xc00000e3f0,s[0] addr:0xc00000e3f0
}
可以看到切片变量s的值在扩容前是0xc000016180,扩容后是0xc00000e3f0
  1. 切片传参是值传递,当然了,go里面只有值传递,但一般会把指针传递(本质也是值传递,指针本身也是一个值)分出来
func testSlice(s []int64) {fmt.Printf("s addr:%p,s:%p,s[0] addr:%p\n", &s, s, &s[0])//s addr:0xc0000080a8,s:0xc000016180,s[0] addr:0xc000016180s[1] = 80s = append(s, 66)fmt.Printf("s addr:%p,s:%p,s[0] addr:%p\n", &s, s, &s[0])//s addr:0xc0000080a8,s:0xc00000e420,s[0] addr:0xc00000e420fmt.Println(s)//[4 80 9 66]
}
func main() {s := []int64{4, 8, 9}fmt.Printf("s addr:%p,s:%p,s[0] addr:%p\n", &s, s, &s[0])//s addr:0xc000008078,s:0xc000016180,s[0] addr:0xc000016180testSlice(s)fmt.Println(s)//[4 80 9]
}
  1. 切片下标截取可能会引起的内存泄漏,如果切片内的其他元素不会再使用,最好申请一个新的切片,copy需要的元素;
  2. 切片下标截取的方式是伪缩容,要想真缩容就要申请一个新的切片,进行copy操作,这样旧的切片是会被GC
func main() {s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}s1 := make([]int,3)copy(s1,s)//之后的代码与切片s无关,那么由于没有地方再使用切片s,就会被GCfmt.Println(s1) //[1,2,3]
}
  1. 切片删除不符合要求的元素
func main() {//该算法时间复杂度是O(N)s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}k := 0for _, n := range slice {if n%3 != 0 { // 指定过滤条件s2[k] = nk++}}s = s[:k],fmt.Println(s) //[1 2 4 5 7 8]
}

七:参考

1]:深入学习go语言-前置知识-编译过程
2]:Go 中切片使用不当会造成内存泄漏的那些场景

相关文章:

go slice源码探索(切片、copy、扩容)和go编译源码分析

文章目录 概要一、数据结构二、初始化2.1、字面量2.2、下标截取2.2.1、截取原理 2.3、make关键字2.3.1、编译时 三、复制3.1、copy源码 四、扩容4.1、append源码 五&#xff1a;切片的GC六&#xff1a;切片使用注意事项七&#xff1a;参考 概要 Go语言的切片&#xff08;slice…...

电影“AI化”已成定局,华为、小米转战入局又将带来什么?

从华为、Pika、小米等联合打造电影工业化实验室、到Pika爆火&#xff0c;再到国内首部AI全流程制作《愚公移山》开机……业内频繁的新动态似乎都在预示着2023年国内电影开始加速进入新的制片阶段&#xff0c;国内AI电影热潮即将来袭。 此时以华为为首的底层技术科技企业加入赛…...

小程序for循环中key值的作用?

在小程序的 for 循环中&#xff0c;key 值有两个主要作用&#xff1a; 识别列表项的唯一性&#xff1a;当在列表渲染时使用 for 循环&#xff0c;每个列表项都应该具有一个唯一的 key 值。这个 key 值用于帮助小程序识别每个列表项的唯一性&#xff0c;以便在列表发生变化时进行…...

深入理解Dockerfile —— 筑梦之路

FROM 基础镜像 可以选择现有的镜像&#xff0c;比如centos、debian、apline等&#xff0c;特殊镜像scratch&#xff0c;它是一个空镜像。 如果你以 scratch 为基础镜像的话&#xff0c;意味着你不以任何镜像为基础&#xff0c;接下来所写的指令将作为镜像第一层开始存在。 不…...

Vue3 魔法:轻松删除响应式对象的属性

&#x1f9d9;‍♂️ 诸位好&#xff0c;吾乃诸葛妙计&#xff0c;编程界之翘楚&#xff0c;代码之大师。算法如流水&#xff0c;逻辑如棋局。 &#x1f4dc; 吾之笔记&#xff0c;内含诸般技术之秘诀。吾欲以此笔记&#xff0c;传授编程之道&#xff0c;助汝解技术难题。 &…...

python命令大全及说明,python命令大全下载

大家好&#xff0c;本文将围绕python命令大全及说明展开说明&#xff0c;python命令大全下载是一个很多人都想弄明白的事情&#xff0c;想搞清楚python简单命令语句需要先了解以下几个事情。 Python有哪些常用但容易忘记的命令&#xff1f; 1 如何忽略报错信息2 Python常见绘图…...

Flink1.17实战教程(第五篇:状态管理)

系列文章目录 Flink1.17实战教程&#xff08;第一篇&#xff1a;概念、部署、架构&#xff09; Flink1.17实战教程&#xff08;第二篇&#xff1a;DataStream API&#xff09; Flink1.17实战教程&#xff08;第三篇&#xff1a;时间和窗口&#xff09; Flink1.17实战教程&…...

ES慢查询分析——性能提升6 倍

问题 生产环境频繁报警。查询跨度91天的数据&#xff0c;请求耗时已经来到了30s。报警的阈值为5s。我们期望值是5s内&#xff0c;大于该阈值的请求&#xff0c;我们认为是慢查询。这些慢查询&#xff0c;最终排查&#xff0c;是因为走到了历史集群上。受到了数据迁移的一定影响…...

[NAND Flash 4.3] 闪存的物理学原理_NAND Flash 的读、写、擦工作原理

依公知及经验整理,原创保护,禁止转载。 专栏 《深入理解NAND Flash》 <<<< 返回总目录 <<<< 2.1.3.1 Flash 的物理学原理与发明历程 经典物理学认为 物体越过势垒,有一阈值能量;粒子能量小于此能量则不能越过,大于此能 量则可以越过。例如骑自行…...

海豚调度 Dolphinscheduler-3.2.0/DolphinScheduler-3.1.9 离线部署 伪集群模式

Dolphinscheduler-3.2.0(离线)伪集群模式 一、依赖(前置准备工作) 1.JDK&#xff1a;版本要求 JDK(1.8),安装并配置 JAVA_HOME 环境变量,并将其下的 bin 目录追加到PATH 环境变量中; 2.数据库&#xff1a;PostgreSQL(8.2.15) 或者MySQL(5.7),两者任选其一即可,如 MySQL 则需要…...

4.33 构建onnx结构模型-Expand

前言 构建onnx方式通常有两种&#xff1a; 1、通过代码转换成onnx结构&#xff0c;比如pytorch —> onnx 2、通过onnx 自定义结点&#xff0c;图&#xff0c;生成onnx结构 本文主要是简单学习和使用两种不同onnx结构&#xff0c; 下面以 Expand 结点进行分析 方式 方法一…...

LeetCode——1599. 经营摩天轮的最大利润

通过万岁&#xff01;&#xff01;&#xff01; 题目&#xff1a;就是一个摩天轮&#xff0c;一共有4个仓位&#xff0c;一个仓位中最多可以做4个人。然后每次上一个人boardingCost钱&#xff0c;但是我们转动1/4圈&#xff0c;需要的成本是runningCost。然后给我们一个数组cu…...

从 MySQL 的事务 到 锁机制 再到 MVCC

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、事务 1.1 含义 1.2 ACID 二、锁机制 2.1 锁分类 2.2 隔离级别 三、MVCC 3.1 介绍 3.2 隔离级别 3.3 原理 四、总结 前…...

PostGreSQL远程连接

1、找到PostGreSQL安装目录&#xff0c;修改“postgresql.conf”配置文件&#xff08;安装路径\data\postgresql.conf&#xff09;。 若不知道安装目录&#xff0c;则通过服务&#xff0c;找到PostGreSQL运行的任务&#xff0c;右击“属性”可以进行查看安装的目录。 进入该目…...

CSS 缩减顶部动画

<template><!-- mouseenter"startAnimation" 表示在鼠标进入元素时触发 startAnimation 方法。mouseleave"stopAnimation" 表示在鼠标离开元素时触发 stopAnimation 方法。 --><!-- 容器元素 --><div class"container" mou…...

开源掌机是什么?

缘起 最近在学习小游戏的开发&#xff0c;偶然发现有一种叫“掌机”的游戏机&#xff0c;可以玩远古的各类游戏机、街机游戏&#xff01;并且价格都还很便宜。这种神器的东西到底是什么&#xff1f;是如何工作的呢&#xff1f;有市场前景吗&#xff1f;带着这些疑问&#xff0…...

基于Wenet长音频分割降噪识别

Wenet是一个流行的语音处理工具&#xff0c;它专注于长音频的处理&#xff0c;具备分割、降噪和识别功能。它的长音频分割降噪识别功能允许对长时间录制的音频进行分段处理&#xff0c;首先对音频进行分割&#xff0c;将其分解成更小的段落或语音片段。接着进行降噪处理&#x…...

mysql基础-表操作

环境&#xff1a; 管理工具&#xff1a;Navicat 数据库版本&#xff1a;5.7.37 mysql的版本&#xff0c;我们可以通过函数&#xff0c;version()进行查看&#xff0c;本次使用的版本如下&#xff1a; 目录 1.管理工具 1.1创建表 1.2.修改表名 1.3.复制表 1.4.删除表 2…...

MySql——1146 - Table‘mysql.proc‘doesn‘t exit是这个

项目场景&#xff1a; 做自己的小项目需要连接mysql数据库 问题描述 点击数据库时报错 1146 - Table’mysql.proc’doesn’t exit 原因分析&#xff1a; 误删原生的mysql数据库 解决方案&#xff1a; 重新安装装部署mysql就好了 注意不要轻易删除原生的东西...

玩转贝启科技BQ3588C开源鸿蒙系统开发板 —— 代码下载(1)

本文主要参考&#xff1a; BQ3588C_代码下载 1. 安装依赖工具 安装命令如下&#xff1a; sudo apt-get update && sudo apt-get install binutils git git-lfs gnupg flexbison gperf build-essential zip curl zlib1g-dev gcc-multilib g-multiliblibc6-dev-i386 l…...

开源预约挂号平台 - 从0到上线

文章目录 开源预约挂号平台 - 从0到上线演示地址源码地址可以学到的技术前端技术后端技术部署上线开发工具其他技术业务功能 项目讲解前端创建项目 - 安装PNPM - 使用VSCODE - 安装插件首页顶部与底部 - 封装组建 - 使用scss左右布局中间内容部分路由 - vue-routerBANNER- 走马…...

Vue3的proxy

vue3.0中,使用proxy替换了原来遍历对象使用Object.defineProperty方法给属性添加set/get    vue的核心能力之一是监听用户定义的状态变化并响应式刷新DOM   vue2是通过替换状态对象属性的getter和setter来实现的,vue3则通过proxy进行   改为proxy后,可以突破vue当前的…...

Vue Router的介绍与引入

在这里是记录我引入Vue Router的全过程&#xff0c;引入方面也最好先看官方文档 一.介绍 Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成&#xff0c;让用 Vue.js 构建单页应用变得轻而易举。功能包括&#xff1a; 嵌套路由映射动态路由选择模块化、基于组件的…...

StratifiedKFold解释和代码实现

StratifiedKFold解释和代码实现 文章目录 一、StratifiedKFold是什么&#xff1f;二、 实验数据设置2.1 实验数据生成代码2.2 代码结果 三、实验代码3.1 实验代码3.2 实验结果3.3 结果解释3.4 数据打乱对这种交叉验证的影响。 四、总结 一、StratifiedKFold是什么&#xff1f; …...

四十八----react实战

一、项目中css模块化管理 1、css-loader 以下可以使用styles.xxx方式使用class是因为使用css-loader配置了module。 import styles from ./index.less export const App(){return <div className={styles.xxx}>hello word</div> }//webpack配置 {test:/\.css$/,u…...

三步实现Java的SM2前端加密后端解密

秦医如毒&#xff0c;无药可解。 话不多说&#xff0c;先上需要用到的js文件下载链接 和 jsp前端代码。 第一步&#xff1a;下载两个必备的js文件—— crypto-js.js、sm2.js 。 它们的下载链接如下↓&#xff08;该网页不魔法上网的话会很卡&#xff0c;毕竟github&#x…...

1分钟带你了解golang(go语言)

Golang&#xff1a;也被称为Go语言&#xff0c;是一种开源的编程语言。由Google的Robert Griesemer、Rob Pike和Ken Thompson于2007年开始设计&#xff0c;2009年11月正式对外发布。&#xff08;被誉为21世纪的C语言&#xff09; 像python一样的优雅&#xff0c;有c一样的性能…...

CSS-4

平面转换 整体认识 div {margin: 100px 0;width: 100px;height: 100px;background-color: pink;/* 过渡效果 */transition: all 1s;}/* 当鼠标悬停到div时&#xff0c;进行平面转换 */div:hover {transform: translate(800px) rotate(360deg) scale(2) skew(180deg);}作用&…...

Python为何适合开发AI项目?

Python在人工智能&#xff08;AI&#xff09;项目中的流行和广泛应用归因于多个因素&#xff0c;其中一些主要原因包括&#xff1a; 1、易学易用&#xff1a; Python语法简洁清晰&#xff0c;易于学习和理解。这使得新手能够更容易上手&#xff0c;并且对于处理复杂的AI算法和…...

总结心得:各设计模式使用场景

单例模式&#xff1a;创建单个对象 工厂模式&#xff1a;创建对象交给工厂完成&#xff0c;当需要创建的对象是一系列相互关联或相互依赖的产品族时 原型模式&#xff1a;克隆对象&#xff0c;避免创建初始化开销 建造者模式&#xff1a;创建一个复杂对象&#xff0c;该对象…...

详解Vue3中的事件监听方式

本文主要介绍Vue3中的事件监听方式。 目录 一、v-on指令二、使用符号简写三、事件修饰符四、动态事件名五、常见的监听事件六、自定义事件 在Vue3中&#xff0c;事件监听的方式与Vue2有一些不同。 下面是Vue3中事件监听方式的详细介绍&#xff1a; 一、v-on指令 Vue3中仍然使…...

Unity关于easySave2 easySave3保存数据的操作;包含EasySave3运行报错的解决

关于easySave2 easySave3保存数据的操作&#xff1b;包含EasySave3运行报错的解决 /// 数据存储路径&#xff08;Easy Save的默认储存位置为&#xff1a;Application.persistentDataPath&#xff0c;为了方便我们可以给它指定储存路径&#xff09; #region 存储数据/*/// /// 存…...

2022年全球软件质量效能大会(QECon上海站)-核心PPT资料下载

一、峰会简介 近年来&#xff0c;以云计算、移动互联网、物联网、工业互联网、人工智能、大数据及区块链等新一代信息技术构建的智能化应用和产品出现爆发式增长&#xff0c;突破了对于软件形态的传统认知&#xff0c;正以各种展现方式诠释着对新型智能软件的定义。这也使得对…...

【python报错】UserWarning: train_labels has been renamed targets

UserWarning: train_labels has been renamed targetswarnings.warn(“train_labels has been renamed targets”) 这是一条 Python 警告信息&#xff0c;它表示 train_labels 这个变量已经被重命名为 targets&#xff0c;在将来的版本中可能会移除 train_labels。因此&#x…...

算法专题四:前缀和

前缀和 一.一维前缀和(模板)&#xff1a;1.思路一&#xff1a;暴力解法2.思路二&#xff1a;前缀和思路 二. 二维前缀和(模板)&#xff1a;1.思路一&#xff1a;构造前缀和数组 三.寻找数组的中心下标&#xff1a;1.思路一&#xff1a;前缀和 四.除自身以外数组的乘积&#xff…...

STM32学习笔记十五:WS2812制作像素游戏屏-飞行射击游戏(5)探索动画之帧动画

本章又是个重要的章节——动画。 动画&#xff0c;本质上时一系列静态的画面连续播放&#xff0c;欺骗人眼产生动画效果。这个原理自打十九世纪电影诞生开始&#xff0c;就从来没变过。 我们的游戏中也需要一些动画效果&#xff0c;比如&#xff0c;被击中时的受伤效果&#…...

期末复习(程序设计)

根据字符出现频率排序 【问题描述】 给定一个字符串 s &#xff0c;根据字符出现的 频率 对其进行降序排序。一个字符出现的频率是它出现在字符串中的次数。 返回已排序的字符串。 频率相同的的字符按ascii值降序排序。 s不包含空格、制表符、换行符等特殊字符。 【输入格…...

html-css-js移动端导航栏底部固定+i18n国际化全局

需求&#xff1a;要做一个移动端的仿照小程序的导航栏页面操作&#xff0c;但是这边加上了i18n国家化&#xff0c;由于页面切换的时候会导致国际化失效&#xff0c;所以写了这篇文章 1.效果 切换页面的时候中英文也会跟着改变&#xff0c;不会导致切换后回到默认的语言 2.实现…...

Ubuntu Linux 入门指南:面向初学者

目录 1. Ubuntu Linux 简介 Ubuntu 的由来 Ubuntu 与其他 Linux 发行版的比较 Debian&#xff1a; Fedora&#xff1a; openSUSE&#xff1a; Arch Linux&#xff1a; Linux Mint&#xff1a; 第二部分&#xff1a;安装 Ubuntu 1. 准备安装 系统需求 创建 Ubuntu 启…...

常见算法面试题目

前言 总结一些常见的算法题目&#xff0c;每一个题目写一行思路&#xff0c;方便大家复习。具体题目的来源是下面的网站。 剑指offer 剑指offe2 leetcode200题 leetcode 100题 leetcode150题 leetcode 75题 文章目录 前言二叉树非递归遍历牛客JZ31 栈的压入、弹出序列 (…...

PiflowX组件-JDBCWrite

JDBCWrite组件 组件说明 使用JDBC驱动向任意类型的关系型数据库写入数据。 计算引擎 flink 有界性 Sink: Batch Sink: Streaming Append & Upsert Mode 组件分组 Jdbc 端口 Inport&#xff1a;默认端口 outport&#xff1a;默认端口 组件属性 名称展示名称默…...

算法导论复习题目

这题需要考虑什么呢&#xff1f; 一换元&#xff0c;二要使用主方法猜出结果&#xff0c;三是证明的时候添加一个低阶项来消除 LC检索 C&#xff08;x&#xff09;是从上帝视角来看的成本 对C(x)的一个估计&#xff1a; 由两个部分组成&#xff0c;就相当于由以往的经验对未来…...

HTTPS协议详解

目录 前言 一、HTTPS协议 1、加密是什么 2、为什么要加密 二、常见加密方式 1、对称加密 2、非对称加密 三、数据摘要与数据指纹 1、数据摘要 2、数据指纹 四、HTTPS加密策略探究 1、只使用对称加密 2、只使用非对称加密 3、双方都使用非对称加密 4、对称加密非…...

菜鸟学习vue3笔记-vue3 router回顾

1、路由router pnpm i vue-router2、创建使用环境 1.src下创建 router文件夹、里面创建index.ts文件 //创建一个路由暴露出去//1.引入createRouter import { createRouter, createWebHistory } from "vue-router";// import Home from ../components/Home.vue//…...

Mybatis枚举类型处理和类型处理器

专栏精选 引入Mybatis Mybatis的快速入门 Mybatis的增删改查扩展功能说明 mapper映射的参数和结果 Mybatis复杂类型的结果映射 Mybatis基于注解的结果映射 Mybatis枚举类型处理和类型处理器 再谈动态SQL Mybatis配置入门 Mybatis行为配置之Ⅰ—缓存 Mybatis行为配置…...

2023 NCTF writeup

CRYPTO Sign 直接给了fx,gx&#xff0c;等于私钥给了&#xff0c;直接套代码&#xff0c;具体可以参考&#xff1a; https://0xffff.one/d/1424 fx [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…...

golang的大杀器协程goroutine

在Golang中&#xff0c;协程&#xff08;Goroutine&#xff09;是轻量级的执行单元&#xff0c;用于实现并发编程。它是Golang语言的重要组成部分&#xff0c;提供了简洁、高效的方式来处理并发任务。 特点&#xff1a; 1&#xff09;轻量级&#xff1a;Go语言的协程是轻量级…...

[Angular] 笔记 9:list/detail 页面以及@Output

1. Output input 好比重力&#xff0c;向下传递数据&#xff0c;list 传给 detail&#xff0c;smart 组件传给 dumb 组件&#xff0c;父组件传给子组件。input 顾名思义&#xff0c;输入数据给组件。 output 与之相反&#xff0c;好比火箭&#xff0c;向上传递数据或事件。ou…...

Linux学习笔记(一)

如果有自己的物理服务器请先查看这篇文章 文章目录 网卡配置Linux基础指令ls:列出目录内容cd(mkdir.rmkdir): 切换文件夹(创建,删除操作)cp:复制文件或目录mv:文件/文件夹移动cat:查看文件vi:文件查看编辑man:查看命令手册more: 查看文件内容less : 查看文件内容 ps: 显示当前进…...

Python 爬虫 教程

python爬虫框架&#xff1a;Scrapyd&#xff0c;Feapder&#xff0c;Gerapy 参考文章&#xff1a; python爬虫工程师&#xff0c;如何从零开始部署ScrapydFeapderGerapy&#xff1f; - 知乎 神器&#xff01;五分钟完成大型爬虫项目 - 知乎 爬虫框架-feapder - 知乎 scrap…...