问题
What's the behavior of creating slice from slice? When you have slice defined like this:
s := []int{2, 3, 5, 7, 11, 13}
And you want to modify your slice like this:
s = s[:3]
// s = [2 3 5]
s = s[:cap(s)]
// s = [2 3 5 7 11 13]
It actually works to "expand right" your slice. Which doesn't work is:
s = s[2:]
// s = [5 7 11 13]
s = [:cap(s)]
// s = [5 7 11 13]
So you can't "retain" the first two elements in this case, when you've created new slice. Even if the underlying array didn't change, you can't change your pointer to the beginning of that array, correct? Why is that?
回答1:
As @JimB pointed out in the comments, this is due to how slices function in Go.
Fundamentally, a slice header is a struct that contains 3 elements: a pointer to the first element, the length of the current data, and the total capacity of the underlying array as measured from the "first element" pointer (even if it's only part of the actual allocated underlying array). It has no other information on the underlying array.
When you cut off data from the end via the s[:x]
, you're creating a new slice header that has a different length field, but the same capacity and first-element pointer. Thus you can expand the slice again up to that maximum capacity, because the runtime knows that that memory is allocated and safe for access.
When you use the s[x:]
, you're creating a new slice header with a different initial pointer to the new first element and a reduced capacity, in addition to a reduced length. You can't "undo" that to point back to the original first element, because the slice struct has no information about the original slice or the underlying array except for that pointer and the capacity field.
For example, if you make the slice s := []int{2, 3, 5, 7, 11, 13}
, your slice header might contain:
ptr: 0x00000000
len: 6
cap: 6
If you then call s = s[:3]
, your slice header would contain:
ptr: 0x00000000
len: 3
cap: 6
Notice that only the len has changed. If you then call s = s[:cap(s)]
, the runtime sees that the capacity can support that slice operation, which means that the underlying array has at least that many "slots" allocated, and expands the slice without problem. Since the original data is still in those "slots", you get back out what is functionally equivalent to the original array:
ptr: 0x00000000
len: 6
cap: 6
However, if you call s = s[2:]
, you get the following:
ptr: 0x00000010
len: 4
cap: 4
Note that in addition to the length changing, the pointer has advanced by 16 bytes (the size of two int
s on a 64-bit system), and the capacity field has also been reduced, because the underlying array only has 4 "slots" allocated relative to that pointer.
Now, the runtime has no additional information about the underlying pointer except that header! From that header, the runtime has no way to know what size the original underlying array was or where it's original start point was, or even if the current slice is not starting at that original start point. As a result, trying to reset the slice back to that original start point isn't legal, because the runtime can't verify that that memory is allocated and safe. This is an intentional design decision, as Go is specifically designed to not allow the type of ill-advised and highly-buggy arbitrary pointer arithmetic so common in C++ programs.
In addition, your call to s = s[:cap(s)]
is using the new (reduced) capacity stored in the slice header. While your first call of that nature was equivalent to s = s[:6]
, because that's what the original slice's capacity was, the call is now equivalent to s = s[:4]
, because moving the slice's pointer also reduced the capacity of the backing array (because that capacity is measured from the element pointed to by that pointer, not the first element of the actual backing array).
If the Go runtime instead kept track of slices as a pointer (to the absolute first element in the backing array), the length, the capacity, and the offset, then what you want would be possible. It does not, however, do this. Partly that's because that would represent a 33% increase in the size of slice headers (which are specifically intended to be as lightweight as possible), and partly because, as @JimB pointed out, the developers of the language decided that that additional complexity was unnecessary, as you can easily keep a handle on the original slice header yourself if you think it necessary.
来源:https://stackoverflow.com/questions/47817849/behaviour-of-pointer-in-slice