热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

golang关于valuereceiver和pointerreceiver

golang关于valuereceiver和pointerreceiver-关于valuereceiver和pointerreceivervaluereceiver其实说的主要是

关于value receiver和pointer receiver

value receiver其实说的主要是基于结构体的方法实现,pointer receiver说的是基于结构体指针的实现, 我们可以先用例子开始说起:

package main

import "fmt"

// Person ...
type Person interface {
    SetAge()
    SetName()
}

// Teacher ...
type Teacher struct {
    Name string
    Age  int
}

// SetAge ...
func (p *Teacher) SetAge() {
    p.Age = 24
}

// SetName ...
func (p *Teacher) SetName() {
    p.Name = "teacher"
}

// Student  ...
type Student struct {
    Name string
    Age  int
}

// SetAge ...
func (p Student) SetAge() {
    p.Age = 16
}

// SetName ...
func (p Student) SetName() {
    p.Name = "student"
}

func main() {
    var p1 Person = &Teacher{}
    p1.SetAge()
    p1.SetAge()
    fmt.Printf("teacher: %+v \n", p1) // teacher: &{Name: Age:24}

    var p2 Person = Student{}
    p2.SetAge()
    p2.SetAge()
    fmt.Printf("Student: %+v \n", p2) // Student: {Name: Age:0}

    var p3 Person = &Student{}
    p3.SetAge()
    p3.SetAge()
    fmt.Printf("Student: %+v \n", p3) // Student: {Name: Age:0}

    var p4 Person = Teacher{}
    p4.SetAge()
    p4.SetAge()
    fmt.Printf("teacher: %+v \n", p4) // cannot use Teacher literal (type Teacher) as type Person in assignment: Teacher does not implement Person (SetAge method has pointer receiver)
}

由此,我们可以得出一个结论如下:

结构体实现接口结构体指针实现接口
结构体初始化变量通过不通过
结构体指针初始化变量通过通过

其中,我们可以明确的知道有2种是肯定可以通过的,那就是:

  • 结构体初始化的变量,和方法接收者也是结构体
  • 结构体指针初始化的变量,和方法接受者也是结构体指针

接着, 结构体指针初始化的变量,方法接受者为结构体也可以编译通过,即上面的p3,

// Person ...
type Person interface {
    SetAge()
    SetName()
}
// Student  ...
type Student struct {
    Name string
    Age  int
}

// SetAge ...
func (p Student) SetAge() {
    p.Age = 16
}

// SetName ...
func (p Student) SetName() {
    p.Name = "student"
}

func main() {
    var p3 Person = &Student{}
    p3.SetAge()
    p3.SetAge()
}

那么为什么可以编译通过呢? 首先,我们知道,golang中,对于结构体方法的调用,例如

func (p Student) SetAge() {
    p.Age = 16
}

方法,编译器展开后,其实是等价于

func SetAge(p) {
    p.Age = 16
}

而在go中,所有的参数传递都是值拷贝,所以,当我们的初始化变量p为一个指针的时候,传递给SetAge方法的也是一个指针的值拷贝,这个拷贝的指针,和原来的指针指向的是同一个结构体,最终,我们的编译器可以隐式的通过解引用的方式,获取到这个指针指向的结构体。

那为什么反过来就不行呢?即:

type Person interface {
    SetAge()
    SetName()
}
type Student struct {
    Name string
    Age  int
}
func (p *Student) SetAge() {
    p.Age = 16
}
func (p *Student) SetName() {
    p.Name = "student"
}
func main() {
    var p3 Person = Student{}
    p3.SetAge()
    p3.SetName()
}

这个时候,我们发现编译会报错:

// cannot use Teacher literal (type Teacher) as type Person in assignment: Teacher does not implement Person (SetAge method has pointer receiver)

带着这个问题,我们先来试试另外一种方式,我们把初始化的变量p3定义为结构体类型Student,而非接口类型Person如下:

type Person interface {
    SetAge()
    SetName()
}
type Student struct {
    Name string
    Age  int
}
func (p *Student) SetAge() {
    p.Age = 16
}
func (p *Student) SetName() {
    p.Name = "student"
}
func main() {
    var p3 Student = Student{}
    p3.SetAge()
    p3.SetName()
    fmt.Printf("p3: %+v \n", p3)
}

你会发现,编译成功,毫无压力的输出

p3: {Name:student Age:16}

