Go:实现接口时指针接收器和值接收器的区别

在为类型定义方法时,如果没有避免拷贝或在方法中修改对象的需求,那选择指针接收器和值接收器区别不大。大多数情况下,我们可以通过指针直接调值接收器方法,也可以通过值直接调指针接收器方法,go会为我们自动做值和指针之间的转换:

type A struct {
    d int
}

func (a A) print() {
    fmt.Println(a.d)
}

type B struct {
    d int
}

func (b *B) print() {
    fmt.Println(b.d)
}

func main() {
    a := A{d: 111}
    pa := &a
    b := B{d: 222}
    pb := &b

    a.print()
    pa.print()
    b.print()
    pb.print()
}

但是当我们是为了实现接口而定义方法时,指针接收器和值接收器之间的区别就不能忽略了:

type X struct {
    d int
}

func (x X) String() string {
    return fmt.Sprintf("X{d:%d}", x.d)
}

type Y struct {
    d int
}

func (y *Y) String() string {
    return fmt.Sprintf("Y{d:%d}", y.d)
}

func print(v fmt.Stringer) {
    fmt.Println(v)
}

func main() {
    x := X{d: 111}
    px := &x
    y := Y{d: 222}
    py := &y
    print(x)
    print(px)
    print(y) // error!
    print(py)
}

编译上面的代码时,print(y)一行会报错:

./main.go:32:7: cannot use y (type Y) as type fmt.Stringer in argument to print:
        Y does not implement fmt.Stringer (String method has pointer receiver)

提示不能把类型Y的实参值赋给类型fmt.Stringer的形参,原因是类型Y没有实现fmt.Stringer接口。

打印一下X*XY*Y类型都实现了哪些方法,以及是否实现了fmt.Stringer接口:

func printMethod(v interface{}) {
    defer fmt.Println("-----")
    t := reflect.TypeOf(v)
    fmt.Printf("Type: %v\n", t)
    if t == nil {
        fmt.Printf("  No method\n")
        return
    }
    methodCount := t.NumMethod()
    fmt.Printf("  Method count: %d\n", methodCount)
    for i := 0; i < methodCount; i++ {
        fmt.Printf("    %d: %v\n", i, t.Method(i))
    }
    stringerType := reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
    fmt.Printf("  Implements fmt.Stringer: %v\n", t.Implements(stringerType))
}

...
    printMethod(x)
    printMethod(px)
    printMethod(y)
    printMethod(py)

输出为:

Type: main.X
  Method count: 1
    0: {String  func(main.X) string <func(main.X) string Value> 0}
  Implements fmt.Stringer: true
-----
Type: *main.X
  Method count: 1
    0: {String  func(*main.X) string <func(*main.X) string Value> 0}
  Implements fmt.Stringer: true
-----
Type: main.Y
  Method count: 0
  Implements fmt.Stringer: false
-----
Type: *main.Y
  Method count: 1
    0: {String  func(*main.Y) string <func(*main.Y) string Value> 0}
  Implements fmt.Stringer: true
-----

唯独main.Y类型没有实现String方法,所以也就没有实现fmt.Stringer接口。这反映了go的一个特性:对于类型*T,其自动实现了类型T实现的方法;而对于类型T,其没有自动实现*T实现的方法;类型T可以调用类型*T的方法只是go的一个语法糖。

在某些情况下,我们可以利用go的这一点特性,在之前一篇文章中我提到过
fmt.Print系列函数默认情况下不打印结构体中指针字段指向的值
,运行以下代码:

type Foo struct {
    A int
}

type Bar struct {
    F *Foo
}

func main() {
    b := Bar{&Foo{1}}
    fmt.Printf("%+v\n", b)
}

输出为:

{F:0xc0000140a0}

我们可以为Foo类型实现fmt.Stringer接口来使fmt.Printf打印出Bar中的F字段:

// 方式1
func (f Foo) String() string {
    return fmt.Sprintf("{A: %v}", f.A)
}

输出为:

{F:{A: 1}}

实际上String方法还可以实现的更简单一些:

// 形式2
func (f *Foo) String() string {
    return fmt.Sprintf("&%+v", *f)
}

在方式2中,*Foo类型实现了fmt.Stringer接口,而Foo类型没有实现,所以打印*f时不会调用到(*Foo).String方法,就不会有递归调用的问题了。而下面这种实现就会发生递归的问题:

// 方式3
func (f Foo) String() string {
    return fmt.Sprintf("&%+v", f)
}

在方式3中,fmt.Sprintf函数在打印f时会发现f实现了fmt.Stringer接口,因此会调用f.String进行字符化,而f.String又会调用到fmt.Sprintf方法,这就产生了无限递归的问题:

#0  main.(*Foo).String (~r0=...) at /mnt/d/wsl/code/godive/main.go:16
#1  0x0000000000489662 in fmt.(*pp).handleMethods (p=0xc00009ed00, verb=118, handled=true) at /usr/lib/go-1.13/src/fmt/print.go:630
#2  0x0000000000489c26 in fmt.(*pp).printArg (p=0xc00009ed00, arg=..., verb=118) at /usr/lib/go-1.13/src/fmt/print.go:713
#3  0x000000000048d5eb in fmt.(*pp).doPrintf (p=0xc00009ed00, format=..., a=...) at /usr/lib/go-1.13/src/fmt/print.go:1030
#4  0x0000000000486866 in fmt.Sprintf (format=..., a=..., ~r2=...) at /usr/lib/go-1.13/src/fmt/print.go:219
#5  0x000000000048ea87 in main.(*Foo).String (~r0=...) at /mnt/d/wsl/code/godive/main.go:16
#6  0x0000000000489662 in fmt.(*pp).handleMethods (p=0xc00009ec30, verb=118, handled=true) at /usr/lib/go-1.13/src/fmt/print.go:630
#7  0x0000000000489c26 in fmt.(*pp).printArg (p=0xc00009ec30, arg=..., verb=118) at /usr/lib/go-1.13/src/fmt/print.go:713
#8  0x000000000048d5eb in fmt.(*pp).doPrintf (p=0xc00009ec30, format=..., a=...) at /usr/lib/go-1.13/src/fmt/print.go:1030
#9  0x0000000000486866 in fmt.Sprintf (format=..., a=..., ~r2=...) at /usr/lib/go-1.13/src/fmt/print.go:219
#10 0x000000000048ea87 in main.(*Foo).String (~r0=...) at /mnt/d/wsl/code/godive/main.go:16
#11 0x0000000000489662 in fmt.(*pp).handleMethods (p=0xc00009eb60, verb=118, handled=true) at /usr/lib/go-1.13/src/fmt/print.go:630
#12 0x0000000000489c26 in fmt.(*pp).printArg (p=0xc00009eb60, arg=..., verb=118) at /usr/lib/go-1.13/src/fmt/print.go:713
#13 0x000000000048d5eb in fmt.(*pp).doPrintf (p=0xc00009eb60, format=..., a=...) at /usr/lib/go-1.13/src/fmt/print.go:1030
#14 0x0000000000486866 in fmt.Sprintf (format=..., a=..., ~r2=...) at /usr/lib/go-1.13/src/fmt/print.go:219
#15 0x000000000048ea87 in main.(*Foo).String (~r0=...) at /mnt/d/wsl/code/godive/main.go:16
#16 0x0000000000489662 in fmt.(*pp).handleMethods (p=0xc00009ea90, verb=118, handled=true) at /usr/lib/go-1.13/src/fmt/print.go:630
#17 0x000000000048cb48 in fmt.(*pp).printValue (p=0xc00009ea90, value=..., verb=118, depth=1) at /usr/lib/go-1.13/src/fmt/print.go:727
#18 0x000000000048bf85 in fmt.(*pp).printValue (p=0xc00009ea90, value=..., verb=118, depth=0) at /usr/lib/go-1.13/src/fmt/print.go:810
#19 0x0000000000489cd4 in fmt.(*pp).printArg (p=0xc00009ea90, arg=..., verb=118) at /usr/lib/go-1.13/src/fmt/print.go:716
#20 0x000000000048d5eb in fmt.(*pp).doPrintf (p=0xc00009ea90, format=..., a=...) at /usr/lib/go-1.13/src/fmt/print.go:1030
#21 0x0000000000486762 in fmt.Fprintf (w=..., format=..., a=..., n=<optimized out>, err=...) at /usr/lib/go-1.13/src/fmt/print.go:204
#22 0x000000000048e9d3 in fmt.Printf (format=..., a=..., n=<optimized out>, err=...) at /usr/lib/go-1.13/src/fmt/print.go:213
#23 main.main () at /mnt/d/wsl/code/godive/main.go:20
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值