确定方法的接收者时的指针和值语义

创建新的数据类型是每个程序员工作的重要组成部分。 在大多数语言中,类型定义由对其字段和方法的描述组成。 除此之外,在Golang中,您需要确定要使用的新类型方法的接收者语义:值(值)或指针(指针)。 乍看起来,这个决定似乎是次要的,因为在大多数情况下,该程序可以使用接收者的任何语义。 因此,许多人跳过了这一点而编写了代码,却没有弄清楚到底是什么,这受方法接收者的语义的影响。 为了弄清楚,您需要更深入地了解Golang的工作方式。

考虑一个小例子。 使用一个“ 名称”字段和sayHello(人员字符串)方法定义一个cat结构。 在下文中, 方法 I将指代与特定类型关联的函数,具有方法的变量的对象 ,并且方法的接收者将是方法说明中单词func后面括号中指示的变量。

type cat struct { Name string } func (c *cat) sayHello(person string) { fmt.Println(fmt.Sprintf("Meow, meow, %s!", person) } 

如果我们定义一个指向cat的指针并向其请求Name字段,那么很显然,由于从nil调用了该字段,我们会得到一个错误:

 var c *cat // c=nil fmt.Println(c.Name) //panic: runtime error: invalid memory address or nil pointer dereference 

https://play.golang.org/p/L3FnRJXKqs0

但是,当对同一个变量调用sayHello()方法时,将不会出现错误:

 var c *cat // c=nil c.sayHello(“Human”) //Meow, meow, Human! 

https://play.golang.org/p/EMoFgKL1HEi

为什么在此示例中不能调用方法,并且如何用语言本身的体系结构对此进行解释? 之所以成为可能,是因为Go中的方法是语法糖,或者换句话说,是具有接收者参数之一的函数的包装。 当调用c.sayHello(“ Human”)方法时,实际上将调用(* cat).sayHello(c,s)构造( https://play.golang.org/p/X9leJeIvxcA )。 通过从上面的示例中调用nil方法,我们实际上在参数中调用了nil的函数,这已经是很正常的情况了。 因此,在Go nil中,它是方法的正确接收者。

由于方法接收器实际上是一个参数,因此对方法接收器使用语义“值”或“指针”的建议类似于对函数参数的建议。 反过来,它们是从Go的基本规则推论得出的: 参数总是通过value传递给函数 。 这意味着将任何参数传递给函数都是通过其复制进行的:如果函数接受一个结构作为输入,则该结构的完整副本将进入其中; 如果它需要一个指向对象的指针,则新变量将带有指向同一对象的指针。 通过将变量地址传递给函数之前,将其与函数内部参数的地址( https://play.golang.org/p/oc2ssC_Irs8,https://play.golang.org/p/FeQa2HUdX0a )进行比较,可以看出这一点。

使用链接传递时:

  • 用于大型结构。 指针仅占用一个机器字(32、64位,取决于系统)。 因此,在接收器中使用指针调用方法时,复制指针要比复制整个对象便宜,就像按值传递一样。
  • 如果被调用的方法修改了对象本身的数据。 当接收者通过引用转移时,该方法可以通过间接进行更改来影响调用对象的状态。 按价值传递时这是不可能的。

使用价值转移时:

  • 对于简单的内置类型,例如数字,字符串,布尔型。 当使用指针时,将使用几乎与这种类型的对象相同数量的内存,并且如下所述,由垃圾收集器维护它的成本增加。
  • 对于切片以及其他参考类型:地图和通道-取指针是没有意义的。 他们本身已经是一个指针。
  • 使用多线程时,与按引用传递不同,按值传递是安全的。
  • 对于小型结构。 在这种情况下,按价值传输更有效。 这是因为方法的内部数据位于堆栈的单独框架中。 退出功能后,将清除其框架。 当我们沿指针翻找东西时,我们会将数据从堆栈传输到堆,从那里这些数据可用于其他功能。 增加堆会增加垃圾收集器的负担,垃圾收集器的操作会使程序速度平均降低25%。 使用逐值传输时,数据保留在堆栈上,不需要其他垃圾收集器工作。

当您需要考虑收件人的语义时:

  • 收件人的类型可能因主题领域而异。 在他的演讲中,比尔·肯尼迪(Bill Kennedy)给出了一个很好的例子,描述了用户类型。 当按值传递时,将为用户创建一个副本。 这将导致以下事实:同一用户的多个副本可以同时在程序中共存,然后可以独立更改,这与主题区域不对应,因为真实用户始终是一个用户,并且无法在不同时间用不同的集合描述他数据。
  • 确定方法的接收者类型的另一种可靠方法是对其类型使用构造方法。 如果构造函数返回值/指针,则在创建实体时,假定他们将继续使用它作为值/指针。 因此,最好在方法接收器中使用相同的语义。
  • 有一个不成文的规则,违反该规则编译器不会发誓,但是您的代码肯定不会因此而变得更好。 如果类型方法之一使用指针/值作为接收器,则为了保持一致性,其余方法必须使用指针/值。 类型方法不应包含值接收器和指针接收器的哈希。

结果如何


在Go Value中,语义表示复制值;指针语义表示提供对值的访问。 这适用于方法的参数及其接收者。 对于数字,线条,切片,地图,通道和小型结构等内置类型,几乎总是需要使用基于值的转移。 对于占用大量内存的结构以及状态可以通过其方法间接更改的结构,必须使用按引用进行传输。 同样,接收者的语义可能取决于类型描述的域,在其工厂中返回的语义以及此类型的其他方法中已经使用的接收者的语义。

Source: https://habr.com/ru/post/zh-CN469859/


All Articles