为什么会这样子呢?查看我们golang的官方文档,我们看到这样的一段说明:

  • This rule arises because pointer methods can modify the receiver; invoking them on a value would cause the method to receive a copy of the value, so any modifications would be discarded. The language therefore disallows this mistake. There is a handy exception, though. When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically. In our example, the variable b is addressable, so we can call its Write method with just b.Write. The compiler will rewrite that to (&b).Write for us.
  • 这个规则的产生是因为指针方法可以修改接收者;对值调用它们将导致方法接收值的副本,因此任何修改都将被丢弃。语言不允许这种错误。不过,有一个例外。当值是可寻址的时,该语言通过自动插入address操作符来处理对值调用指针方法的常见情况。在我们的例子中,变量b是可寻址的,所以我们可以用b.Write调用它的Write方法。编译器将把它重写为(&b)。

所以,当我们var p3 Student = Student{} 把初始化的结构体Student赋值给变量类型为结构体类型Student的时候,我们可以通过&p3隐式的获取到它的地址,从而实现方法的调用。

但是,当我们var p3 Person = Student{} 把初始化的结构体Student赋值给变量类型为接口Person的时候,为什么编译器就报错了呢?

根据上面的文档,我们知道,当p3是可寻址的时候,编译器可以隐式的通过(&p3)获取到它的地址,那么,这里编译器报错的唯一原因,那就是类型为Person接口的p3是不可寻址的,或者说编译器本身就禁止了去获取接口类型p3的地址的这种行为,从而导致边我们编译不通过,出现:

  • cannot use Teacher literal (type Teacher) as type Person in assignment: Teacher does not implement Person (SetAge method has pointer receiver)

通过官方的文档我们确认了这一点:

  • The concrete value stored in an interface is not addressable, in the same way that a map element is not addressable. Therefore, when you call a method on an interface, it must either have an identical receiver type or it must be directly discernible from the concrete type: pointer- and value-receiver methods can be called with pointers and values respectively, as you would expect. Value-receiver methods can be called with pointer values because they can be dereferenced first. Pointer-receiver methods cannot be called with values, however, because the value stored inside an interface has no address. When assigning a value to an interface, the compiler ensures that all possible interface methods can actually be called on that value, and thus trying to make an improper assignment will fail on compilation.

那么,编译器为什么要这么做? 我们可以举个栗子:

type I interface{}
type A int
type B string

func main() {
    var a A = 5
    var i I = a
    fmt.Printf("i is of type %T \n", i)
    var aPtr *A
    //aPtr = &(i.(A))
    //fmt.Printf("%p \n", aPtr)
    var b B = "hello"
    i = b
    fmt.Printf("i is of type %T, aPtr is of type %T \n", i, aPtr)
}

假如一个接口类型是可寻址的,即假如上面注释的aPtr = &(i.(A))这行代码可以运行成功,那么,当我们把变量类型为B的字符串变量b赋值给i的时候,这个时候aPtr它指向的是啥?aPtr被声明为指向A,但i现在包含B,并且aPtr不再是指向A的有效指针(我的理解就是,你不能再基于类型A去使用aPtr这个指针,因为此时你已经不知道它指向的是什么类型)

所以,通篇下来,我们围绕的一个问题就是:

  • 为什么一个包含非指针值的接口不能成为一个带有指针接收器的方法的接收器呢?
  • 因为存储在接口中的非指针值是不可寻址的,所以编译器不能将其地址传递给带有指针接收器的方法。

参考的连接:

blog.golang.org/laws-of-ref…

colobu.com/2018/02/27/…

golang.org/ref/spec#Ad…

golang.org/doc/effecti…

github.com/golang/go/w…

stackoverflow.com/questions/4…

stackoverflow.com/questions/4…

stackoverflow.com/questions/3…

colobu.com/2018/02/27/…


推荐阅读
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • (三)多表代码生成的实现方法
    本文介绍了一种实现多表代码生成的方法,使用了java代码和org.jeecg框架中的相关类和接口。通过设置主表配置,可以生成父子表的数据模型。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • 也就是|小窗_卷积的特征提取与参数计算
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了卷积的特征提取与参数计算相关的知识,希望对你有一定的参考价值。Dense和Conv2D根本区别在于,Den ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • 3.223.28周学习总结中的贪心作业收获及困惑
    本文是对3.223.28周学习总结中的贪心作业进行总结,作者在解题过程中参考了他人的代码,但前提是要先理解题目并有解题思路。作者分享了自己在贪心作业中的收获,同时提到了一道让他困惑的题目,即input details部分引发的疑惑。 ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
author-avatar
龚magnett_672
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有