关于go:go-slice切片到底是指针吗为什么p输出的切片是地址

46次阅读

共计 6518 个字符,预计需要花费 17 分钟才能阅读完成。

咱们先来看下 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]  它俩曾经不再是指向同一个底层数组了
}

参考:

  • https://draveness.me/golang/d…
  • https://pkg.go.dev/fmt

正文完
 0