golang切片原理详细解析

目录
  • 切片的解析
  • 切片的初始化
    • 字面量初始化
    • make初始化
  • 切片的截取
  • 切片的复制
  • 切片的扩容
  • 总结

切片的解析

当我们的代码敲下[]时,便会被go编译器解析为抽象语法树上的切片节点, 被初始化为切片表达式SliceType:

// go/src/cmd/compile/internal/syntax/parser.go
// TypeSpec = identifier [ TypeParams ] [ "=" ] Type .
func (p *parser) typeDecl(group *Group) Decl {
    ...
    if p.tok == _Lbrack {
        // d.Name "[" ...
        // array/slice type or type parameter list
        pos := p.pos()
        p.next()
        switch p.tok {
        ...
        case _Rbrack:
            // d.Name "[" "]" ...
            p.next()
            d.Type = p.sliceType(pos)
        ...
        }
    }
    ...
}
func (p *parser) sliceType(pos Pos) Expr {
    t := new(SliceType)
    t.pos = pos
    t.Elem = p.type_()
    return t
}
// go/src/cmd/compile/internal/syntax/nodes.go
type (
    ...
  // []Elem
    SliceType struct {
        Elem Expr
        expr
    }
  ...
)

编译时切片定义为Slice结构体,属性只包含同一类型的元素Elem,编译时通过NewSlice()函数进行创建:

// go/src/cmd/compile/internal/types/type.go
type Slice struct {
    Elem *Type // element type
}
func NewSlice(elem *Type) *Type {
    if t := elem.cache.slice; t != nil {
        if t.Elem() != elem {
            base.Fatalf("elem mismatch")
        }
        if elem.HasTParam() != t.HasTParam() || elem.HasShape() != t.HasShape() {
            base.Fatalf("Incorrect HasTParam/HasShape flag for cached slice type")
        }
        return t
    }
    t := newType(TSLICE)
    t.extra = Slice{Elem: elem}
    elem.cache.slice = t
    if elem.HasTParam() {
        t.SetHasTParam(true)
    }
    if elem.HasShape() {
        t.SetHasShape(true)
    }
    return t
}

切片的初始化

切片有两种初始化方式,一种声明即初始化称为字面量初始化,一种称为make初始化,

例如:

litSlic := []int{1,2,3,4}  // 字面量初始化
makeSlic := make([]int,0)  // make初始化

字面量初始化

切片字面量的初始化是在生成抽象语法树后进行遍历的walk阶段完成的。通过walkComplit方法,首先会进行类型检查,此时会计算出切片元素的个数length,然后通过slicelit方法完成具体的初始化工作。整个过程会先创建一个数组存储于静态区(static array),并在堆区创建一个新的切片(auto array),然后将静态区的数据复制到堆区(copy the static array to the auto array),对于切片中的元素会按索引位置一个一个的进行赋值。 在程序启动时这一过程会加快切片的初始化。

// go/src/cmd/compile/internal/walk/complit.go
// walkCompLit walks a composite literal node:
// OARRAYLIT, OSLICELIT, OMAPLIT, OSTRUCTLIT (all CompLitExpr), or OPTRLIT (AddrExpr).
func walkCompLit(n ir.Node, init *ir.Nodes) ir.Node {
    if isStaticCompositeLiteral(n) && !ssagen.TypeOK(n.Type()) {
        n := n.(*ir.CompLitExpr) // not OPTRLIT
        // n can be directly represented in the read-only data section.
        // Make direct reference to the static data. See issue 12841.
        vstat := readonlystaticname(n.Type())
        fixedlit(inInitFunction, initKindStatic, n, vstat, init)
        return typecheck.Expr(vstat)
    }
    var_ := typecheck.Temp(n.Type())
    anylit(n, var_, init)
    return var_
}

类型检查时,计算出切片长度的过程为:

// go/src/cmd/compile/internal/typecheck/expr.go
func tcCompLit(n *ir.CompLitExpr) (res ir.Node) {
    ...
    t := n.Type()
    base.AssertfAt(t != nil, n.Pos(), "missing type in composite literal")

    switch t.Kind() {
    ...
    case types.TSLICE:
        length := typecheckarraylit(t.Elem(), -1, n.List, "slice literal")
        n.SetOp(ir.OSLICELIT)
        n.Len = length
    ...
  }
    return n
}

