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?
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With