Evan Lin
Evan Lin

Reputation: 1322

Is pointer to slice using reference or copy?

I found there is different result from my code as follow which is a pointer refer to a slide between Go.Tour compiler (http://tour.golang.org/welcome/1) and my local compiler (Go Version 1.4)

Which one is correct? And I am also wondering how pointer work between my code p1, p2? Because the address seems not moving but p1 using reference but p2 using copy.

package main

import "fmt"

func main() {
    var a []int
    var b []int
    a = append(a, 0)
    b = append(b, 0)
    p := &a[0]
    fmt.Printf("a[0] = %d pointer=%d, p = %d \n", a[0], &a[0], *p)
    a[0] = 2
    fmt.Printf("a[0] = %d pointer=%d, p = %d \n", a[0], &a[0], *p)
    /*
        a[0] = 0, p = 0
        a[0] = 2, p = 2
    */
    var c []int
    var d []int
    c = append(c, 0)
    d = append(d, 0)
    p2 := &c[0]
    fmt.Printf("c[0]=%d pointer=%d, p2 = %d\n", c[0], &c[0], *p2)
    c = append(c, 1)
    c[0] = 2
    fmt.Printf("c[0]=%d pointer=%d, p2 = %d\n", c[0], &c[0], *p2)
    /* 
        c[0]=0, p2 = 0
        c[0]=2, p2 = 0

      copy the same code run in http://tour.golang.org/welcome/1 will get.
        c[0]=0, p2 = 0
        c[0]=2, p2 = *2*  << why??

    */
}

Update: The reason why I use pointer to slice is that I am trying to test vector push_pack issue which RUST present on their web side in Go. Refer to http://doc.rust-lang.org/nightly/intro.html#ownership.

Upvotes: 1

Views: 453

Answers (2)

Linear
Linear

Reputation: 22196

Firstly, as for anything with slices, I'd like to recommend reading through Go Slices: usage and internals. The short story is Go's handling of a slice's capacity with append can be wonky.

A given slice variable has three components: an underlying pointer to a data array, a length, and a capacity. Plenty of words have been spilled about the difference, but the important part here is that the length is (effectively) the currently used part of the underlying memory buffer, and the capacity is the overall size of the underlying buffer. This is an imprecise definition, but it works well enough in practice.

The next part of the mystery is the append builtin. The functionality of append is actually somewhat hard to reason about sometimes, and probably one of the biggest gotchas in Go:

  1. If the underlying buffer is large enough (cap > len), simply increase len by the number of elements to be added and put the data in the new space.
  2. If the underlying buffer is NOT large enough, allocate a new buffer with some larger capacity, copy the old buffer into the new buffer, and add all the new elements.

The biggest sticking point with 2 is that given two arbitrary operations on the same slice after an append, it's difficult to know if the old or a new memory buffer is being effected a priori. Indeed, let's try this:

var c []int
var d []int
c = append(c, 0)
d = append(d, 0)
p2 := &c[0]
fmt.Printf("c[0]=%d pointer=%d, p2 = %d\n", c[0], &c[0], *p2)
c = append(c, 1)
c[0] = 2
fmt.Printf("c[0]=%d pointer=%d, p2 = %d\n", c[0], &c[0], *p2)
c = append(c, 1)
c[0] = 25
fmt.Printf("c[0]=%d pointer=%d, p2 = %d\n", c[0], &c[0], *p2)

playground

You'll get c[0]=25, p2=2. We've only added one more statement, and suddenly the pointer and slice values are diverging!

This means that the cap changed, or rather, a new buffer was used. Indeed, printing cap(c) after the first append, but before the third, will yield 2. This means that when appending a single element to a slice of capacity 0, Go initializes[footnote] a slice of length 1 and capacity 2. So no new buffer is allocated after the second append, because there's space. That's why p2 and c[0] are the same after the second append but not the third.

In general, while the exact rules for when a slice and a reference to a specific location in memory are consistent, in practice the slice-growing behavior is so finnicky that it's usually best to never rely on pointers to slice values (or two slice variables having the same underlying buffer) unless you plan on either never using append, or pre-allocating the buffer with make to such a size that using append will never reallocate.

[footnote] Not entirely true, I'd like to give a huge warning the exact capacity after append is implementation dependent. PLEASE DO NOT RELY ON APPEND'S RESULTING CAPACITY BEING CONSISTENT BETWEEN COMPILERS OR EVEN DIFFERENT COMPILER TARGETS

Upvotes: 5

VonC
VonC

Reputation: 1324377

To get the same result in both environment (play.golang.org and local go 1.4), you would need to add:

c[0] = 2
p2 = &c[0] // REDEFINE p2 there

(as in this example)

That would give, when run locally:

c[0]=0 pointer=826814759400, p2 = 0
c[0]=2 pointer=826814759440, p2 = 2

instead of:

c[0]=0 pointer=826814759400, p2 = 0
c[0]=2 pointer=826814759440, p2 = 0

It is possible, as mentioned in "Inside the Go Playground", that since "Playground programs are limited in the amount of CPU time and memory they can use", the memory allocate for a slice is managed differently between a playground and a full Go program.

If you were to remove c = append(c, 1), you would get the expected result.

With a local program, expending the slice results in a new slice: its default capacity being 1, adding a new element creates a new slice with a new capacity (see "slice internals").
Not with playground where the slice capacity might be larger by default.

Upvotes: 1

Related Questions