切片的具体初始化过程为:

  • 在静态存储区创建一个数组;
  • 将数组赋值给一个常量部分;
  • 创建一个自动指针即切片分配到堆区,并指向数组;
  • 将数组中的数据从静态区拷贝到切片的堆区;
  • 对每一个切片元素按索引位置分别进行赋值;
  • 最后将分配到堆区的切片赋值给定义的变量;

源代码通过注释也写明了整个过程。

// go/src/cmd/compile/internal/walk/complit.go
func anylit(n ir.Node, var_ ir.Node, init *ir.Nodes) {
    t := n.Type()
    switch n.Op() {
  ...
  case ir.OSLICELIT:
        n := n.(*ir.CompLitExpr)
        slicelit(inInitFunction, n, var_, init)
  ...
  }
}
func slicelit(ctxt initContext, n *ir.CompLitExpr, var_ ir.Node, init *ir.Nodes) {
    // make an array type corresponding the number of elements we have
    t := types.NewArray(n.Type().Elem(), n.Len)
    types.CalcSize(t)

    if ctxt == inNonInitFunction {
        // put everything into static array
        vstat := staticinit.StaticName(t)

        fixedlit(ctxt, initKindStatic, n, vstat, init)
        fixedlit(ctxt, initKindDynamic, n, vstat, init)

        // copy static to slice
        var_ = typecheck.AssignExpr(var_)
        name, offset, ok := staticinit.StaticLoc(var_)
        if !ok || name.Class != ir.PEXTERN {
            base.Fatalf("slicelit: %v", var_)
        }
        staticdata.InitSlice(name, offset, vstat.Linksym(), t.NumElem())
        return
    }

    // recipe for var = []t{...}
    // 1. make a static array
    //  var vstat [...]t
    // 2. assign (data statements) the constant part
    //  vstat = constpart{}
    // 3. make an auto pointer to array and allocate heap to it
    //  var vauto *[...]t = new([...]t)
    // 4. copy the static array to the auto array
    //  *vauto = vstat
    // 5. for each dynamic part assign to the array
    //  vauto[i] = dynamic part
    // 6. assign slice of allocated heap to var
    //  var = vauto[:]
    //
    // an optimization is done if there is no constant part
    //  3. var vauto *[...]t = new([...]t)
    //  5. vauto[i] = dynamic part
    //  6. var = vauto[:]

    // if the literal contains constants,
    // make static initialized array (1),(2)
    var vstat ir.Node

    mode := getdyn(n, true)
    if mode&initConst != 0 && !isSmallSliceLit(n) {
        if ctxt == inInitFunction {
            vstat = readonlystaticname(t)
        } else {
            vstat = staticinit.StaticName(t)
        }
        fixedlit(ctxt, initKindStatic, n, vstat, init)
    }

    // make new auto *array (3 declare)
    vauto := typecheck.Temp(types.NewPtr(t))

    // set auto to point at new temp or heap (3 assign)
    var a ir.Node
    if x := n.Prealloc; x != nil {
        // temp allocated during order.go for dddarg
        if !types.Identical(t, x.Type()) {
            panic("dotdotdot base type does not match order's assigned type")
        }
        a = initStackTemp(init, x, vstat)
    } else if n.Esc() == ir.EscNone {
        a = initStackTemp(init, typecheck.Temp(t), vstat)
    } else {
        a = ir.NewUnaryExpr(base.Pos, ir.ONEW, ir.TypeNode(t))
    }
    appendWalkStmt(init, ir.NewAssignStmt(base.Pos, vauto, a))

    if vstat != nil && n.Prealloc == nil && n.Esc() != ir.EscNone {
        // If we allocated on the heap with ONEW, copy the static to the
        // heap (4). We skip this for stack temporaries, because
        // initStackTemp already handled the copy.
        a = ir.NewStarExpr(base.Pos, vauto)
        appendWalkStmt(init, ir.NewAssignStmt(base.Pos, a, vstat))
    }

    // put dynamics into array (5)
    var index int64
    for _, value := range n.List {
        if value.Op() == ir.OKEY {
            kv := value.(*ir.KeyExpr)
            index = typecheck.IndexConst(kv.Key)
            if index < 0 {
                base.Fatalf("slicelit: invalid index %v", kv.Key)
            }
            value = kv.Value
        }
        a := ir.NewIndexExpr(base.Pos, vauto, ir.NewInt(index))
        a.SetBounded(true)
        index++
        // TODO need to check bounds?
        switch value.Op() {
        case ir.OSLICELIT:
            break
        case ir.OARRAYLIT, ir.OSTRUCTLIT:
            value := value.(*ir.CompLitExpr)
            k := initKindDynamic
            if vstat == nil {
                // Generate both static and dynamic initializations.
                // See issue #31987.
                k = initKindLocalCode
            }
            fixedlit(ctxt, k, value, a, init)
            continue
        }
        if vstat != nil && ir.IsConstNode(value) { // already set by copy from static value
            continue
        }
        // build list of vauto[c] = expr
        ir.SetPos(value)
        as := ir.NewAssignStmt(base.Pos, a, value)
        appendWalkStmt(init, orderStmtInPlace(typecheck.Stmt(as), map[string][]*ir.Name{}))
    }
    // make slice out of heap (6)
    a = ir.NewAssignStmt(base.Pos, var_, ir.NewSliceExpr(base.Pos, ir.OSLICE, vauto, nil, nil, nil))
    appendWalkStmt(init, orderStmtInPlace(typecheck.Stmt(a), map[string][]*ir.Name{}))
}

