-2

I don't understand the last step result, I thought "copy" uses values. I'd appreciate it if you explain.

type Vertex struct {
    X []int
    Y []int
}

func main() {
    var v Vertex
    x := []int{1, 2}
    y := []int{3, 4}
    v.X = x
    v.Y = y
    fmt.Println(v)

    x[0] = 5
    fmt.Println(v)

    copy(v.X, x)
    copy(v.Y, y)
    fmt.Println(v)

    x[0] = 6
    fmt.Println(v)
}

results:

{[1 2] [3 4]}
{[5 2] [3 4]}
{[5 2] [3 4]}
{[6 2] [3 4]}
AMM
  • 37
  • 1
  • 6
  • 1
    Does this answer your question? [Are golang slices passed by value?](https://stackoverflow.com/questions/39993688/are-golang-slices-passed-by-value) – Brits Oct 27 '20 at 00:42
  • Thanks but not really, that explains the second print result. – AMM Oct 27 '20 at 01:07
  • 1
    OK - please edit your question an add the result you are expecting (and why). – Brits Oct 27 '20 at 01:22
  • Sure. Should it be {[5 2] [3 4]} in the last print? since I am using the "copy" function. – AMM Oct 27 '20 at 01:50
  • See blog.golang.org/slices-intro and blog.golang.org/slices (and of course the Tour of Go). – Volker Oct 27 '20 at 06:33

1 Answers1

5

It's not entirely clear to me where your misunderstanding is: whether it's with slices themselves, or with copy. So this is a bit long.

Depending on how you want to look at it (note: only one of two these ways is really correct), a slice consists of either two (correct) or three (wrong, but may help you here) parts. Let's start with the wrong one, noting that it's wrong, then fix it:

  • there's your variable, such as v.X, which can be nil;
  • then, if your variable isn't nil, there is a slice header somewhere in memory, and your variable holds that header;
  • and, if the header itself is good, it holds a pointer to the underlying array that exists somewhere in memory.

What's wrong with this is simple enough: your variable always holds some header. It's just that the header your variable holds can compare equal to nil. If it does, the language doesn't let you proceed to use the rest of it. So the correct version is:

  • You have a variable of type [] T for some type T. This variable either is nil, or isn't nil. You can test for this using, e.g., v.X == nil.

  • If the variable isn't nil, the variable itself holds the slice header. The array that holds the values in that slice is somewhere else.

Let's go back to your code now:

var v Vertex

This creates the struct-valued variable v. It has two slice variables inside it, v.X and v.Y; both are nil.

x := []int{1, 2}

This is shorthand for:

var x []int
x = []int{1, 2}

The first line creates the variable that holds the header (which is initially nil, except that the compiler probably skips over the set-to-nil-step since it doesn't need to do that). The second line makes a two-element array of int somewhere, fills in that array with 1 and 2, and then sets up the slice header in the variable x to hold the slice value:

  • a pointer to the array that contains {1, 2};
  • the length, 2; and
  • the capacity, 2.

So v.X is now a valid slice header, with the compiler having built an underlying array (somewhere).

y := []int{3, 4}

We do the same thing now with variable y. It ends up holding a slice header, with the three values:

  • a pointer to the array that contains {3, 4};
  • the length 2; and
  • the capacity 2.

Now we have:

v.X = x
v.Y = y

These two lines copy the slice headers from variable x to v.X, and from variable y to v.Y. The two sets of slice headers now match. There are still two underlying arrays as well: one holds {1, 2} and the other holds {3, 4}.

fmt.Println(v)

The Println function has a lot of complicated code built into it: it checks v, discovers that it is a struct type, checks the elements of v to find v.X and v.Y, checks them to discover that they are []int types, checks their headers for nil-ness, discovers that they are not-nil, and prints out what you see.

x[0] = 5

This uses the value in x—the slice header—to find the array that holds {1, 2}, and then replaces the 1 with 5. So now, the underlying array holds {5, 2}. The slice header in x is not changed, and of course the slice header in v.X is also not changed. Both of these slice headers still list the length and capacity as 2, and both still point to that single underlying array.

fmt.Println(v)

This repeats the rather complicated work, and this time prints the [5 2] value that you saw.

copy(v.X, x)

The copy function needs, as arguments, two slice headers.1 The first, the destination dst, needs to be a non-nil slice to have anything useful happen.2 The second, the source src, can be nil or non-nil. Both must have the same underlying type. What copy does then is:

  • find the smaller of len(dst) and len(src);
  • copy that many elements from the underlying array of src to the underlying array of dst; and
  • return that number.

So we find len(v.X), which is 2, and len(x), which is also 2, and therefore copy will copy 2 ints from the array underlying x to the array underlying v.X.

Those two arrays are the same array. So this copies the elements back into themselves, changing nothing. The copy function now returns 2, without changing the length or capacity of either slice.

In other words, nothing has happened to either v.X or x, nor has anything changed in the underlying array.

You now repeat this work for v.Y, y, which again changes nothing. Both slice headers share the same underlying array, so this copy does copy two elements, but changes nothing.

The rest of the code proceeds in the obvious way.

If you want to make a new copy of some slice, you should:

  • find the length of the original slice;
  • create a new slice of that length; and
  • copy into the new slice.

In this case, that would be, e.g.:

tmp := make([]int, len(x))
copy(tmp, x)
v.X = tmp

The tmp variable isn't necessary: you could instead write:

v.X = make([]int, len(x))
copy(v.X, x)

but you can define a small allocate-and-copy function and use it:

func copyIntSlice([]int a) []int {
    tmp := make([]int, len(a))
    copy(tmp, a)
    return tmp
}

and then use:

v.X = copyIntSlice(x)

for instance, which makes it a single line to do this for v.Y as well:

v.Y = copyIntSlice(y)

It's the make step that actually creates the new array. Given some initial underlying array, you can fiddle with slice headers all you like, but at some point, you probably need to create a new array, otherwise all your slices are working with the old (shared) underlying array.


1The second argument can be a string, instead of a slice. A string is like a slice of byte, but lacks a separate capacity field. Fortunately, copy doesn't use the capacity of the second argument; that's what makes this all work out in the end.

2If the first argument to copy is nil, copy does nothing. It's not wrong to call it, it just doesn't do anything. If that was the right thing to do—if we should do nothing for this case—then that's fine.

torek
  • 448,244
  • 59
  • 642
  • 775