Tag: error handling

  • Porting from Perl to Go: Simplifying for Platform Engineering

    Porting from Perl to Go: Simplifying for Platform Engineering

    Rewriting a script for the Homebrew pack­age man­ag­er taught me how the Go pro­gram­ming lan­guage’s design choic­es align with platform-​ready tools.

    The problem with brew upgrade

    By default, the brew upgrade 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, 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 + CPAN mod­ules. Distribution means ​“install Perl (if it’s not already), install mod­ules, run script.” Tools like carton and the cpan or cpanm com­mands help auto­mate this. Additionally, one can use fur­ther tool­ing like fatpack and pp to build more self-​contained pack­ages. But those are nei­ther com­mon nor (except for cpan) dis­trib­uted with Perl.
    • Go: go.mod + go build. 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.

    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:

    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:

    1. Binary dis­tri­b­u­tion mat­ters. Operational tools should be instal­lable with a sin­gle copy step. Go makes that trivial.
    2. 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.
    3. 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 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