英文:
In Go is naming the receiver variable 'self' misleading or good practice?
问题
我看过很多关于Go语言的博客和视频,就我记得,没有一个作者在编写方法时使用"self"或"this"作为接收者变量。然而,在Stack Overflow上有一些问题使用了这种方式,这让我想到是否将变量命名为"self"会产生误导?
阅读Method Sets的规范并没有提供任何证据支持或反对这种做法(根据我的理解)。
我记得在某个地方找到过它实际上并不是一个self指针,有人能列举证据或提供支持或反对的理由吗?还有,如果将其视为"self"可能会出现什么问题或陷阱?
一个快速的例子:
type MyStruct struct {
Name string
}
哪种方法更合适,或者两种都可以?
func (m *MyStruct) MyMethod() error {
// do something useful
}
或者
func (self *MyStruct) MyMethod() error {
// do something useful
}
英文:
I have seen a fair amount of blogs & videos on Go and as far as I recall, none of the authors use 'self' or 'this' for the receiver variable when writing methods. However there seems to be a number of questions on stack overflow that do this, and it got me thinking about if this is misleading to name the variable 'self'?
Reading the Spec for Method Sets does not provide any evidence either way (in my interpretation).
I seem to recall finding somewhere that it was not really a self pointer, can anyone list evidence or provide reasoning either way, and if any problems/traps that might occur from thinking of it as 'self'?
A quick example:
type MyStruct struct {
Name string
}
Which method is more appropriate, or both?
func (m *MyStruct) MyMethod() error {
// do something useful
}
or
func (self *MyStruct) MyMethod() error {
// do something useful
}
答案1
得分: 51
除了其他人说的(特别是PeterSO
和dskinner
- 在他对Peter的回答中的评论中),还要注意几个重要的事情:
你可以像调用简单函数一样调用方法
在Go中,你可以将任何方法函数作为一个普通函数调用,而不是作为接收器的方法,只需用它所定义为方法的类型的名称限定其名称,并显式传递一个接收器参数(从方法中获取一个简单函数被称为使用方法表达式)。
示例:
package main
import "fmt"
type Foo int
func (f Foo) Bar() {
fmt.Printf("My receiver is %v\n", f)
}
func main() {
a := Foo(46)
a.Bar()
b := Foo(51)
Foo.Bar(b)
}
运行时,该程序输出:
My receiver is 46
My receiver is 51
正如你所看到的,这里self
失去了它的神圣意义,因为你刚刚调用了一个方法,人为地构造了与之无关的上下文,这与那个被引用的“调用对象的方法就是向该对象传递消息”的概念无关。
总结一下,在Go中,方法只是语义上绑定到特定类型的函数,它接收一个额外的参数 - 接收器,无论如何调用它。与许多其他主流语言相反,Go不会将这个事实隐藏起来。
接收器在其类型的方法中不一定是可变的
正如我示例中所示,我在一个非指针接收器上定义了一个方法Bar()
,如果你尝试给接收器赋值,赋值会成功,但不会影响调用者,因为接收器 - 就像Go中的一切一样 - 是按值传递的(所以整数只是被复制了一份)。
要能够在方法中改变接收器的值,你必须在适当类型的指针上定义它,例如:
func (f *Foo) Bar() {
// 在这里你可以通过*f改变值,比如
*f = 73
}
同样,你可以看到在这里使用self
表示“我”,“我的内部”变得无关紧要:在我的示例中,该方法只是接收了一个它知道类型的值。你可以看到这与许多面向对象的语言相反,在这些语言中,对象通常是通过引用传递的黑盒子。在Go中,你可以在几乎任何东西上定义一个方法(包括其他方法,这是net/http
标准包使用的方式,顺便说一下),这侵蚀了“方法是为对象而存在”的概念。
不同的方法集可能在不同的时间适用于同一个值
在Go中,方法是围绕特定类型组织功能的一种便捷方式,在程序流的不同点上,可能适用于同一个值的不同方法集。结合接口和鸭子类型,这个概念真的很有用。这个想法是,在Go中,有一种定义“支持”类型的习惯用法,它在某些其他类型的值上执行某些操作。
一个很好的例子是标准包sort
:例如,它提供了类型IntSlice
,允许你对整数切片进行排序 - 类型为[]int
的值。为了做到这一点,你将切片转换为sort.IntSlice
,得到的值具有一整套用于对切片进行排序的方法,而你的值的内部表示没有改变 - 因为sort.IntSlice
被定义为type IntSlice []int
。在IntSlice
类型的每个方法中,很难将其接收器值的含义与self
协调起来 - 简单地因为该类型仅仅存在于为另一种类型提供一组方法;从哲学上讲,这种实用类型没有“self”的概念
结论
所以我想说的是,在你的头脑中保持简单,不要试图用它没有明确提供的语义来“过载”Go采用的明确而简单的方法。
还有一点需要注意。我个人对Go的习惯用法的感知是,Go的最重要特性是其实用性(与理想主义等相对),所以如果你看到一些“感觉”不自然的概念,尝试弄清楚为什么它被设计成这样,往往你会发现为什么这个概念在你的大脑中“点击”并变得自然。(我必须承认,要理解Go中的方法的这个特定问题,对C
的良好工作熟悉度将非常有帮助。)
英文:
In addition to what others said (especially PeterSO
and dskinner
—in his comment to the Peter's answer), note several important things:
You can call a method like a simple function
In Go, you can call any method function not as a method on a receiver but rather as a regular function—simply by qualifying its name with the name of the type it's defined to be a method on and explicitly passing it a receiver argument (obtaining a simple function from a method is called
using a method expression).
To demonstrate:
package main
import "fmt"
type Foo int
func (f Foo) Bar() {
fmt.Printf("My receiver is %v\n", f)
}
func main() {
a := Foo(46)
a.Bar()
b := Foo(51)
Foo.Bar(b)
}
When run, this program prints:
My receiver is 46
My receiver is 51
As you can see, self
loses its sacred meaning here because you've just called a method artificially constructing the context for it which has nothing to do with the much cited "calling an object's method is passing a message to that object" concept.
To recap, in Go, a method is just a function semantically bound to a particular type which receives a single extra argument—its receiver—no matter how it's called. Contrary to many other mainstream languages, Go does not hide this fact under the carpet.
A receiver is not necessarily mutable inside a method defined on its type
As demonstrated in my example, I've defined a method, Bar()
, on a non-pointer receiver, and if you'll try to assign a value to the receiver that will succeed but won't affect the caller because the receiver—as everything in Go—has been passed by value (so that integer has just been copied).
To be able to mutate the receiver's value in the method, you'd have to define it on an appropriately-typed pointer, like
func (f *Foo) Bar() {
// here you can mutate the value via *f, like
*f = 73
}
Again, you can see that using self
meaning "me", "my internals" becomes moot here: in my example the method merely received a value which type it knows. You can see this is in contrast with many OO-languages in which an object is a black box usually passed around by reference. In Go, you can define a method on virtually anything (including other methods, which is used by the net/http
standard package, by the way) which erodes that "methods are for objects" concept.
Different sets of methods might be applicable to the same value at different times
In Go, methods are a convenient way to group functionality around particular types, and different sets of methods might be applicable to the same value in different points of the program flow. Combined with interfaces and duck-typing they provide, this concept really flourishes. The idea is that in Go, there's an idiom of defining "support" types which perform certain operation on values of some other type.
A good example of this is the standard package sort
: for instance, it provides the type IntSlice
which allows you to sort a slice of integers—a value of type []int
. To do that you type-convert your slice to sort.IntSlice
and the value you get as a result has a whole set of methods for sorting your slice while the internal representation of your value has not changed— because sort.IntSlice
is defined as type IntSlice []int
. In each method of that IntSlice
type, it's hard to reconcile the meaning of their receiver value with self
—simply because the type solely exists to provide a set of methods for another type; in a philosophical sense, such utility types have no concept of "self"
Conclusion
So I'd say, keep things simple in your head and do not try to "overload" the clear and simple approach taken by Go with semantics it does not explicitly state it provides.
One more note. My personal perception of Go's idioms as I learned them is that the paramount property of Go is its practicality (as opposed to idealism etc) so if you see some concept which "feels" unnatural try to work out why it's designed that way, and most often you'll discover why so the concept "clicks" in your brain and gets natural. (I must admit that to grok this particular problem with understanding methods in Go, a good working familiarity with C
would be of much help.)
答案2
得分: 38
我看不出有什么特别强烈的理由避免使用 this
/ self
的约定。这里的其他帖子只是引用了社区规范或描述了与命名约定无关的方法调度方面的内容。
这些代码审查准则拒绝使用 this
或 self
,却没有给出任何理由,除非你能从其中暗示的观点中读出一些东西,即 Go 语言对方法的重视程度较其他语言较低。
承诺使用 this
或 self
约定的一个优点是它有助于突出违反德米特法则的情况。像这样的代码:
func (w *ChunkWriter) Write(recId uint32, msg []byte) (recs uint64, err error) {
recs = w.chunk.Records
err = w.handle.Write(recId, msg)
if err == nil {
recs++
w.chunk.Records = recs
}
return
}
表面上看似乎不适当地访问了 w
的成员。实际上,它是在访问其接收器的成员,这是完全正确的。
英文:
I can see no particularly compelling reason to avoid the this
/ self
convention. Other posts here merely cite community norms or describe aspects of method dispatch which have no bearing on naming conventions.
These code review guidelines reject this
or self
without giving any reason at all, unless you can read something into the implied claim that go places less emphasis on methods than other languages.
One advantage of committing to a this
or self
convention is that it helps to highlight violations of the Law of Demeter. Code such as this:
func (w *ChunkWriter) Write(recId uint32, msg []byte) (recs uint64, err error) {
recs = w.chunk.Records
err = w.handle.Write(recId, msg)
if err == nil {
recs++
w.chunk.Records = recs
}
return
}
appears on the face of it to be reaching into w
's members inappropriately. In fact it is accessing members of its receiver, which is perfectly proper.
答案3
得分: 20
我同意Oliver Goodman的回答,并钦佩他在面对如此压倒性的困难时的勇气。
基本上,反对者认为在其他语言中,使用“this”(我个人偏好)作为接收器变量并不完全具有相同的含义。因此,我们应该避免使用它,以预防潜在的问题,并尽量减少混淆。
但是,当然,同样的观点也可以适用于->
和.
,更不用说Go中的许多其他类似C的特性了(我们无法控制)。
然而,按照提问者所暗示的方式使用“this”使得理解上下文变得更加容易。并且,由于我们知道我们在使用Go进行编程,我们理解它与其他语言有不同的含义。
使用“this”还意味着我们的类型(“类”)方法是“常规的”。如果期望有一些奇怪的情况,那么你可以始终使用另一个接收器名称来强调这一事实。
但是,让我们不要把孩子和洗澡水一起扔掉!
从一开始,我发现顽固的Gopher们往往非常固执。但我必须承认,在许多方面,这对语言的发展非常有益。
在这方面,我向他们致敬。
然而,这种不妥协的思维方式在其他领域引发了问题,
GOPATH的疯狂就是最明显的例子。
听专家的意见并不意味着不经过思考。即使专家也有盲点和偏见,其程度往往与他们的专业性成正比。
简而言之:使用最适合你的约定!
英文:
I concur with Oliver Goodman's answer and admire the courage of his convictions in the face of such overwhelming odds.
Basically, the naysayers are saying that using "this" (my preference) as the receiver variable does NOT have precisely the same meaning as in other languages. So we should avoid it to preempt potential gotchas and keep confusion to a minimum.
But, of course, the same could be said about ->
and .
, not to mention many other C-like facets of Go (over which we have no control).
However, using "this" in the way the OP implies makes it so much easier to understand context. And since we know we are programming in Go, we understand that it has different implications compared to other languages.
Using "this" also implies that our type ("class") methods are "conventional". If some weirdness is expected then you can always use another receiver moniker to emphasise this fact.
But let's not throw the baby out with the bathwater!
From the very start, I have found that hard core Gophers tend to be very rigid and unbending. But I must admit that this has served the language well in many many areas.
And I take my hat off to them in that respect.
However, this same unyielding mindset has unleashed problems in other areas,
The GOPATH craziness is the most obvious example.
Listening to the experts is no excuse for not thinking things through. Even the experts have blind spots and hang-ups whose magnitude is often in direct proportion to their speciality.
Short answer: Use the convention that suits you best!
答案4
得分: 13
Go wiki建议不要使用诸如this
或self
之类的术语,而是使用从类型名称派生的缩写词。你可以在这里查看详细信息:https://github.com/golang/go/wiki/CodeReviewComments#receiver-names
英文:
The Go wiki recommends not using terms like this
or self
and instead using abbreviations derived from the type name: https://github.com/golang/go/wiki/CodeReviewComments#receiver-names
答案5
得分: 1
从https://blog.heroku.com/neither-self-nor-this-receivers-in-go中。
func (this *Room) Announce() {
srv := this.Server()
for _, c := range srv.Clients() {
// 向所有客户端发送有关新房间的公告
c.Send(srv.RenderAnnouncement(this))
}
}
// 移动代码之间...
func (this *Server) AddRoom(room *Room) {
for _, c := range this.Clients() {
// 向所有客户端发送有关新房间的公告
c.Send(this.RenderAnnouncement(room))
}
}
在使用this时,存在对于我们是指服务器还是房间的混淆,因为我们正在移动代码。
- c.Send(this.RenderAnnouncement(room))
+ c.Send(srv.RenderAnnouncement(this))
重构这种代码会产生一些错误,编译器有希望能够捕捉到(或者也许不会,如果接口恰好兼容)。即使忽略错误,编辑所有细节确实使代码移动更加繁琐。
在不同抽象级别之间移动是一个很好的例子,说明一致命名的接收器会产生巨大的差异:
func (room *Room) Announce() {
srv := room.Server()
for _, c := range srv.Clients() {
// 向所有客户端发送有关新房间的公告
c.Send(srv.RenderAnnouncement(room))
}
}
// 移动代码之间...
func (srv *Server) AddRoom(room *Room) {
for _, c := range srv.Clients() {
// 向所有客户端发送有关新房间的公告
c.Send(srv.RenderAnnouncement(room))
}
}
英文:
From https://blog.heroku.com/neither-self-nor-this-receivers-in-go.
func (this *Room) Announce() {
srv := this.Server()
for _, c := range srv.Clients() {
// Send announcement to all clients about a new room
c.Send(srv.RenderAnnouncement(this))
}
}
// Moved between...
func (this *Server) AddRoom(room *Room) {
for _, c := range this.Clients() {
// Send announcement to all clients about a new room
c.Send(this.RenderAnnouncement(room))
}
}
When using this, there is confusion about whether we're referring to the server or the room as we're moving the code between.
- c.Send(this.RenderAnnouncement(room))
+ c.Send(srv.RenderAnnouncement(this))
Refactoring this kind of code produce some bugs that the compiler will hopefully catch (or maybe not, if the interfaces happen to be compatible). Even bugs aside, having to edit all the little innards does make moving code around more tedious.
Moving across levels of abstraction is a great example of when consistently well-named receivers make a huge difference:
func (room *Room) Announce() {
srv := room.Server()
for _, c := range srv.Clients() {
// Send announcement to all clients about a new room
c.Send(srv.RenderAnnouncement(room))
}
}
// Moved between...
func (srv *Server) AddRoom(room *Room) {
for _, c := range srv.Clients() {
// Send announcement to all clients about a new room
c.Send(srv.RenderAnnouncement(room))
}
}
答案6
得分: -2
查看Go命令和Go标准包的源代码,可以了解良好编程风格的示例。在Go方法接收器中,不使用名称self
。
名称self
是Python的约定。Go不是Python。
9.4. 随机备注
通常,方法的第一个参数被称为self。这只是一种约定:self这个名称对Python没有任何特殊含义。然而,请注意,如果不遵循这种约定,你的代码可能对其他Python程序员来说不太易读,而且还有可能编写依赖于这种约定的类浏览器程序。
英文:
Look at the source code for Go commands and Go standard packages for exemplars of good programming style. The name self
is not used for Go method receivers.
The name self
is a Python convention. Go is not Python.
> The Python Tutorial
>
> 9.4. Random Remarks
>
> Often, the first argument of a method is called self. This is nothing
> more than a convention: the name self has absolutely no special
> meaning to Python. Note, however, that by not following the convention
> your code may be less readable to other Python programmers, and it is
> also conceivable that a class browser program might be written that
> relies upon such a convention.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论