make初始化

当使用make初始化一个切片时,会被编译器解析为一个OMAKESLICE操作:

// go/src/cmd/compile/internal/walk/expr.go
func walkExpr1(n ir.Node, init *ir.Nodes) ir.Node {
    switch n.Op() {
    ...
    case ir.OMAKESLICE:
        n := n.(*ir.MakeExpr)
        return walkMakeSlice(n, init)
    ...
}

如果make初始化一个较大的切片则会逃逸到堆中,如果分配了一个较小的切片则直接在栈中分配。

  • walkMakeSlice函数中,如果未指定切片的容量Cap,则初始容量等于切片的长度。
  • 如果切片的初始化未发生内存逃逸n.Esc() == ir.EscNone,则会先在内存中创建一个同样容量大小的数组NewArray(), 然后按切片长度将数组中的值arr[:l]赋予切片。
  • 如果发生了内存逃逸,切片会调用运行时函数makeslicemakeslice64在堆中完成对切片的初始化。
// go/src/cmd/compile/internal/walk/builtin.go
func walkMakeSlice(n *ir.MakeExpr, init *ir.Nodes) ir.Node {
    l := n.Len
    r := n.Cap
    if r == nil {
        r = safeExpr(l, init)
        l = r
    }
    ...
    if n.Esc() == ir.EscNone {
        if why := escape.HeapAllocReason(n); why != "" {
            base.Fatalf("%v has EscNone, but %v", n, why)
        }
        // var arr [r]T
        // n = arr[:l]
        i := typecheck.IndexConst(r)
        if i < 0 {
            base.Fatalf("walkExpr: invalid index %v", r)
        }
        ...
        t = types.NewArray(t.Elem(), i) // [r]T
        var_ := typecheck.Temp(t)
        appendWalkStmt(init, ir.NewAssignStmt(base.Pos, var_, nil))  // zero temp
        r := 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)
    }
    // n escapes; set up a call to makeslice.
    // When len and cap can fit into int, use makeslice instead of
    // makeslice64, which is faster and shorter on 32 bit platforms.
    len, cap := l, r
    fnname := "makeslice64"
    argtype := types.Types[types.TINT64]
    // Type checking guarantees that TIDEAL len/cap are positive and fit in an int.
    // The case of len or cap overflow when converting TUINT or TUINTPTR to TINT
    // will be handled by the negative range checks in makeslice during runtime.
    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"
        argtype = types.Types[types.TINT]
    }
    fn := typecheck.LookupRuntime(fnname)
    ptr := mkcall1(fn, types.Types[types.TUNSAFEPTR], init, reflectdata.TypePtr(t.Elem()), typecheck.Conv(len, argtype), typecheck.Conv(cap, argtype))
    ptr.MarkNonNil()
    len = typecheck.Conv(len, types.Types[types.TINT])
    cap = typecheck.Conv(cap, types.Types[types.TINT])
    sh := ir.NewSliceHeaderExpr(base.Pos, t, ptr, len, cap)
    return walkExpr(typecheck.Expr(sh), init)
}

