Structs as mutable interfaces in Go
This is one of my favorite and underrated patterns in Go.
Let’s assume a format based on Lua tables, let’s just call it LTRN (Lua Table Resource Notation) and I was imagining a mock value generator for that format.
Now is the perfect time to use a struct of function types!
Imagine we have a simple spec for a property in our imaginary resource:
type LtnSpec struct {
Type string // "string", "int", "bool", "table"
Default any
}
Now we want to generate mock data for that primitive types:
func generate(s LtnSpec, generator Generator) any {
switch s.Type {
case "string":
return generator.String(10)
case "int":
return generator.Number(0, 100)
case "bool":
return generator.Bool()
}
return nil
}
The generator would be a concrete struct that contains functions that we can swap as we like. We need a seam to insert our mock data during testing. Injecting a struct of functions makes this code easily testable.
Why, oh why, is this not an interface, I hear you ask?
My argument here is that fill is an un-exported function. It won’t be used by 3rd party developers.
Let us take a look at how the struct looks:
type randStringFunc func(length int) string
type randNumberFunc func(min, max int) int
type randBoolFunc func() bool
type Generator struct {
String randStringFunc
Number randNumberFunc
Bool randBoolFunc
}
This is very flexible for low state structs that you want to heavily test. Ideally it would not have any fields at all. (Please don’t store your db connection in the generator!)
Back to my argument: The advantage of using a struct over an interface. When we implement the interface for a test mock we have to implement every single function even though we might only need one of them for the particular test. For large interfaces, this sucks! Add that to the reasons why large interfaces are generally discouraged and not idiomatic Go.
With a struct with function types we can provide only the functions that we actually need. To be honest, function types are reference types. But for our purpose we can think of them as pointers. They can be nil! This saves us from writing a lot of unnecessary code in our tests. (don’t forget to check you nils or you might run into panic)
func main() {
fixedStringGenerator := Generator{
String: func(length int) string {
return "TESTSTRING"
},
}
x := generate(LtnSpec{Type: "string"}, fixedStringGenerator)
fmt.Println(x.(string))
}
My heuristic:
- Standard Interfaces are for defining API Contracts (what a library provides)
- Structs of Functions are for defining operational dependencies (what a library needs internally to work).
It is clean, easy to read and also fast to write.
What do you think? Would you prefer an interface or maybe just hardwire the functions without injection? Let me know!