DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Event-Driven Pipelines With Apache Pulsar and Go
  • DevOps and Platform Engineering Readiness Checklist: Everything Needed for a Scalable, Secure, High-Velocity Delivery Platform
  • Architecting an Embedded Efficiency Layer: A Platform Deep Dive into Day-Two Operational Tuning
  • Product-Led Software Delivery: Intelligent Platforms for DevOps at Scale

Trending

  • Building a Zero-Cost Approval Workflow With AWS Lambda Durable Functions
  • S3 Vectors: How to Build a RAG Without a Vector Database
  • LLM Agents and Getting Started with Them
  • Architecting an Embedded Efficiency Layer: A Platform Deep Dive into Day-Two Operational Tuning
  1. DZone
  2. Coding
  3. Languages
  4. Porting From Perl to Go: Simplifying for Platform Engineering

Porting From Perl to Go: Simplifying for Platform Engineering

Rewriting a script for the Homebrew package manager taught me how the Go programming language’s design choices align with platform-ready tools.

By 
Mark Gardner user avatar
Mark Gardner
DZone Core CORE ·
Oct. 16, 25 · Analysis
Likes (2)
Comment
Save
Tweet
Share
3.7K Views

Join the DZone community and get the full member experience.

Join For Free

The Problem With the brew upgrade Command

By default, the brew upgrade command updates every formula (terminal utility or library). It also updates every cask (GUI application) it manages. All are upgraded to the latest version — major, minor, and patch. That’s convenient when you want the newest features, but disruptive when you only want quiet patch-level fixes.

Last week, I solved this in Perl with brew-patch-upgrade.pl, a script that parsed brew upgrade’s JSON output, compared semantic versions, and upgraded only when the patch number changed. It worked, but it also reminded me how much Perl leans on implicit structures and runtime flexibility.

This week I ported the script to Go, the lingua franca of DevOps. The goal wasn’t feature parity — it was to see how Go’s design choices map onto platform engineering concerns.

Why Port to Go?

  • Portfolio practice: I’m building a body of work that demonstrates platform engineering skills.
  • Operational focus: Go is widely used for tooling in infrastructure and cloud environments.
  • Learning by contrast: Rewriting a working Perl script in Go forces me to confront differences in error handling, type safety, and distribution.

The Journey

Error Handling Philosophy

Perl gave me try/catch (experimental in the Perl v5.34.1 that ships with macOS, but since accepted into the language in v5.40). Go, famously, does not. Instead, every function 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 version is noisier, but it forces explicit decisions. That’s a feature in production tooling: no silent failures.

Dependency Management

  • Perl: cpanfile + CPAN modules. Distribution means “install Perl (if it’s not already), install modules, run script.” Tools like carton and the cpan or cpanm commands help automate this. Additionally, one can use further tooling like fatpack and pp to build more self-contained packages, but those are neither common, nor (except for cpan) distributed with Perl.
  • Go: go.mod + go build. Distribution is a single (platform-specific) binary.

For operational tools, that’s a massive simplification. No runtime interpreter, no dependency dance.

Type Safety

Perl let me parse JSON into hashrefs and trust the keys exist. Go required a struct:

Go
 
type Formula struct {
  Name              string   `json:"name"`
  CurrentVersion    string   `json:"current_version"`
  InstalledVersions []string `json:"installed_versions"`
}


The compiler enforces assumptions that Perl left implicit. That friction is valuable — it surfaces errors early.

Binary Distribution

This is where Go shines. Instead of telling colleagues “install Perl v5.34 and CPAN modules,” I can hand them a binary. No need to worry about scripting runtime environments — just grab the right file for your system.

  • homebrew-semver-guard-darwin (Universal Binary for macOS)
  • homebrew-semver-guard-linux-amd64 (Intel/AMD 64-bit binary for Linux)
  • homebrew-semver-guard-linux-arm64 (Arm 64-bit binary for Linux)

These are available on the release page. Download, run, done.

Semantic Versioning Logic

In Perl, I manually compared arrays of version numbers. In Go, I imported golang.org/x/mod/semver:

Go
 
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 legible, and less error-prone. The library encodes the convention, so I don’t have to.

Deliberate Simplification

I didn’t port every feature. Logging adapters, signal handlers, and edge-case diagnostics remained in Perl. The Go version focuses on the core logic: parse JSON, compare versions, and run upgrades. That restraint was intentional — I wanted to learn Go’s idioms, not replicate every Perl flourish.

Platform Engineering Insights

Three lessons stood out:

  1. Binary distribution matters. Operational tools should be installable with a single copy step. Go makes that trivial.
  2. Semantic versioning is an operational practice. It’s not just a convention for library authors — it’s a contract that tooling can enforce.
  3. Go’s design aligns with platform needs. Explicit errors, type safety, and static binaries all reduce surprises in production.

Bringing It Home

This isn’t a “Perl vs. Go” story. It’s a story about deliberate simplification, taking a working Perl script and recasting it in Go. The aim is to see how the language’s choices shape a solution to the same problem.

The result is homebrew-semver-guard v0.1.0, a small but sturdy tool. It’s not feature-finished, but it’s production-ready in the ways that matter.

Next up: I’m considering more Go tools, maybe even Kubernetes for services on my home server. This port was practice, an artifact demonstrating platform engineering in action.

Links

  • Original Perl script: brew-patch-upgrade.pl
  • Go release: homebrew-semver-guard v0.1.0
  • Last week’s post: Patch-Perfect: Smarter Homebrew Upgrades on macOS
Go (programming language) Perl (programming language) platform engineering

Published at DZone with permission of Mark Gardner. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Event-Driven Pipelines With Apache Pulsar and Go
  • DevOps and Platform Engineering Readiness Checklist: Everything Needed for a Scalable, Secure, High-Velocity Delivery Platform
  • Architecting an Embedded Efficiency Layer: A Platform Deep Dive into Day-Two Operational Tuning
  • Product-Led Software Delivery: Intelligent Platforms for DevOps at Scale

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook