This is decouple, a Go package and command that analyzes your Go code to find “overspecified” function parameters.
A parameter is overspecified, and eligible for “decoupling,” if it has a more-specific type than it actually needs.
For example,
if your function takes a *os.File
parameter,
but it’s only ever used for its Read
method,
it could be specified as an abstract io.Reader
instead.
When you decouple a function parameter from its too-specific type, you broaden the set of values on which it can operate.
You also make it easier to test. For a simple example, suppose you’re testing this function:
func CountLines(f *os.File) (int, error) {
var result int
sc := bufio.NewScanner(f)
for sc.Scan() {
result++
}
return result, sc.Err()
}
Your unit test will need to open a testdata file and pass it to this function to get a result.
But as decouple
can tell you,
f
is only ever used as an io.Reader
(the type of the argument to bufio.NewScanner).
If you were testing func CountLines(r io.Reader) (int, error)
instead,
the unit test can simply pass it something like strings.NewReader("a\nb\nc")
.
go install github.com/bobg/decouple/cmd/decouple@latest
decouple [-v] [-json] [DIR]
This produces a report about the Go packages rooted at DIR (the current directory by default). With -v, very verbose debugging output is printed along the way. With -json, the output is in JSON format.
The report will be empty if decouple has no findings. Otherwise, it will look something like this (without -json):
$ decouple
/home/bobg/kodigcs/handle.go:105:18: handleDir
req: [Context]
w: io.Writer
/home/bobg/kodigcs/handle.go:167:18: handleNFO
req: [Context]
w: [Header Write]
/home/bobg/kodigcs/handle.go:428:6: isStale
t: [Before]
/home/bobg/kodigcs/imdb.go:59:6: parseIMDbPage
cl: [Do]
This is the output when running decouple on the current commit of kodigcs. It’s saying that:
- In the function handleDir,
the
req
parameter is being used only for itsContext
method and so could be declared asinterface{ Context() context.Context }
, allowing objects other than*http.Request
values to be passed in here (or, better still, the function could be rewritten to take acontext.Context
parameter instead); - Also in handleDir,
w
could be anio.Writer
, allowing more types to be used than justhttp.ResponseWriter
; - Similarly in handleNFO,
req
is used only for itsContext
method, andw
for itsWrite
andHeader
methods (more thanio.Writer
, but less thanhttp.ResponseWriter
); - Anything with a
Before(time.Time) bool
method could be used in isStale, it does not need to be limited totime.Time
; - The
*http.Client
argument of parseIMDbPage is being used only for itsDo
method.
Note that,
in the report,
the presence of square brackets means “this is a set of methods,”
while the absence of them means “this is an existing type that already has the right method set”
(as in the io.Writer
line in the example above).
Decouple can’t always find a suitable existing type even when one exists,
and if two or more types match,
it doesn’t always choose the best one.
The same report with -json
specified looks like this:
{
"PackageName": "main",
"FileName": "/home/bobg/kodigcs/handle.go",
"Line": 105,
"Column": 18,
"FuncName": "handleDir",
"Params": [
{
"Name": "req",
"Methods": [
"Context"
]
},
{
"Name": "w",
"Methods": [
"Write"
],
"InterfaceName": "io.Writer"
}
]
}
{
"PackageName": "main",
"FileName": "/home/bobg/kodigcs/handle.go",
"Line": 167,
"Column": 18,
"FuncName": "handleNFO",
"Params": [
{
"Name": "req",
"Methods": [
"Context"
]
},
{
"Name": "w",
"Methods": [
"Header",
"Write"
]
}
]
}
{
"PackageName": "main",
"FileName": "/home/bobg/kodigcs/handle.go",
"Line": 428,
"Column": 6,
"FuncName": "isStale",
"Params": [
{
"Name": "t",
"Methods": [
"Before"
]
}
]
}
{
"PackageName": "main",
"FileName": "/home/bobg/kodigcs/imdb.go",
"Line": 59,
"Column": 6,
"FuncName": "parseIMDbPage",
"Params": [
{
"Name": "cl",
"Methods": [
"Do"
]
}
]
}
Replacing overspecified function parameters with more-abstract ones, which this tool helps you to do, is often but not always the right thing, and it should not be done blindly.
Using Go interfaces can impose an abstraction penalty compared to using concrete types. Function arguments that could have been on the stack may end up in the heap, and method calls may involve a virtual-dispatch step.
In many cases this penalty is small and can be ignored, especially since the Go compiler may optimize some or all of it away. But in tight inner loops and other performance-critical code it is often preferable to operate only on concrete types when possible.
That said, avoid the fallacy of premature optimization. Write your code for clarity and utility first. Then sacrifice those for the sake of performance not in the places where you think they’ll make a difference, but in the places where you’ve measured that they’re needed.