下面這段程序會輸出什麼?linux
package main
import "fmt"
func f(s []string, level int) {
if level > 5 {
return
}
s = append(s, fmt.Sprint(level))
f(s, level+1)
fmt.Println("level:", level, "slice:", s)
}
func main() {
f(nil, 0)
}
其輸出爲:golang
level: 5 slice: [0 1 2 3 4 5]
level: 4 slice: [0 1 2 3 4]
level: 3 slice: [0 1 2 3]
level: 2 slice: [0 1 2]
level: 1 slice: [0 1]
level: 0 slice: [0]
若是對輸出結果有一些疑惑,你須要瞭解這篇文章的內容web
若是你知道告終果,你仍然須要瞭解這篇文章的內容,由於本文完整介紹了windows
切片的典型用法數組
切片的陷阱安全
切片的逃逸分析數據結構
切片的擴容app
切片在編譯與運行時的研究ide
若是你啥都知道了,請直接滑動最下方,雙擊666.函數
切片是某種程度上和其餘語言(例如C語言)中的數組
在使用中有許多類似之處,可是go語言中的切片有許多獨特之處
Slice(切片)表明變長的序列,序列中每一個元素都有相同的類型。
一個slice類型通常寫做[]T
,其中T表明slice中元素的類型;slice的語法和數組很像,可是沒有固定長度。
數組和slice之間有着緊密的聯繫。一個slice是一個輕量級的數據結構,提供了訪問數組子序列(或者所有)元素的功能。一個slice在運行時由三個部分構成:指針、長度和容量。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
指針指向第一個slice元素對應的底層數組元素的地址
長度對應slice中元素的數目;長度不能超過容量
容量通常是從slice的開始位置到底層數據的結尾位置的長度
//切片的聲明1 //nil
var slice1 []int
//切片的聲明2
var slice2 []int = make([]int,5)
var slice3 []int = make([]int,5,7)
numbers:= []int{1,2,3,4,5,6,7,8}
numbers:= []int{1,2,3,4,5,6,7,8}
//從下標1一直到下標4,可是不包括下標4
numbers1 :=numbers[1:4]
//從下標0一直到下標3,可是不包括下標3
numbers2 :=numbers[:3]
//從下標3一直到結束
numbers3 :=numbers[3:]
內置的len和cap函數分別返回slice的長度和容量
slice6 := make([]int,0)
fmt.Printf("len=%d,cap=%d,slice=%v\n",len(slice4),cap(slice4),slice4)
數組的拷貝是副本拷貝。對於副本的改變不會影響到原來的數組
可是,切片的拷貝很特殊,切片的拷貝只是對於運行時切片結構體的拷貝,切片的副本仍然指向了相同的數組。因此,對於副本的修改會影響到原來的切片。
下面用一個簡單的例子來講明
//數組是值類型
a := [4]int{1, 2, 3, 4}
//切片是引用類型
b := []int{100, 200, 300}
c := a
d := b
c[1] = 200
d[0] = 1
//output: c[1 200 3 4] a[1 2 3 4]
fmt.Println("a=", a, "c=", c)
//output: d[1 200 300] b[1 200 300]
fmt.Println("b=", b, "d=", d)
numbers := make([]int, 0, 20)
//append一個元素
numbers = append(numbers, 0)
//append多個元素
numbers = append(numbers, 1, 2, 3, 4, 5, 6, 7)
//append添加切片
s1 := []int{100, 200, 300, 400, 500, 600, 700}
numbers = append(numbers, s1...)
//now:[0 1 2 3 4 5 6 7 100 200 300 400 500 600 700]
// 刪除第一個元素
numbers = numbers[1:]
// 刪除最後一個元素
numbers = numbers[:len(numbers)-1]
// 刪除中間一個元素
a := int(len(numbers) / 2)
numbers = append(numbers[:a], numbers[a+1:]...)
// reverse reverses a slice of ints in place.
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
編譯時新建一個切片,切片內元素的類型是在編譯期間肯定的
func NewSlice(elem *Type) *Type {
if t := elem.Cache.slice; t != nil {
if t.Elem() != elem {
Fatalf("elem mismatch")
}
return t
}
t := New(TSLICE)
t.Extra = Slice{Elem: elem}
elem.Cache.slice = t
return t
}
切片的類型
// Slice contains Type fields specific to slice types.
type Slice struct {
Elem *Type // element type
}
當咱們使用字面量 []int{1, 2, 3} 建立新的切片時,會建立一個array數組([3]int{1,2,3}
)存儲於靜態區中。同時會建立一個變量。
核心邏輯位於slicelit函數
// go/src/cmd/compile/internal/gc/sinit.go
func slicelit(ctxt initContext, n *Node, var_ *Node, init *Nodes)
其抽象的過程以下:
var vstat [3]int
vstat[0] = 1
vstat[1] = 2
vstat[2] = 3
var vauto *[3]int = new([3]int)
*vauto = vstat
slice := vauto[:]
源碼中的註釋以下:
// 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[:]
例如make([]int,3,4)
使用make
關鍵字,在typecheck1類型檢查階段,節點Node的op操做變爲OMAKESLICE
,而且左節點存儲長度3, 右節點存儲容量4
func typecheck1(n *Node, top int) (res *Node) {
switch t.Etype {
case TSLICE:
if i >= len(args) {
yyerror("missing len argument to make(%v)", t)
n.Type = nil
return n
}
l = args[i]
i++
l = typecheck(l, ctxExpr)
var r *Node
if i < len(args) {
r = args[i]
i++
r = typecheck(r, ctxExpr)
}
if l.Type == nil || (r != nil && r.Type == nil) {
n.Type = nil
return n
}
if !checkmake(t, "len", l) || r != nil && !checkmake(t, "cap", r) {
n.Type = nil
return n
}
n.Left = l
n.Right = r
n.Op = OMAKESLICE
下面來分析一下編譯時內存的逃逸問題,若是make初始化了一個太大的切片,這個空間會逃逸到堆中,由運行時分配。若是一個空間比較小,會在棧中分配。
此臨界值值定義在/usr/local/go/src/cmd/compile/internal/gc
,能夠被flag smallframes更新,默認爲64KB。
因此make([]int64,1023)
與make([]int64,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)
核心邏輯位於go/src/cmd/compile/internal/gc/walk.go
,n.Esc
表明變量是否逃逸
func walkexpr(n *Node, init *Nodes) *Node{
case OMAKESLICE:
...
if n.Esc == EscNone {
// var arr [r]T
// n = arr[:l]
i := indexconst(r)
if i < 0 {
Fatalf("walkexpr: invalid index %v", r)
}
t = types.NewArray(t.Elem(), i) // [r]T
var_ := temp(t)
a := nod(OAS, var_, nil) // zero temp
a = typecheck(a, ctxStmt)
init.Append(a)
r := nod(OSLICE, var_, nil) // arr[:l]
r.SetSliceBounds(nil, l, nil)
r = conv(r, n.Type) // in case n.Type is named.
r = typecheck(r, ctxExpr)
r = walkexpr(r, init)
n = r
} else {
if t.Elem().NotInHeap() {
yyerror("%v is go:notinheap; heap allocation disallowed", t.Elem())
}
len, cap := l, r
fnname := "makeslice64"
argtype := types.Types[TINT64]
m := nod(OSLICEHEADER, nil, nil)
m.Type = t
fn := syslook(fnname)
m.Left = mkcall1(fn, types.Types[TUNSAFEPTR], init, typename(t.Elem()), conv(len, argtype), conv(cap, argtype))
m.Left.SetNonNil(true)
m.List.Set2(conv(len, types.Types[TINT]), conv(cap, types.Types[TINT]))
m = typecheck(m, ctxExpr)
m = walkexpr(m, init)
n = m
}
對上面代碼具體分析,若是沒有逃逸,分配在棧中。
抽象爲:
arr := [r]T
ss := arr[:l]
若是發生了逃逸,運行時調用makeslice64或makeslice分配在堆中,當切片的長度和容量小於int類型的最大值,會調用makeslice,反之調用makeslice64建立切片。
makeslice64最終也是調用了makeslice,比較簡單,最後調用mallocgc申請的內存大小爲類型大小 * 容量cap
// 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)
}
func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer {
len := int(len64)
if int64(len) != len64 {
panicmakeslicelen()
}
cap := int(cap64)
if int64(cap) != cap64 {
panicmakeslicecap()
}
return makeslice(et, len, cap)
}
Go 中切片append表示添加元素,但不是使用了append就須要擴容,以下代碼不須要擴容
a:= make([]int,3,4)
append(a,1)
當Go 中切片append當容量超過了現有容量,才須要進行擴容,例如:
a:= make([]int,3,3)
append(a,1)
核心邏輯位於go/src/runtime/slice.go growslice函數
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
...
}
上面的代碼顯示了擴容的核心邏輯,Go 中切片擴容的策略是這樣的:
首先判斷,若是新申請容量(cap)大於2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap)
不然判斷,若是舊切片的長度小於1024,則最終容量(newcap)就是舊容量(old.cap)的兩倍,即(newcap=doublecap)
不然判斷,若是舊切片長度大於等於1024,則最終容量(newcap)從舊容量(old.cap)開始循環增長原來的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最終容量(newcap)大於等於新申請的容量(cap),即(newcap >= cap)
若是最終容量(cap)計算值溢出,則最終容量(cap)就是新申請容量(cap)
接着根據切片類型的大小,肯定不一樣的內存分配大小。其主要是用做內存的對齊。所以,申請的內存可能會大於實際的et.size * newcap
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
最後核心是申請內存。要注意的是,新的切片不必定意味着新的地址。
根據切片類型et.ptrdata
是否爲指針,須要執行不一樣的邏輯。
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in old.array since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
}
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
當切片類型不是指針,分配內存後只須要將內存的後面的值清空,memmove(p, old.array, lenmem)
函數用於將old切片的值賦值給新的切片
整個過程的抽象抽象表示以下
old = make([]int,3,3)
new = append(old,1) => new = malloc(newcap * sizeof(int)) a[4] = 0
new[1] = old[1]
new[2] = old[2]
new[3] = old[3]
當切片類型爲指針,指針須要寫入當前協程緩衝區中,這個地方涉及到GC 回收機制中的寫屏障,後面介紹。
對於數組下標的截取,以下所示,能夠從多個維度證實,切片的截取生成了一個新的切片,可是底層數據源倒是使用的同一個。
old := make([]int64,3,3)
new := old[1:3]
fmt.Printf("%p %p",arr,slice)
輸出爲:
0xc000018140 0xc000018148
兩者的地址正好相差了8個字節,這不是偶然的,而是由於兩者指向了相同的數據源,恰好相差int64的大小。
另外咱們也能夠從生成的彙編的過程查看到到一些端倪
GOSSAFUNC=main GOOS=linux GOARCH=amd64 go tool compile main.go
在ssa的初始階段start
,old := make([]int64,3,3)
對應的是SliceMake <[]int> v10 v15 v15
, SliceMake操做須要傳遞數組的指針、長度、容量。
而 new := old[1:3]
對應SliceMake <[]int> v34 v28 v29
。傳遞的指針v34正好的原始的Ptr + 8個字節後的位置
下面列出一張圖比較形象的表示切片引用相同數據源的圖:
因爲切片的複製不會改變指向的底層數據源。可是咱們有些時候但願建一個新的數組,連底層數據源也是全新的。這個時候可使用copy
函數
切片進行值拷貝:copy
// 建立目標切片
numbers1 := make([]int, len(numbers), cap(numbers)*2)
// 將numbers的元素拷貝到numbers1中
count := copy(numbers1, numbers)
切片轉數組
slice := []byte("abcdefgh")
var arr [4]byte
copy(arr[:], slice[:4])
//或者直接以下,這涉及到一個特性,即只會拷貝min(len(arr),len(slice)
copy(arr[:], slice)
copy函數在編譯時會決定使用哪種方式,普通的方式會直接調用memmove
func copyany(n *Node, init *Nodes, runtimecall bool) *Node {
...
if runtimecall {
if n.Right.Type.IsString() {
fn := syslook("slicestringcopy")
fn = substArgTypes(fn, n.Left.Type, n.Right.Type)
return mkcall1(fn, n.Type, init, n.Left, n.Right)
}
fn := syslook("slicecopy")
fn = substArgTypes(fn, n.Left.Type, n.Right.Type)
return mkcall1(fn, n.Type, init, n.Left, n.Right, nodintconst(n.Left.Type.Elem().Width))
}
...
fn := syslook("memmove")
fn = substArgTypes(fn, nl.Type.Elem(), nl.Type.Elem())
nwid := temp(types.Types[TUINTPTR])
setwid := nod(OAS, nwid, conv(nlen, types.Types[TUINTPTR]))
ne.Nbody.Append(setwid)
nwid = nod(OMUL, nwid, nodintconst(nl.Type.Elem().Width))
call := mkcall1(fn, nil, init, nto, nfrm, nwid)
}
抽象表示爲:
init {
n := len(a)
if n > len(b) { n = len(b) }
if a.ptr != b.ptr { memmove(a.ptr, b.ptr, n*sizeof(elem(a))) }
}
除非是協程調用的方式go copy(numbers1, numbers)
或者(加入了race等檢測 && 不是在編譯go運行時代碼) 會轉而調用運行時slicestringcopy 或 slicecopy .
case OCOPY:
n = copyany(n, init, instrumenting && !compiling_runtime)
case OGO:
switch n.Left.Op {
case OCOPY:
n.Left = copyany(n.Left, &n.Ninit, true)
slicestringcopy 或 slicecopy 本質上仍然是調用了memmove
只是進行了額外的race衝突等判斷。
func slicecopy(to, fm slice, width uintptr) int {
...
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(slicecopy)
racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
}
if msanenabled {
msanwrite(to.array, uintptr(n*int(width)))
msanread(fm.array, uintptr(n*int(width)))
}
size := uintptr(n) * width
if size == 1 { // common case worth about 2x to do here
// TODO: is this still worth it with new memmove impl?
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
memmove(to.array, fm.array, size)
}
return n
}
切片是go語言中重要的數據結果,其和其餘語言不一樣的是,其維護了底層的內存,以及長度和容量
切片與數組的賦值拷貝有明顯區別,切片在賦值拷貝與下標截斷時引用了相同的底層數據
若是要徹底複製切片,使用copy
函數。其邏輯是新建一個新的內存,並拷貝過去。在極端狀況須要考慮其對性能的影響
切片字面量的初始化,數組存儲於靜態區。切片make
的初始化方式時,若是make初始化了一個大於64KB的切片,這個空間會逃逸到堆中,在運行時調用makeslice
建立。小於64KB的切片在棧中初始化
Go 中切片append當容量超過了現有容量,須要進行擴容,其策略是:
首先判斷,若是新申請容量(cap)大於2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap)
不然判斷,若是舊切片的長度小於1024,則最終容量(newcap)就是舊容量(old.cap)的兩倍,即(newcap=doublecap)
不然判斷,若是舊切片長度大於等於1024,則最終容量(newcap)從舊容量(old.cap)開始循環增長原來的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最終容量(newcap)大於等於新申請的容量(cap),即(newcap >= cap)
若是最終容量(cap)計算值溢出,則最終容量(cap)就是新申請容量(cap)
Go 中切片append後返回的切片地址並不必定是原來的、也不必定是新的內存地址,所以必須當心其可能遇到的陷阱。通常會使用a = append(a,T)
的方式保證安全。
項目連接
做者知乎
blog