切片在栈中初始化还是在堆中初始化,存在一个临界值进行判断。临界值maxImplicitStackVarSize默认为64kb。从下面的源代码可以看到,显式变量声明explicit variable declarations 和隐式变量implicit variables逃逸的临界值并不一样。

  • 当我们使用var变量声明以及:=赋值操作时,内存逃逸的临界值为10M, 小于该值的对象会分配在栈中。
  • 当我们使用如下操作时,内存逃逸的临界值为64kb,小于该值的对象会分配在栈中。
p := new(T)
p := &T{}
s := make([]T, n)
s := []byte("...") 
// go/src/cmd/compile/internal/ir/cfg.go
var (
    // maximum size variable which we will allocate on the stack.
    // This limit is for explicit variable declarations like "var x T" or "x := ...".
    // Note: the flag smallframes can update this value.
    MaxStackVarSize = int64(10 * 1024 * 1024)
    // maximum size of implicit variables that we will allocate on the stack.
    //   p := new(T)          allocating T on the stack
    //   p := &T{}            allocating T on the stack
    //   s := make([]T, n)    allocating [n]T on the stack
    //   s := []byte("...")   allocating [n]byte on the stack
    // Note: the flag smallframes can update this value.
    MaxImplicitStackVarSize = int64(64 * 1024)
    // MaxSmallArraySize is the maximum size of an array which is considered small.
    // Small arrays will be initialized directly with a sequence of constant stores.
    // Large arrays will be initialized by copying from a static temp.
    // 256 bytes was chosen to minimize generated code + statictmp size.
    MaxSmallArraySize = int64(256)
)

切片的make初始化就属于s := make([]T, n)操作,当切片元素分配的内存大小大于64kb时, 切片会逃逸到堆中进行初始化。此时会调用运行时函数makeslice来完成这一个过程:

// go/src/runtime/slice.go
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 {
        // NOTE: Produce a 'len out of range' error instead of a
        // 'cap out of range' error when someone does make([]T, bignumber).
        // 'cap out of range' is true too, but since the cap is only being
        // supplied implicitly, saying len is clearer.
        // See golang.org/issue/4085.
        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 {
            panicmakeslicelen()
        }
        panicmakeslicecap()
    }
    return mallocgc(mem, et, true)
}

根据切片的运行时结构定义,运行时切片结构底层维护着切片的长度len、容量cap以及指向数组数据的指针array:

// go/src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
// 或者
// go/src/reflect/value.go
// SliceHeader is the runtime representation of a slice.
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

切片的截取

从切片的运行时结构已经知道,切片底层数据是一个数组,切片本身只是持有一个指向改数组数据的指针。因此,当我们对切片进行截取操作时,新的切片仍然指向原切片的底层数据,当对原切片数据进行更新时,意味着新切片相同索引位置的数据也发生了变化:

slic := []int{1, 2, 3, 4, 5}
slic1 := slic[:2]
fmt.Printf("slic1: %v\n", slic1)
slic[0] = 0
fmt.Printf("slic: %v\n", slic)
fmt.Printf("slic1: %v\n", slic1)
// slic1: [1 2]
// slic: [0 2 3 4 5]
// slic1: [0 2]

切片截取后,虽然底层数据没有发生变化,但指向的数据范围发生了变化,表现为截取后的切片长度、容量会相应发生变化:

  • 长度为截取的范围
  • 容量为截取起始位置到原切片末尾的范围
slic := []int{1, 2, 3, 4, 5}
slic1 := slic[:2]
slic2 := slic[2:]
fmt.Printf("len(slic): %v\n", len(slic))
fmt.Printf("cap(slic): %v\n", cap(slic))
fmt.Printf("len(slic1): %v\n", len(slic1))
fmt.Printf("cap(slic1): %v\n", cap(slic1))
fmt.Printf("len(slic2): %v\n", len(slic2))
fmt.Printf("cap(slic2): %v\n", cap(slic2))
// len(slic): 5
// cap(slic): 5

// len(slic1): 2
// cap(slic1): 5

// len(slic2): 3
// cap(slic2): 3

