指向切片的指针是使用引用还是副本?

huangapple go评论83阅读模式
英文:

Is pointer to slice using reference or copy?

问题

我发现我的代码与Go.Tour编译器(http://tour.golang.org/welcome/1)和我的本地编译器(Go版本1.4)之间存在不同的结果。

哪个是正确的?我也想知道指针在我的代码p1和p2之间是如何工作的?
因为地址似乎没有移动,但p1使用引用,而p2使用副本。

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

        在http://tour.golang.org/welcome/1上运行相同的代码会得到:
        c[0]=0, p2 = 0
        c[0]=2, p2 = *2*  << 为什么??
    */
}

更新:
我使用指针切片的原因是我正在尝试在Go中测试RUST在其网站上提出的向量push_pack问题。请参考http://doc.rust-lang.org/nightly/intro.html#ownership。

英文:

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 &quot;fmt&quot;
 
func main() {
	var a []int
	var b []int
	a = append(a, 0)
	b = append(b, 0)
	p := &amp;a[0]
	fmt.Printf(&quot;a[0] = %d pointer=%d, p = %d \n&quot;, a[0], &amp;a[0], *p)
	a[0] = 2
	fmt.Printf(&quot;a[0] = %d pointer=%d, p = %d \n&quot;, a[0], &amp;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 := &amp;c[0]
	fmt.Printf(&quot;c[0]=%d pointer=%d, p2 = %d\n&quot;, c[0], &amp;c[0], *p2)
	c = append(c, 1)
	c[0] = 2
	fmt.Printf(&quot;c[0]=%d pointer=%d, p2 = %d\n&quot;, c[0], &amp;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*  &lt;&lt; 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.

答案1

得分: 5

首先,关于切片的任何内容,我想推荐阅读《Go Slices: usage and internals》。简而言之,Go在使用append时处理切片的容量可能会有些奇怪。

给定的切片变量有三个组成部分:指向数据数组的底层指针、长度和容量。关于它们的区别已经有很多讨论,但这里重要的部分是长度(实际上)是底层内存缓冲区当前使用的部分,而容量是底层缓冲区的总大小。这是一个不太精确的定义,但在实践中足够好用。

谜团的下一部分是append内置函数。有时候很难理解append的功能,可能是Go中最大的陷阱之一:

  1. 如果底层缓冲区足够大(cap > len),只需通过要添加的元素数量增加len,并将数据放入新的空间。
  2. 如果底层缓冲区不够大,将分配一个具有更大容量的新缓冲区,将旧缓冲区复制到新缓冲区,并添加所有新元素。

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 = append(c, 1)
c[0] = 25
fmt.Printf("c[0]=%d pointer=%d, p2 = %d\n", c[0], &c[0], *p2)

playground

你会得到c[0]=25, p2=2。我们只添加了一个语句,突然间指针和切片值就不一样了!

这意味着容量发生了变化,或者换句话说,使用了一个新的缓冲区。实际上,在第一个append之后但第三个append之前打印cap(c)将得到2。这意味着当向容量为0的切片追加一个元素时,Go会初始化一个长度为1、容量为2的切片。因此,在第二个append之后不会分配新的缓冲区,因为有空间。这就是为什么第二个append之后p2c[0]是相同的,但第三个append之后不同的原因。

总的来说,虽然切片和对内存中特定位置的引用的一致性的确切规则是一致的,但实际上切片的增长行为非常棘手,通常最好不要依赖指向切片值的指针(或两个切片变量具有相同的底层缓冲区),除非你打算要么从不使用append,要么使用make预先分配足够大的缓冲区,以便使用append永远不会重新分配。

[脚注] 不完全正确,我想发出一个巨大的警告append的结果容量在不同编译器甚至不同编译目标之间是依赖于实现的,请不要依赖于append的结果容量在不同编译器或甚至不同编译目标之间是一致的

英文:

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 := &amp;c[0]
fmt.Printf(&quot;c[0]=%d pointer=%d, p2 = %d\n&quot;, c[0], &amp;c[0], *p2)
c = append(c, 1)
c[0] = 2
fmt.Printf(&quot;c[0]=%d pointer=%d, p2 = %d\n&quot;, c[0], &amp;c[0], *p2)
c = append(c, 1)
c[0] = 25
fmt.Printf(&quot;c[0]=%d pointer=%d, p2 = %d\n&quot;, c[0], &amp;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

答案2

得分: 1

要在play.golang.org和本地go 1.4环境中获得相同的结果,您需要添加以下代码:

c[0] = 2
p2 = &c[0] // 在此重新定义p2

这将在本地运行时产生以下结果:

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

而不是:

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

正如在"这个示例"中所示。

可能是因为在"Go Playground内部"中提到的原因,由于"Playground程序在使用的CPU时间和内存量上有限制",在playground和完整的Go程序之间,为切片分配的内存管理方式可能不同。

如果您删除c = append(c, 1),您将获得预期的结果。

在本地程序中,扩展切片会创建一个新的切片:它的默认容量为1,添加一个新元素会创建一个具有新容量的新切片(参见"切片内部")。
但在playground中,切片的容量可能默认较大。

英文:

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 = &amp;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.

huangapple
  • 本文由 发表于 2014年12月27日 16:38:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/27665669.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定