使用指针定义golang结构体函数与否的区别

66

有人能解释一下为什么在这种情况下向数组附加元素是有效的吗:

func (s *Sample) Append(name string) {
    d := &Stuff{
        name: name,
    }
    s.data = append(s.data, d)
}

完整代码在此处

但是,如果你这样做就不行:

func (s Sample) Append(name string) {
    d := &Stuff{
        name: name,
    }
    s.data = append(s.data, d)
}

您是否有任何理由想要使用第二个示例。

4个回答

100

常见问题中所述

我应该在值上定义方法还是指针上定义方法?

func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct)  valueMethod()   { } // method on value

首先,最重要的是,这个方法是否需要修改接收者?如果需要,接收者必须是一个指针。(切片和映射作为引用,所以它们的情况有点更加微妙,但是例如要在方法中改变切片的长度,接收者仍然必须是一个指针。)
在上面的例子中,如果pointerMethod修改了s的字段,调用者将看到这些更改,但是valueMethod使用调用者参数的副本进行调用(这就是传递值的定义),因此它所做的更改对调用者来说是不可见的。
在您的情况下,func (s Sample) Append(name string)修改了一个副本。 laher评论中提醒我们,使用值而不是指针也意味着得到一个副本,并且尊重对象的不可变性。

如果您要返回从“不可变”私有属性派生的值,可以使用非指针valueMethod

参见“为什么在Go中接收器按值传递?”:

如果您有一个小的不可变对象,则可以很有用。调用者可以确定此方法不会修改其接收器。
如果接收器是指针,则在未阅读代码的情况下无法确定这一点。


1
很棒的信息,但这并没有提出你想使用非指针值方法的情况。我可以想到一个例子——当你返回一个“不可变”的私有属性派生的值时,通过非指针方法显示它是不可变的会更清晰。 - laher
2
@laher 很好的观点。为了增加可见性,我已将您的评论包含在答案中,并添加了一些链接/参考资料。 - VonC
2
不错,现在看起来很好。我一直在想这个问题已经有一段时间了,如果有其他用例,我会很感兴趣知道。我猜你可以说它避免了需要显式的“防御性拷贝”,但那只是不可变性点的一部分。谢谢@VonC。 - laher

9

Go切片是一个棘手的问题。在内部,一个切片类型的变量(比如[]int)看起来像这样:

struct {
    data *int // pointer to the data area
    len  int
    cap  int
}

当您将切片传递给函数时,该结构体被按值传递,而基础数据区域(即 data 指向的内容)不会被复制。内置的 append()函数修改了 data 区域(或生成一个新的),并返回一个更新后的新切片,其中包含更新后的 len data cap 值。如果您想要覆盖不属于基础数据区域的任何内容,则需要传递切片的指针或返回经过修改的切片。

8

大多数答案都准确地描述了发生了什么,但我想深入了解为什么/如何发生这种情况。我从一些简短的代码片段开始:

  1. pointer method

     package main
    
     import "fmt"
    
     type Bar struct{}
    
     func (b *Bar) Print() {
         fmt.Println("debosmit ray")
     }
    
     func main() {
         b := Bar{}
         b.Print()
     }
    
  2. value method

     package main
    
     import "fmt"
    
     type Bar struct{}
    
     func (b Bar) Print() {
         fmt.Println("debosmit ray")
     }
    
     func main() {
         b := Bar{}
         b.Print()
     }
    

然后,我想仅查看文件的汇编代码(使用go tool compile -S filename.go > filename.S为每个文件生成),两个输出都可以在这里找到(应该永久可用)。

让我们来看一下diff pointer.S value.S的输出(pointer -> 具有指针方法,value -> 具有值方法)。