所以,切片截取变化的是底层data指针、长度以及容量,data指针指向的数组数据本身没有变化。切片的赋值拷贝就等价于于全切片,底层data指针仍然指向相同的数组地址,长度和容量保持不变:

slic := []int{1, 2, 3, 4, 5}
s := slic  // 等价于 s := slic[:]

当切片作为参数传递时,即使切片中包含大量的数据,也只是切片数据地址的拷贝,拷贝的成本是较低的。

切片的复制

当我们想要完整拷贝一个切片时,可以使用内置的copy函数,效果类似于"深拷贝"。

slic := []int{1, 2, 3, 4, 5}
var slic1 []int
copy(slic1, slic)
fmt.Printf("slic: %p\n", slic)
fmt.Printf("slic1: %p\n", slic1)
// slic: 0xc0000aa030
// slic1: 0x0

完整复制后,新的切片指向了新的内存地址。切片的复制在运行时会调用slicecopy()函数,通过memmove移动数据到新的内存地址:

// go/src/runtime/slice.go
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
    if fromLen == 0 || toLen == 0 {
        return 0
    }

    n := fromLen
    if toLen < n {
        n = toLen
    }
    ...
    if size == 1 { // common case worth about 2x to do here
        // TODO: is this still worth it with new memmove impl?
        *(*byte)(toPtr) = *(*byte)(fromPtr) // known to be a byte pointer
    } else {
        memmove(toPtr, fromPtr, size)
    }
    return n
}

切片的扩容

切片元素个数可以动态变化,切片初始化后会确定一个初始化容量,当容量不足时会在运行时通过growslice进行扩容:

func growslice(et *_type, old slice, cap int) slice {
    ...
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256
        if old.cap < threshold {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                // 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 = cap
            }
        }
    }
    ...
    memmove(p, old.array, lenmem)
    return slice{p, old.len, newcap}
}

从growslice的代码可以看出:

  • 当新申请的容量(cap)大于二倍旧容量(old.cap)时,最终容量(newcap)是新申请的容量;
  • 当新申请的容量(cap)小于二倍旧容量(old.cap)时,
    • 如果旧容量小于256,最终容量为旧容量的2倍;
    • 如果旧容量大于等于256,则会按照公式newcap += (newcap + 3*threshold) / 4来确定最终容量。实际的表现为:
  1. 当切片长度小于等于1024时,最终容量是旧容量的2倍;
  2. 当切片长度大于1024时,最终容量是旧容量的1.25倍,随着长度的增长,大于1.25倍;
  3. 扩容后,会通过memmove()函数将旧的数组移动到新的地址,因此扩容后新的切片一般和原来的地址不同。

示例:

var slic []int
oldCap := cap(slic)
for i := 0; i < 2048; i++ {
  slic = append(slic, i)
  newCap := cap(slic)
  grow := float32(newCap) / float32(oldCap)
  if newCap != oldCap {
    fmt.Printf("len(slic):%v cap(slic):%v grow:%v %p\n", len(slic), cap(slic), grow, slic)
  }
  oldCap = newCap
}
// len(slic):1     cap(slic):1     grow:+Inf       0xc0000140c0
// len(slic):2     cap(slic):2     grow:2          0xc0000140e0
// len(slic):3     cap(slic):4     grow:2          0xc000020100
// len(slic):5     cap(slic):8     grow:2          0xc00001e340
// len(slic):9     cap(slic):16    grow:2          0xc000026080
// len(slic):17    cap(slic):32    grow:2          0xc00007e000
// len(slic):33    cap(slic):64    grow:2          0xc000100000
// len(slic):65    cap(slic):128   grow:2          0xc000102000
// len(slic):129   cap(slic):256   grow:2          0xc000104000
// len(slic):257   cap(slic):512   grow:2          0xc000106000
// len(slic):513   cap(slic):1024  grow:2          0xc000108000
// len(slic):1025  cap(slic):1280  grow:1.25       0xc00010a000
// len(slic):1281  cap(slic):1696  grow:1.325      0xc000114000
// len(slic):1697  cap(slic):2304  grow:1.3584906  0xc00011e000

总结

切片在编译时定义为Slice结构体,并通过NewSlice()函数进行创建;

type Slice struct {
  Elem *Type // element type
}

