我们先来看下slice的结构体
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
我们先看一下创建slice的方法,我们用go1.11和go1.18做比较,go1.11返回的是结构体(slice),go1.18返回的是slice里面的array指针,然后在上层调用方再创建reflect.SliceHeader.这一改变是在cmd/compile: move slice construction to callers of makeslice
go1.11
// runtime/slice.go
func makeslice(et *_type, len, cap int) slice {
// NOTE: The len > maxElements check here is not strictly necessary,
// but it produces 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 issue 4085.
maxElements := maxSliceCap(et.size)
if len < 0 || uintptr(len) > maxElements {
panicmakeslicelen()
}
if cap < len || uintptr(cap) > maxElements {
panicmakeslicecap()
}
p := mallocgc(et.size*uintptr(cap), et, true)
return slice{p, len, cap}
}
// cmd/compile/internal/gc/walk.go
// 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.
if t.Elem().NotInHeap() {
yyerror("%v is go:notinheap; heap allocation disallowed", t.Elem())
}
len, cap := l, r
fnname := "makeslice64"
argtype := 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(TIDEAL) || maxintval[len.Type.Etype].Cmp(maxintval[TUINT]) <= 0) &&
(cap.Type.IsKind(TIDEAL) || maxintval[cap.Type.Etype].Cmp(maxintval[TUINT]) <= 0) {
fnname = "makeslice"
argtype = types.Types[TINT]
}
fn := syslook(fnname)
fn = substArgTypes(fn, t.Elem()) // any-1
n = mkcall1(fn, t, init, typename(t.Elem()), conv(len, argtype), conv(cap, argtype))
go1.18
// 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)
}
// cmd/compile/internal/walk/builtin.go
// walkMakeSlice walks an OMAKESLICE node.
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
}
t := n.Type()
if t.Elem().NotInHeap() {
//....
}
// 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])
// 对比go1.11生成了数组指针后再初始化了len和cap
// ir.NewSliceHeaderExpr主要就是生成了SliceHeader,参考下面的tcSliceHeader
sh := ir.NewSliceHeaderExpr(base.Pos, t, ptr, len, cap)
return walkExpr(typecheck.Expr(sh), init)
}
// cmd/compile/internal/typecheck/expr.go
// tcSliceHeader typechecks an OSLICEHEADER node.
func tcSliceHeader(n *ir.SliceHeaderExpr) ir.Node {
// Errors here are Fatalf instead of Errorf because only the compiler
// can construct an OSLICEHEADER node.
// Components used in OSLICEHEADER that are supplied by parsed source code
// have already been typechecked in e.g. OMAKESLICE earlier.
t := n.Type()
if t == nil {
base.Fatalf("no type specified for OSLICEHEADER")
}
if !t.IsSlice() {
base.Fatalf("invalid type %v for OSLICEHEADER", n.Type())
}
if n.Ptr == nil || n.Ptr.Type() == nil || !n.Ptr.Type().IsUnsafePtr() {
base.Fatalf("need unsafe.Pointer for OSLICEHEADER")
}
n.Ptr = Expr(n.Ptr)
n.Len = DefaultLit(Expr(n.Len), types.Types[types.TINT])
n.Cap = DefaultLit(Expr(n.Cap), types.Types[types.TINT])
...
return n
}
// SliceHeader is the runtime representation of a slice.
// SliceHeader就是slice的运行时表示
// ...
type SliceHeader struct {
Data uintptr
Len int
Cap int
所以我们可以简单的理解成slice的创建返回的是slice结构体,而不是指针.所以在函数参数传递拷贝的时候拷贝的是slice
结构体,而不是*slice
,但是map不一样,map创建的时候是*hmap
,创建的是一个指针.
// runtime/map.go
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
...
return h
}
但是我们很好奇,这里有两个问题:
1.并且%p能输出地址
2.为什么复制slice的时候会影响旧的值
问题1:
其实fmt.Printf()的%p并不只是指针地址,当为slice的时候是输出的是底层数组第0个元素的地址
//go1.18 fmt/print.go
func (p *pp) fmtPointer(value reflect.Value, verb rune) {
var u uintptr
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer:
u = value.Pointer()
default:
p.badVerb(verb)
return
}
...
}
// reflect/value.go
func (v Value) Pointer() uintptr {
k := v.kind()
switch k {
...
case Slice:
//这里的slice返回的指针其实是`*SliceHeader.Data`,Data就是前面slice.array指针
return (*SliceHeader)(v.ptr).Data
}
panic(&ValueError{"reflect.Value.Pointer", v.kind()})
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
问题2:
这是因为复制的时候并不是深拷贝,相当于slice.array还没有变,但是如果我们append足够大就会申请新的slice.array,如下
func main() {
var oldSlice = []int64{1, 2, 3, 4, 5} // len:5,capacity:5
var newSlice = oldSlice[1:3] // len:2,capacity:4 (已经使用了两个位置,所以还空两位置可以append)
fmt.Printf("%p\n", oldSlice) //0xc420098000
fmt.Printf("%p\n", newSlice) //0xc420098008 可以看到newSlice的地址指向的是array[1]的地址,即他们底层使用的还是一个数组
fmt.Printf("%v\n", oldSlice) //[1 2 3 4 5]
fmt.Printf("%v\n", newSlice) //[2 3]
newSlice[1] = 9 //更改后oldSlice、newSlice都改变了
fmt.Printf("%v\n", oldSlice) // [1 2 9 4 5]
fmt.Printf("%v\n", newSlice) // [2 9]
newSlice = append(newSlice, 11, 12) //append 操作之后,oldSlice的len和capacity不变,newSlice的len变为4,capacity:4。因为这是对newSlice的操作
fmt.Printf("%v\n", oldSlice) //[1 2 9 11 12] //注意对newSlice做append操作之后,oldSlice[3],oldSlice[4]的值也发生了改变
fmt.Printf("%v\n", newSlice) //[2 9 11 12]
newSlice = append(newSlice, 13, 14) // 因为newSlice的len已经等于capacity,所以再次append就会超过capacity值,
// 此时,append函数内部会创建一个新的底层数组(是一个扩容过的数组),并将oldSlice指向的底层数组拷贝过去,然后在追加新的值。
fmt.Printf("%p\n", oldSlice) //0xc420098000
fmt.Printf("%p\n", newSlice) //0xc4200a0000
fmt.Printf("%v\n", oldSlice) //[1 2 9 11 12]
fmt.Printf("%v\n", newSlice) //[2 9 11 12 13 14] 它俩已经不再是指向同一个底层数组了
}
参考: