Reputation: 7807
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?
Upvotes: 2
Views: 2154
Reputation: 9458
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 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 provide ftp
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 a gzip.Reader
which invokes an underlying File
. Anything that takes an io.Reader
can also take a gzip.Reader
, allowing you to substitute behavior without having to change the rest of your code)
I could keep going...
Upvotes: 3
Reputation: 48076
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.
Upvotes: 0