英文:
Go best practices: function vs method with ambiguous receiver?
问题
在Go语言中,使用方法(methods)和函数(functions)的最佳实践是什么?
具体来说,我有两个结构体:probeManager
和probeWorker
,我正在编写一个名为run
的函数,该函数需要访问这两个结构体的成员。这可以理解为告诉管理器运行工作器,或者在工作器上调用run并传递管理器以进行访问,或者我可以创建一个接受这两个结构体作为参数的run函数:
func (m *ProbeManager) run(w *ProbeWorker) { ... }
func (w *ProbeWorker) run(m *ProbeManager) { ... }
func run(m *ProbeManager, w *ProbeWorker) { ... }
由于这三种方法在语义上都是有效的,是否有一种方法比另一种方法更有优势,还是只是个人偏好的问题?
英文:
What are the best practices around when to use methods vs. functions in Go?
Specifically, I have 2 structs: probeManager
and probeWorker
, and I'm writing a function run
which needs to access members of both structs. This could be interpreted as telling the manager to run the worker, or as calling run on the worker and passing the manager for access, or I could just create a run function which takes both as arguments:
func (m *ProbeManager) run(w *ProbeWorker) { ... }
func (w *ProbeWorker) run(m *ProbeManager) { ... }
func run(m *ProbeManager, w *ProbeWorker) { ... }
Since all 3 approaches are semantically valid, are there any advantages to one approach over another, or does this just come down to personal preference?
答案1
得分: 3
使用方法可以定义接口。假设你有以下代码:
func (m *ProbeManager) Run(w *ProbeWorker) {}
你可以创建一个接口:
type Manager interface {
Run(w *ProbeWorker)
}
现在,任何接受 *ProbeManager
的地方都可以接受一个 Manager
。这样可以将 Run
与其具体实现的细节解耦。这样做有很多好处:
- 它使代码更易于理解和更容易安全地修改,因为它隐藏了不必要的细节(信息隐藏)
- 它使代码更易于测试,因为你可以模拟一个接口并在隔离的代码片段中测试它:
type mockManager struct {
run func(w *ProbeWorker)
}
func (m mockManager) Run(w *ProbeWorker) {
m.run(w)
}
func Test(t *testing.T) {
wasCalled := false
m := mockManager{
run: func(w *ProbeWorker) {
wasCalled = true
},
}
// 将 m 传递给接受 Manager 的函数
}
- 接口还使你能够实现依赖注入。有很多方法,但其中一个非常简单的方法是提供一个
Default
实现:
var DefaultManager Manager = &ProbeManager{}
或者使用基于字符串的注册表:
var managerLookup = map[string]Manager{}
func RegisterManager(nm string, m Manager) {
managerLookup[nm] = m
}
func GetManager(nm string) Manager {
return managerLookup[nm]
}
这非常强大,因为它允许你修改现有包的行为,而无需更改其代码。(例如,假设你有一个文件下载器,并实现了 http
支持。其他人可以提供 ftp
支持,使用这种注册表方法,不需要更改解析 URL 的代码)
- 接口还允许你实现其他编程语言中常见的类似问题的方法。它们提供了一种通用的多态性(参见
sort
包),你可以通过实现一个调用相同接口的接口来实现面向方面的编程或猴子补丁(考虑一个调用底层File
的gzip.Reader
。任何接受io.Reader
的地方也可以接受gzip.Reader
,允许你在不更改其余代码的情况下替换行为)
我还可以继续讲下去...
英文:
Using methods allows you to define interfaces. Suppose you have:
func (m *ProbeManager) Run(w *ProbeWorker) {}
You can create an interface:
type Manager interface {
Run(w *ProbeWorker)
}
And now anything that took the *ProbeManager
can take a Manager
instead. This decouples Run
from the details of its implementation. There are many reasons why this is useful:
-
It makes code easier to reason about and easier to safely change because it hides unnecessary details (information hiding)
-
It makes code easier to test as you can mock out an interface and test a small segment of your code in isolation:
type mockManager struct { run func(w *ProbeWorker) } func (m mockManager) Run(w *ProbeWorker) { m.run(w) } func Test(t *testing.T) { wasCalled := false m := mockManager{ run: func(w *ProbeWorker) { wasCalled = true }, } // pass m to something that takes a Manager }
-
Interfaces also give you the ability to implement dependency injection. There are many approaches, but one very simple one is to provide a
Default
implementation:var DefaultManager Manager = &ProbeManager{}
Or a string-based registry:
var managerLookup = map[string]Manager{} func RegisterManager(nm string, m Manager) { managerLookup[nm] = m } func GetManager(nm string) Manager { return managerLookup[nm] }
This is very powerful because it allows you modify the behavior of existing packages without having to change their code. (For example imagine you had a file downloader and you implemented
http
support. Someone else could provideftp
support, and the code needed to parse URLs wouldn't need to change by using this registry approach) -
Interfaces allow you to implement similar approaches to problems that you will find in other programming languages. They give you a kind of generic polymorphism (see the
sort
package), you can implement Aspect Oriented Programming or Monkey Patching by implementing an interface which invokes the same interface (consider agzip.Reader
which invokes an underlyingFile
. Anything that takes anio.Reader
can also take agzip.Reader
, allowing you to substitute behavior without having to change the rest of your code)
I could keep going...
答案2
得分: 0
它们实际上是等效的。接收器像其他参数一样传递给方法。由于无论如何都需要同时使用这两种类型(来调用该方法),所以定义在哪个类型上并不重要。根据个人经验,我会选择你提到的最后一种选项。对我来说更有意义,因为在其他情况下,你将方法与这两种类型之一关联起来,而实际上它需要两者。不过,这只是关于如何组织代码的问题。在性能或应用行为方面,它们之间没有任何区别,都是相同的。
编辑:最后一点,这些方法都不会被导出,所以它们是作为包内部的辅助方法或者说“私有方法”使用的。这更加说明了不需要为其指定接收类型的原因。
英文:
They are all actually equivalent. The receiver is passed into the method like every other argument. Since you need both types on hand no matter what (to call the method) it doesn't really matter on which it's defined. Personally, based on that, I would use the last of your three options. It makes more sense to me because in the other cases you're associating the method with one of those two types when really it requires both. That's just a matter of how you would like to organize your code though. There is no benefit from one to the other regarding performance or application behavior, they're all the same.
EDIT: One last point. None of those would be exported so it's a 'private' or rather a method used as a helper internal to the package. More reason to not have a receiving type for it.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论