golang切片原理詳細(xì)解析
切片的解析
當(dāng)我們的代碼敲下[]時,便會被go編譯器解析為抽象語法樹上的切片節(jié)點, 被初始化為切片表達(dá)式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結(jié)構(gòu)體,屬性只包含同一類型的元素Elem,編譯時通過NewSlice()函數(shù)進(jìn)行創(chuàng)建:
// 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初始化字面量初始化
切片字面量的初始化是在生成抽象語法樹后進(jìn)行遍歷的walk階段完成的。通過walkComplit方法,首先會進(jìn)行類型檢查,此時會計算出切片元素的個數(shù)length,然后通過slicelit方法完成具體的初始化工作。整個過程會先創(chuàng)建一個數(shù)組存儲于靜態(tài)區(qū)(static array),并在堆區(qū)創(chuàng)建一個新的切片(auto array),然后將靜態(tài)區(qū)的數(shù)據(jù)復(fù)制到堆區(qū)(copy the static array to the auto array),對于切片中的元素會按索引位置一個一個的進(jìn)行賦值。 在程序啟動時這一過程會加快切片的初始化。
// 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
}切片的具體初始化過程為:
- 在靜態(tài)存儲區(qū)創(chuàng)建一個數(shù)組;
- 將數(shù)組賦值給一個常量部分;
- 創(chuàng)建一個自動指針即切片分配到堆區(qū),并指向數(shù)組;
- 將數(shù)組中的數(shù)據(jù)從靜態(tài)區(qū)拷貝到切片的堆區(qū);
- 對每一個切片元素按索引位置分別進(jìn)行賦值;
- 最后將分配到堆區(qū)的切片賦值給定義的變量;
源代碼通過注釋也寫明了整個過程。
// 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初始化
當(dāng)使用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函數(shù)中,如果未指定切片的容量Cap,則初始容量等于切片的長度。 - 如果切片的初始化未發(fā)生內(nèi)存逃逸
n.Esc() == ir.EscNone,則會先在內(nèi)存中創(chuàng)建一個同樣容量大小的數(shù)組NewArray(), 然后按切片長度將數(shù)組中的值arr[:l]賦予切片。 - 如果發(fā)生了內(nèi)存逃逸,切片會調(diào)用運行時函數(shù)
makeslice和makeslice64在堆中完成對切片的初始化。
// 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)
}切片在棧中初始化還是在堆中初始化,存在一個臨界值進(jìn)行判斷。臨界值maxImplicitStackVarSize默認(rèn)為64kb。從下面的源代碼可以看到,顯式變量聲明explicit variable declarations 和隱式變量implicit variables逃逸的臨界值并不一樣。
- 當(dāng)我們使用
var變量聲明以及:=賦值操作時,內(nèi)存逃逸的臨界值為10M, 小于該值的對象會分配在棧中。 - 當(dāng)我們使用如下操作時,內(nèi)存逃逸的臨界值為
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)操作,當(dāng)切片元素分配的內(nèi)存大小大于64kb時, 切片會逃逸到堆中進(jìn)行初始化。此時會調(diào)用運行時函數(shù)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)
}根據(jù)切片的運行時結(jié)構(gòu)定義,運行時切片結(jié)構(gòu)底層維護(hù)著切片的長度len、容量cap以及指向數(shù)組數(shù)據(jù)的指針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
}切片的截取
從切片的運行時結(jié)構(gòu)已經(jīng)知道,切片底層數(shù)據(jù)是一個數(shù)組,切片本身只是持有一個指向改數(shù)組數(shù)據(jù)的指針。因此,當(dāng)我們對切片進(jìn)行截取操作時,新的切片仍然指向原切片的底層數(shù)據(jù),當(dāng)對原切片數(shù)據(jù)進(jìn)行更新時,意味著新切片相同索引位置的數(shù)據(jù)也發(fā)生了變化:
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]切片截取后,雖然底層數(shù)據(jù)沒有發(fā)生變化,但指向的數(shù)據(jù)范圍發(fā)生了變化,表現(xiàn)為截取后的切片長度、容量會相應(yīng)發(fā)生變化:
- 長度為截取的范圍
- 容量為截取起始位置到原切片末尾的范圍
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指針指向的數(shù)組數(shù)據(jù)本身沒有變化。切片的賦值拷貝就等價于于全切片,底層data指針仍然指向相同的數(shù)組地址,長度和容量保持不變:
slic := []int{1, 2, 3, 4, 5}
s := slic // 等價于 s := slic[:]當(dāng)切片作為參數(shù)傳遞時,即使切片中包含大量的數(shù)據(jù),也只是切片數(shù)據(jù)地址的拷貝,拷貝的成本是較低的。
切片的復(fù)制
當(dāng)我們想要完整拷貝一個切片時,可以使用內(nèi)置的copy函數(shù),效果類似于"深拷貝"。
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
完整復(fù)制后,新的切片指向了新的內(nèi)存地址。切片的復(fù)制在運行時會調(diào)用slicecopy()函數(shù),通過memmove移動數(shù)據(jù)到新的內(nèi)存地址:
// 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
}切片的擴(kuò)容
切片元素個數(shù)可以動態(tài)變化,切片初始化后會確定一個初始化容量,當(dāng)容量不足時會在運行時通過growslice進(jìn)行擴(kuò)容:
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的代碼可以看出:
- 當(dāng)新申請的容量(
cap)大于二倍舊容量(old.cap)時,最終容量(newcap)是新申請的容量; - 當(dāng)新申請的容量(
cap)小于二倍舊容量(old.cap)時,- 如果舊容量小于256,最終容量為舊容量的2倍;
- 如果舊容量大于等于256,則會按照公式
newcap += (newcap + 3*threshold) / 4來確定最終容量。實際的表現(xiàn)為:
- 當(dāng)切片長度小于等于1024時,最終容量是舊容量的2倍;
- 當(dāng)切片長度大于1024時,最終容量是舊容量的1.25倍,隨著長度的增長,大于1.25倍;
- 擴(kuò)容后,會通過
memmove()函數(shù)將舊的數(shù)組移動到新的地址,因此擴(kuò)容后新的切片一般和原來的地址不同。
示例:
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總結(jié)
切片在編譯時定義為Slice結(jié)構(gòu)體,并通過NewSlice()函數(shù)進(jìn)行創(chuàng)建;
type Slice struct {
Elem *Type // element type
}切片的運行時定義為slice結(jié)構(gòu)體, 底層維護(hù)著指向數(shù)組數(shù)據(jù)的指針,切片長度以及容量;
type slice struct {
array unsafe.Pointer
len int
cap int
}- 切片字面量初始化時,會在編譯時的類型檢查階段計算出切片的長度,然后在walk遍歷語法樹時創(chuàng)建底層數(shù)組,并將切片中的每個字面量元素按索引賦值給數(shù)組,切片的數(shù)據(jù)指針指向該數(shù)組;
- 切片make初始化時,會調(diào)用運行時
makeslice函數(shù)進(jìn)行內(nèi)存分配,當(dāng)內(nèi)存占用大于64kb時會逃逸到堆中; - 切片截取后,底層數(shù)組數(shù)據(jù)沒有發(fā)生變化,但指向的數(shù)據(jù)范圍發(fā)生了變化,表現(xiàn)為截取后的切片長度、容量會相應(yīng)發(fā)生變化:
- 長度為截取的范圍
- 容量為截取起始位置到原切片末尾的范圍
- 使用
copy復(fù)制切片時,會在運行時會調(diào)用slicecopy()函數(shù),通過memmove移動數(shù)據(jù)到了新的內(nèi)存地址; - 切片擴(kuò)容是通過運行時
growslice函數(shù)完成的,一般表現(xiàn)為:- 當(dāng)切片長度小于等于1024時,最終容量是舊容量的2倍;
- 當(dāng)切片長度大于1024時,最終容量是舊容量的1.25倍,并隨著長度的增長,緩慢大于1.25倍;
- 擴(kuò)容時會通過
memmove()函數(shù)將舊的數(shù)組移動到新的地址,因此擴(kuò)容后地址會發(fā)生變化。
到此這篇關(guān)于golang切片原理詳細(xì)解析的文章就介紹到這了,更多相關(guān)golang切片 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解如何通過Go來操作Redis實現(xiàn)簡單的讀寫操作
作為最常用的分布式緩存中間件——Redis,了解運作原理和如何使用是十分有必要的,今天來學(xué)習(xí)如何通過Go來操作Redis實現(xiàn)基本的讀寫操作,需要的朋友可以參考下2023-09-09
Go語言中函數(shù)可變參數(shù)(Variadic Parameter)詳解
在Python中,在函數(shù)參數(shù)不確定數(shù)量的情況下,可以動態(tài)在函數(shù)內(nèi)獲取參數(shù)。在Go語言中,也有類似的實現(xiàn)方式,本文就來為大家詳細(xì)講解一下2022-07-07
Go語言實現(xiàn)一個簡單生產(chǎn)者消費者模型
本文主要介紹了Go語言實現(xiàn)一個簡單生產(chǎn)者消費者模型,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12
golang服務(wù)報錯:?write:?broken?pipe的解決方案
在開發(fā)在線客服系統(tǒng)的時候,看到日志里有一些錯誤信息,下面這篇文章主要給大家介紹了關(guān)于golang服務(wù)報錯:?write:?broken?pipe的解決方案,需要的朋友可以參考下2022-09-09
Go語言使用Request,Response處理web頁面請求
這篇文章主要介紹了Go語言使用Request,Response處理web頁面請求,需要的朋友可以參考下2022-04-04