切片的运行时定义为slice结构体, 底层维护着指向数组数据的指针,切片长度以及容量;

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}
  • 切片字面量初始化时,会在编译时的类型检查阶段计算出切片的长度,然后在walk遍历语法树时创建底层数组,并将切片中的每个字面量元素按索引赋值给数组,切片的数据指针指向该数组;
  • 切片make初始化时,会调用运行时makeslice函数进行内存分配,当内存占用大于64kb时会逃逸到堆中;
  • 切片截取后,底层数组数据没有发生变化,但指向的数据范围发生了变化,表现为截取后的切片长度、容量会相应发生变化:
    • 长度为截取的范围
    • 容量为截取起始位置到原切片末尾的范围
  • 使用copy复制切片时,会在运行时会调用slicecopy()函数,通过memmove移动数据到了新的内存地址;
  • 切片扩容是通过运行时growslice函数完成的,一般表现为:
    • 当切片长度小于等于1024时,最终容量是旧容量的2倍;
    • 当切片长度大于1024时,最终容量是旧容量的1.25倍,并随着长度的增长,缓慢大于1.25倍;
    • 扩容时会通过memmove()函数将旧的数组移动到新的地址,因此扩容后地址会发生变化。

到此这篇关于golang切片原理详细解析的文章就介绍到这了,更多相关golang切片 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • golang 删除切片的某个元素及剔除切片内的零值方式

    看代码吧~ func remove(slice []interface{}, elem interface{}) []interface{}{ if len(slice) == 0 { return slice } for i, v := range slice { if v == elem { slice = append(slice[:i], slice[i+1:]...) return remove(slice,elem) break } } return slice } func rem

  • golang修改结构体中的切片值方法

    golang修改结构体中的切片值,直接传结构体地址就可以 package main import "fmt" type rspInfo struct { KeyWords string `json:"key_words"` Value []string `json:"value"` } func setSlice(te *[]string){ str := "12" *te = append(*te,str) } //结构提传

  • golang常用手册之切片(Slice)原理

    切片,这是一个在go语言中引入的新的理念.它有一些特征如下: 对数组抽象 数组长度不固定 可追加元素 切片容量可增大 容量大小成片增加 我们先把上面的理念整理在这里,但是实际的还是要撸码来解决问题. 定义或申明切片 首先我们看看申明切片: var sliceName []type 定义完成后,我们需要定义切片: sliceName = make([]type, len) 也可以适当简写: sliceName := make([]type, len) 在上面的例子中,我们申明了一个切片,我们现在先

  • 浅谈Golang 切片(slice)扩容机制的原理

    我们知道 Golang 切片(slice) 在容量不足的情况下会进行扩容,扩容的原理是怎样的呢?是不是每次扩一倍?下面我们结合源码来告诉你答案. 一.源码 Version : go1.15.6  src/runtime/slice.go //go1.15.6 源码 src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { //省略部分判断代码 //计算扩容部分 //其中,cap : 所需容量,newcap

  • golang中切片copy复制和等号复制的区别介绍

    结论: copy复制会比等号复制慢.但是copy复制为值复制,改变原切片的值不会影响新切片.而等号复制为指针复制,改变原切片或新切片都会对另一个产生影响. 测试复制速度: func TestArr1(t *testing.T) { var a []int for i := 0; i < 100000000; i++ { a = append(a, i) } start := time.Now().UnixNano() var b = make([]int, 1000000) copy(b, a)

  • 浅谈golang slice 切片原理

    slice介绍 数组的长度在定义之后无法再次修改:数组是值类型,每次传递都将产生一份副本.显然这种数据结构无法完全满足开发者的真实需求.在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要"动态数组".在Go里面这种数据结构叫slice,slice并不是真正意义上的动态数组,而是一个引用类型.slice总是指向一个底层array,slice的声明也可以像array一样,只是不需要长度,它是可变长的,可以随时往slice里面加数据. 初看起来,数组切片就像一个指向数组的指针,实际

  • golang切片原理详细解析

    目录 切片的解析 切片的初始化 字面量初始化 make初始化 切片的截取 切片的复制 切片的扩容 总结 切片的解析 当我们的代码敲下[]时,便会被go编译器解析为抽象语法树上的切片节点, 被初始化为切片表达式SliceType: // go/src/cmd/compile/internal/syntax/parser.go // TypeSpec = identifier [ TypeParams ] [ "=" ] Type . func (p *parser) typeDecl(g

  • Golang Mutex 原理详细解析

    目录 前言 Lock 单协程加锁 加锁被阻塞 Unlock 无协程阻塞下的解锁 解锁并唤醒协程 自旋 什么是自旋 自旋条件 自旋的优势 自旋的问题 Mutex 的模式 Normal 模式 Starving 模式 Woken 状态 前言 互斥锁是在并发程序中对共享资源进行访问控制的主要手段.对此 Go 语言提供了简单易用的 Mutex.Mutex 和 Goroutine 合作紧密,概念容易混淆,一定注意要区分各自的概念. Mutex 是一个结构体,对外提供 Lock()和Unlock()两个方法,

  • vue.js diff算法原理详细解析

    目录 diff算法的概念 虚拟Dom h函数 diff对比规则 patch patchVnode updateChildren 总结 diff算法的概念 diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom.顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom. 虚拟Dom 上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟

  • Tomcat中的catalina.bat原理详细解析

    前言 本文主要给大家详细解析了关于Tomcat中catalina.bat原理的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. tomcat 的真正启动是在 catalina.bat 设置并启动的.startup.bat 只是找到catalina.bat 然后执行catalina.bat 来启动tomat的.下面我们来分析下catalina.bat 验证CATALINA_HOME 环境变量 验证CATALINA_HOME 设置是否正确,如果不正确,重新设置CATALIN

  • Tomcat中的startup.bat原理详细解析

    前言 在刚开始接触计算机,一开始就是win2000,所以对批处理脚本命令都不会.平时启TOMCAT都是鼠标双击startup.bat了,很少看过里面写的是什么,也借学习TOMCAT的机会学习一下批处理的常用命令,不求都记住,但求以后再见到批处理命令能看的懂,说的出是干什么的.本文主要给大家介绍了关于Tomcat中startup.bat原理的相关内容,下面话不多说了,来一起看看详细的介绍吧. startup.bat 解析 验证CATALINA_HOME 环境变量是否设置,如果没有设置则通过CATA

  • Android 操作系统获取Root权限 原理详细解析

    android root权限破解分析 许多机友新购来的Android机器没有破解过Root权限,无法使用一些需要高权限的软件,以及进行一些高权限的操作,其实破解手机Root权限是比较简单及安全的,破解Root权限的原理就是在手机的/system/bin/或/system/xbin/目录下放置一个可执行文件"su",这是一个二进制文件,相当于电脑上的exe文件,仅仅在系统中置入这个"su"文件是不会给手机的软件或硬件造成任何故障. 下面的代码是android系统原版的

  • C++多态的实现及原理详细解析

    1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数.2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的.3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性.4. 多态用虚函数来实现,结合动态绑定.5. 纯虚函数是虚函数再加上= 0.6. 抽象类是指包括至少一个纯虚函数的类. 纯虚函数:virtual void breathe()=0:即抽象类!必须在子类实现这个函数!

  • Golang底层原理解析String使用实例

    目录 引言 String底层 stringStruct结构 引言 本人因为种种原因(说来听听),放弃大学学的java,走上了golang这条路,本着干一行爱一行的情怀,做开发嘛,不能只会使用这门语言,所以打算开一个底层原理系列,深挖一下,狠狠的掌握一下这门语言 废话不多说,上货 String底层 既然研究底层,那就得全方面覆盖,必须先搞一下基础的东西,那必须直接基本数据类型走起啊, 字符串String的底层我看就很基础 string大家应该都不陌生,go中的string是所有8位字节字符串的集合

  • Docker网络原理及自定义网络详细解析

    Docker在宿主机上虚拟了一个网桥,当创建并启动容器的时候,每一个容器默认都会被分配一个跟网桥网段一致的ip,网桥作为容器的网关,网桥与每一个容器联通,容器间通过网桥可以通信.由于网桥是虚拟出来的,外网无法进行寻址,也就是默认外网无法访问容器,需要在创建启动容器时把宿主机的端口与容器端口进行映射,通过宿主机IP端口访问容器.这是Docker默认的网络,它有一个弊端是只能通过IP让容器互相访问,如果想使用容器名称或容器ID互相访问需要在创建启动容器时候用link的方式修改hosts文件实现.一般

随机推荐