Rewriting a script for the Homebrew packĀage manĀagĀer taught me how Goāās design choicĀes align with platform-āready tools.
The problem with brew upgrade
By default, the [brew upgrade](https://docs.brew.sh/Manpage#upgrade-options-installed_formulainstalled_cask-)
comĀmand updates every forĀmuĀla (terĀmiĀnal utilĀiĀty or library). It also updates every cask (GUI appliĀcaĀtion) it manĀages. All are upgradĀed to the latĀest verĀsion ā major, minor, and patch. Thatās conĀveĀnient when you want the newest feaĀtures, but disĀrupĀtive when you only want quiĀet patch-ālevel fixes.
Last week I solved this in [ā¦
Rewriting a script for the Homebrew packĀage manĀagĀer taught me how Goāās design choicĀes align with platform-āready tools.
The problem with brew upgrade
By default, the [brew upgrade](https://docs.brew.sh/Manpage#upgrade-options-installed_formulainstalled_cask-)
comĀmand updates every forĀmuĀla (terĀmiĀnal utilĀiĀty or library). It also updates every cask (GUI appliĀcaĀtion) it manĀages. All are upgradĀed to the latĀest verĀsion ā major, minor, and patch. Thatās conĀveĀnient when you want the newest feaĀtures, but disĀrupĀtive when you only want quiĀet patch-ālevel fixes.
Last week I solved this in Perl with [brew-patch-upgrade\.pl](https://codeberg.org/mjgardner/brew-patch-upgrade/src/branch/main/brew-patch-upgrade.pl)
, a script that parsed brew upgrade
āās JSON outĀput, comĀpared semanĀtic verĀsions, and upgradĀed only when the patch numĀber changed. It worked, but it also remindĀed me how much Perl leans on implicĀit strucĀtures and runĀtime flexibility.
This week I portĀed the script to Go, the linĀgua franĀca of DevOps. The goal wasĀnāt feaĀture parĀiĀty ā it was to see how Goās design choicĀes map onto platĀform engiĀneerĀing concerns.
Why port to Go?
- Portfolio pracĀtice: Iām buildĀing a body of work that demonĀstrates platĀform engiĀneerĀing skills.
- Operational focus: Go is wideĀly used for toolĀing in infraĀstrucĀture and cloud environments.
- Learning by conĀtrast: Rewriting a workĀing Perl script in Go forces me to conĀfront difĀferĀences in error hanĀdling, type safeĀty, and distribution.
The journey
Error handling philosophy
Perl gave me try/ācatch (experĀiĀmenĀtal in the Perl v5.34.1 that ships with macOS, but since acceptĀed into the lanĀguage in v5.40). Go, famousĀly, does not. Instead, every funcĀtion returns an error explicitly.
Perl:
use v5.34;
use warnings;
use experimental qw(try);
use Carp;
use autodie;
...
try {
system 'brew', 'upgrade', $name;
$result = 'upgraded';
}
catch ($e) {
$result = 'failed';
carp $e;
}
Go:
package main
import (
"os/exec"
"log"
)
...
cmd := exec.Command("brew", "upgrade", name)
if output, err := cmd.CombinedOutput(); err != nil {
log.Printf("failed to upgrade %s: %v\n%s",
name,
err,
output)
}
The Go verĀsion is noisĀiĀer, but it forces explicĀit deciĀsions. Thatās a feaĀture in proĀducĀtion toolĀing: no silent failures.
Dependency management
- Perl:
[cpanfile](https://metacpan.org/dist/Module-CPANfile/view/lib/cpanfile.pod)
+ CPAN modĀules. Distribution means āāinstall Perl (if itās not already), install modĀules, run script.ā Tools like[carton](https://metacpan.org/dist/Carton)
and the[cpan](https://perldoc.perl.org/cpan)
or[cpanm](https://metacpan.org/dist/App-cpanminus/view/bin/cpanm)
comĀmands help autoĀmate this. Additionally, one can use furĀther toolĀing like[fatpack](https://metacpan.org/dist/App-FatPacker/view/bin/fatpack)
and[pp](https://metacpan.org/pod/pp)
to build more self-ācontained packĀages. But those are neiĀther comĀmon nor (except forcpan
) disĀtribĀuted with Perl. - Go:
[go\.mod](https://go.dev/doc/modules/gomod-ref)
+[go build](https://go.dev/doc/tutorial/compile-install)
. Distribution is a sinĀgle (platform-āspecific) binary.
For operĀaĀtional tools, thatās a masĀsive simĀpliĀfiĀcaĀtion. No runĀtime interĀpreter, no depenĀdenĀcy dance.
Type safety
Perl let me parse JSON into hashrefs and trust the keys exist. Go required a struct
:
type Formula struct {
Name string `json:"name"`
CurrentVersion string `json:"current_version"`
InstalledVersions []string `json:"installed_versions"`
}
The comĀpilĀer enforces assumpĀtions that Perl left implicĀit. That fricĀtion is valuĀable ā it surĀfaces errors early.
Binary distribution
This is where Go shines. Instead of telling colĀleagues āāinstall Perl v5.34 and CPAN modĀules,ā I can hand them a binaĀry. No need to worĀry about scriptĀing runĀtime enviĀronĀments ā just grab the right file for your system.
[homebrew-semver-guard-darwin](https://codeberg.org/mjgardner/homebrew-semver-guard/releases/download/v0.1.0/homebrew-semver-guard-darwin)
(Universal Binary for macOS)[homebrew-semver-guard-linux-amd64](https://codeberg.org/mjgardner/homebrew-semver-guard/releases/download/v0.1.0/homebrew-semver-guard-linux-amd64)
(Intel/āAMD 64-ābit binaĀry for Linux)[homebrew-semver-guard-linux-arm64](https://codeberg.org/mjgardner/homebrew-semver-guard/releases/download/v0.1.0/homebrew-semver-guard-linux-arm64)
(Arm 64-ābit binaĀry for Linux)
Available on the release page. Download, run, done.
Semantic versioning logic
In Perl, I manĀuĀalĀly comĀpared arrays of verĀsion numĀbers. In Go, I importĀed [golang\.org/x/mod/semver](https://pkg.go.dev/golang.org/x/mod/semver)
:
import (
golang.org/x/mod/semver
)
...
if semver.MajorMinor(toSemver(formula.InstalledVersions[0])) !=
semver.MajorMinor(toSemver(formula.CurrentVersion)) {
log.Printf("%s is not a patch upgrade", formula.Name)
results.skipped++
continue
}
Cleaner, more legĀiĀble, and less error-āprone. The library encodes the conĀvenĀtion, so I donāt have to.
Deliberate simplification
I didĀnāt port every feaĀture. Logging adapters, sigĀnal hanĀdlers, and edge-ācase diagĀnosĀtics remained in Perl. The Go verĀsion focusĀes on the core logĀic: parse JSON, comĀpare verĀsions, run upgrades. That restraint was intenĀtionĀal ā I wantĀed to learn Goās idioms, not repliĀcate every Perl flourish.
Platform engineering insights
Three lessons stood out:
- Binary disĀtriĀbĀuĀtion matĀters. Operational tools should be instalĀlable with a sinĀgle copy step. Go makes that trivial.
- Semantic verĀsionĀing is an operĀaĀtional pracĀtice. Itās not just a conĀvenĀtion for library authors ā itās a conĀtract that toolĀing can enforce.
- Goās design aligns with platĀform needs. Explicit errors, type safeĀty, and staĀtĀic binaĀries all reduce surĀprisĀes in production.
Bringing it home
This isnāt a āāPerl vs. Goā stoĀry. Itās a stoĀry about delibĀerĀate simĀpliĀfiĀcaĀtion, takĀing a workĀing Perl script and recastĀing it in Go. The aim is to see how the lanĀguageās choicĀes shape a soluĀtion to the same problem.
The result is [homebrew-semver-guard](https://codeberg.org/mjgardner/homebrew-semver-guard)
v0.1.0, a small but sturĀdy tool. Itās not feature-āfinished, but itās production-āready in the ways that matter.
Next up: Iām conĀsidĀerĀing more Go tools, maybe even Kubernetes for serĀvices on my home servĀer. This port was pracĀtice, an artiĀfact demonĀstratĀing platĀform engiĀneerĀing in action.
Links
- Original Perl script:
[brew-patch-upgrade\.pl](https://codeberg.org/mjgardner/brew-patch-upgrade/src/branch/main/brew-patch-upgrade.pl)
- Go release: homebrew-semver-guard v0.1.0
- Last weekās post: Patch-āPerfect: Smarter Homebrew Upgrades on macOS