I have some projects that have evolved into a big mess. Code spread about packages organically, and named things that don’t entirely make sense in hindsight. Some of this is down to me being new at Go and my unfamiliarity with good patterns to lay out code within it. Some of that is down to me simply not being sure how the code fitted together until it was more complete.
I wanted to refactor it, and Go has a good reputation for tools that operate on source code.
The first challenge was to figure out what I had in there. From my point of view an overview of packages/files and types seemed like it would give me a decent idea of what I was looking at. I didn’t spot any tools that appeared to do what I wanted out of the box (although my google fu was failing me as I struggled to think up search terms that pulled up things that weren’t for other topics like simply writing Go code). After experimenting with ripgrep to simply grep for type as a keyword I realised what I was missing in terms of information. I ended up coming up with a simple tool using the golang.org/x/tools/go/packages library.
Often when people look at the Go source code they mention the ast library which is a great tool, but this is kind of a level above, simply looking at what packages you have, and doing some analysis. For what I wanted, this appeared to do all I needed to (and may well have been using the ast under the hood). The scopes appeared to provide the type info I wanted, and while I couldn’t seem to access the info more easily than as a string which felt a bit clunky, a bit of simple fudging got me the sort of output I wanted.
This generated a markdown document noting the layout of the types in the packages. I then created 2 copies of that document, one labelled current, then another labelled planned. This allowed me to get a feel for where the source code files should go and how well the names fit.
I moved various packages about wholesale, simply moving whole source files around. With that I then used shell fu to rename reams of files adjusting their imports and if necessary hand editing the package names.
For instance combining ripgrep and perl to rename the imports:
rg internal/mysql/packet --type go -l | xargs perl -p -i -e 's|internal/mysql/packet|pkg/mysql/packet|g'
I did this in discrete commits, checking the build/lint/test suite as I went. Then it came to renaming some of the types, and there I was able to make use of gopls to rename names, and it would update all the right places in the code.
At first I did this from the command line because I didn’t realise it was integrated in my editor like this:
gopls rename -d internal/streamfactory/factory.go:16:6 StreamFactory
You specify the filename, line, columns (all nicely visible in your editor footer probably) and then the word to change the identifier to.
Then I realised that vim-go had indeed added support for rename via gopls, and I just needed to turn it on:
let g:go_rename_command = 'gopls'
While researching all this I discovered an unrelated but neat utility/feature,
:GoAddTags that adds
`json:"name"` tags to struct elements (you might need to install the external companion tool for that).
With the editor integration I can move my cursor to the start of something to rename and then do
:GoRename newIdentifier and it will update all the code accordingly.