14,15c14,15
< "".(*Bar).Print STEXT size=138 args=0x8 locals=0x58
<   0x0000 00000 (bar.go:7) TEXT    "".(*Bar).Print(SB), ABIInternal, $88-8
---
> "".Bar.Print STEXT size=138 args=0x0 locals=0x58
>   0x0000 00000 (bar.go:7) TEXT    "".Bar.Print(SB), ABIInternal, $88-0
24c24
<   0x001d 00029 (bar.go:7) FUNCDATA    $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
---
>   0x001d 00029 (bar.go:7) FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
26c26
<   0x001d 00029 (bar.go:7) FUNCDATA    $3, "".(*Bar).Print.stkobj(SB)
---
>   0x001d 00029 (bar.go:7) FUNCDATA    $3, "".Bar.Print.stkobj(SB)
126a127,200
> "".(*Bar).Print STEXT dupok size=187 args=0x8 locals=0x58
>   0x0000 00000 (<autogenerated>:1)    TEXT    "".(*Bar).Print(SB), DUPOK|WRAPPER|ABIInternal, $88-8
>   0x0000 00000 (<autogenerated>:1)    MOVQ    (TLS), CX
>   0x0009 00009 (<autogenerated>:1)    CMPQ    SP, 16(CX)
>   0x000d 00013 (<autogenerated>:1)    PCDATA  $0, $-2
>   0x000d 00013 (<autogenerated>:1)    JLS 154
>   0x0013 00019 (<autogenerated>:1)    PCDATA  $0, $-1
>   0x0013 00019 (<autogenerated>:1)    SUBQ    $88, SP
>   0x0017 00023 (<autogenerated>:1)    MOVQ    BP, 80(SP)
>   0x001c 00028 (<autogenerated>:1)    LEAQ    80(SP), BP
>   0x0021 00033 (<autogenerated>:1)    MOVQ    32(CX), BX
>   0x0025 00037 (<autogenerated>:1)    TESTQ   BX, BX
>   0x0028 00040 (<autogenerated>:1)    JNE 165
>   0x002a 00042 (<autogenerated>:1)    NOP
>   0x002a 00042 (<autogenerated>:1)    FUNCDATA    $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
>   0x002a 00042 (<autogenerated>:1)    FUNCDATA    $1, gclocals·2589ca35330fc0fce83503f4569854a0(SB)
>   0x002a 00042 (<autogenerated>:1)    FUNCDATA    $3, "".(*Bar).Print.stkobj(SB)
>   0x002a 00042 (<autogenerated>:1)    CMPQ    ""..this+96(SP), $0
>   0x0030 00048 (<autogenerated>:1)    JEQ 148
>   0x0032 00050 (<unknown line number>)    NOP
>   0x0032 00050 (bar.go:8) XORPS   X0, X0
>   0x0035 00053 (bar.go:8) MOVUPS  X0, ""..autotmp_13+64(SP)
>   0x003a 00058 (bar.go:8) LEAQ    type.string(SB), AX
>   0x0041 00065 (bar.go:8) MOVQ    AX, ""..autotmp_13+64(SP)
>   0x0046 00070 (bar.go:8) LEAQ    ""..stmp_2(SB), AX
>   0x004d 00077 (bar.go:8) MOVQ    AX, ""..autotmp_13+72(SP)
>   0x0052 00082 (<unknown line number>)    NOP
>   0x0052 00082 ($GOROOT/src/fmt/print.go:274) MOVQ    os.Stdout(SB), AX
>   0x0059 00089 ($GOROOT/src/fmt/print.go:274) LEAQ    go.itab.*os.File,io.Writer(SB), CX
>   0x0060 00096 ($GOROOT/src/fmt/print.go:274) MOVQ    CX, (SP)
>   0x0064 00100 ($GOROOT/src/fmt/print.go:274) MOVQ    AX, 8(SP)
>   0x0069 00105 ($GOROOT/src/fmt/print.go:274) LEAQ    ""..autotmp_13+64(SP), AX
>   0x006e 00110 ($GOROOT/src/fmt/print.go:274) MOVQ    AX, 16(SP)
>   0x0073 00115 ($GOROOT/src/fmt/print.go:274) MOVQ    $1, 24(SP)
>   0x007c 00124 ($GOROOT/src/fmt/print.go:274) MOVQ    $1, 32(SP)
>   0x0085 00133 ($GOROOT/src/fmt/print.go:274) PCDATA  $1, $1
>   0x0085 00133 ($GOROOT/src/fmt/print.go:274) CALL    fmt.Fprintln(SB)
>   0x008a 00138 (bar.go:8) MOVQ    80(SP), BP
>   0x008f 00143 (bar.go:8) ADDQ    $88, SP
>   0x0093 00147 (bar.go:8) RET
>   0x0094 00148 (<autogenerated>:1)    CALL    runtime.panicwrap(SB)
>   0x0099 00153 (<autogenerated>:1)    XCHGL   AX, AX
>   0x009a 00154 (<autogenerated>:1)    NOP
>   0x009a 00154 (<autogenerated>:1)    PCDATA  $1, $-1
>   0x009a 00154 (<autogenerated>:1)    PCDATA  $0, $-2
>   0x009a 00154 (<autogenerated>:1)    CALL    runtime.morestack_noctxt(SB)
>   0x009f 00159 (<autogenerated>:1)    PCDATA  $0, $-1
>   0x009f 00159 (<autogenerated>:1)    NOP
>   0x00a0 00160 (<autogenerated>:1)    JMP 0
>   0x00a5 00165 (<autogenerated>:1)    LEAQ    96(SP), DI
>   0x00aa 00170 (<autogenerated>:1)    CMPQ    (BX), DI
>   0x00ad 00173 (<autogenerated>:1)    JNE 42
>   0x00b3 00179 (<autogenerated>:1)    MOVQ    SP, (BX)
>   0x00b6 00182 (<autogenerated>:1)    JMP 42
>   0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 0f 86 87  eH..%....H;a....
>   0x0010 00 00 00 48 83 ec 58 48 89 6c 24 50 48 8d 6c 24  ...H..XH.l$PH.l$
>   0x0020 50 48 8b 59 20 48 85 db 75 7b 48 83 7c 24 60 00  PH.Y H..u{H.|$`.
>   0x0030 74 62 0f 57 c0 0f 11 44 24 40 48 8d 05 00 00 00  tb.W...D$@H.....
>   0x0040 00 48 89 44 24 40 48 8d 05 00 00 00 00 48 89 44  .H.D$@H......H.D
>   0x0050 24 48 48 8b 05 00 00 00 00 48 8d 0d 00 00 00 00  $HH......H......
>   0x0060 48 89 0c 24 48 89 44 24 08 48 8d 44 24 40 48 89  H..$H.D$.H.D$@H.
>   0x0070 44 24 10 48 c7 44 24 18 01 00 00 00 48 c7 44 24  D$.H.D$.....H.D$
>   0x0080 20 01 00 00 00 e8 00 00 00 00 48 8b 6c 24 50 48   .........H.l$PH
>   0x0090 83 c4 58 c3 e8 00 00 00 00 90 e8 00 00 00 00 90  ..X.............
>   0x00a0 e9 5b ff ff ff 48 8d 7c 24 60 48 39 3b 0f 85 77  .[...H.|$`H9;..w
>   0x00b0 ff ff ff 48 89 23 e9 6f ff ff ff                 ...H.#.o...
>   rel 5+4 t=17 TLS+0
>   rel 61+4 t=16 type.string+0
>   rel 73+4 t=16 ""..stmp_2+0
>   rel 85+4 t=16 os.Stdout+0
>   rel 92+4 t=16 go.itab.*os.File,io.Writer+0
>   rel 134+4 t=8 fmt.Fprintln+0
>   rel 149+4 t=8 runtime.panicwrap+0
>   rel 155+4 t=8 runtime.morestack_noctxt+0
139,143c213,217
< go.info."".(*Bar).Print$abstract SDWARFINFO dupok size=26
<   0x0000 04 2e 28 2a 42 61 72 29 2e 50 72 69 6e 74 00 01  ..(*Bar).Print..
<   0x0010 01 11 62 00 00 00 00 00 00 00                    ..b.......
<   rel 0+0 t=24 type.*"".Bar+0
<   rel 21+4 t=29 go.info.*"".Bar+0
---
> go.info."".Bar.Print$abstract SDWARFINFO dupok size=23
>   0x0000 04 2e 42 61 72 2e 50 72 69 6e 74 00 01 01 11 62  ..Bar.Print....b
>   0x0010 00 00 00 00 00 00 00                             .......
>   rel 0+0 t=24 type."".Bar+0
>   rel 18+4 t=29 go.info."".Bar+0
297c371,392
< type."".Bar SRODATA size=96
---
> type..namedata.*func(main.Bar)- SRODATA dupok size=18
>   0x0000 00 00 0f 2a 66 75 6e 63 28 6d 61 69 6e 2e 42 61  ...*func(main.Ba
>   0x0010 72 29                                            r)
> type.*func("".Bar) SRODATA dupok size=56
>   0x0000 08 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00  ................
>   0x0010 7f 95 9a 2f 08 08 08 36 00 00 00 00 00 00 00 00  .../...6........
>   0x0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
>   0x0030 00 00 00 00 00 00 00 00                          ........
>   rel 24+8 t=1 runtime.memequal64·f+0
>   rel 32+8 t=1 runtime.gcbits.01+0
>   rel 40+4 t=5 type..namedata.*func(main.Bar)-+0
>   rel 48+8 t=1 type.func("".Bar)+0
> type.func("".Bar) SRODATA dupok size=64
>   0x0000 08 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00  ................
>   0x0010 b4 2e bc 27 02 08 08 33 00 00 00 00 00 00 00 00  ...'...3........
>   0x0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
>   0x0030 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
>   rel 32+8 t=1 runtime.gcbits.01+0
>   rel 40+4 t=5 type..namedata.*func(main.Bar)-+0
>   rel 44+4 t=6 type.*func("".Bar)+0
>   rel 56+8 t=1 type."".Bar+0
> type."".Bar SRODATA size=112
303c398,399
<   0x0050 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00  ................
---
>   0x0050 00 00 00 00 01 00 01 00 10 00 00 00 00 00 00 00  ................
>   0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
309a406,409
>   rel 96+4 t=5 type..namedata.Print.+0
>   rel 100+4 t=25 type.func()+0
>   rel 104+4 t=25 "".(*Bar).Print+0
>   rel 108+4 t=25 "".Bar.Print+0
320a421,423
> ""..stmp_2 SRODATA size=16
>   0x0000 00 00 00 00 00 00 00 00 0c 00 00 00 00 00 00 00  ................
>   rel 0+8 t=1 go.string."debosmit ray"+0
325,326c428,429
< gclocals·2a5305abe05176240e61b8620e19a815 SRODATA dupok size=9
<   0x0000 01 00 00 00 01 00 00 00 00                       .........
---
> gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
>   0x0000 01 00 00 00 00 00 00 00                          ........
329c432
< "".(*Bar).Print.stkobj SRODATA size=24
---
> "".Bar.Print.stkobj SRODATA size=24
333,334d435
< gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
<   0x0000 01 00 00 00 00 00 00 00                          ........
338a440,447
> gclocals·1a65e721a2ccc325b382662e7ffee780 SRODATA dupok size=10
>   0x0000 02 00 00 00 01 00 00 00 01 00                    ..........
> gclocals·2589ca35330fc0fce83503f4569854a0 SRODATA dupok size=10
>   0x0000 02 00 00 00 02 00 00 00 00 00                    ..........
> "".(*Bar).Print.stkobj SRODATA dupok size=24
>   0x0000 01 00 00 00 00 00 00 00 f0 ff ff ff ff ff ff ff  ................
>   0x0010 00 00 00 00 00 00 00 00                          ........
>   rel 16+8 t=1 type.[1]interface {}+0

在这里,对于value method的情况,很明显:

  1. 由于b.Print()的调用而创建了b的一个副本
  2. 在副本结构体上设置了字符串rel 0+8 t=1 go.string."debosmit ray"+0的一个副本。

因此,这进一步说明,当您使用value指针时:

  1. 当您调用该对象的方法时,会创建该对象的副本
  2. 对内部状态的任何更改都只会反映在该对象的副本上

6

Go通过值传递参数,而不是引用传递,除非你使用指针。因此,在函数内部,如果你只是按值传递,那么你并没有修改任何外部作用域中的s。但是,当你传递一个指针时,你能够修改“真正”的变量,而不仅仅是存在于函数内部的副本。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接