Golang
-
CLI First, GUI Never
I’ve been enjoying building CLI tooling and so here’s why your next app should be a CLI not a GUI.
CLI tools have longevity. If you design them for composability, there’s a good chance they’ll stick around because they’re much easier to plug into an existing ecosystem of other CLI tools. GUIs on the other hand come and go, as aesthetics change and as popular UI libraries rise and fall.
I’ll be honest, this is just a thinly veiled excuse for myself to explore building CLIs in different languages. I’ve built them in TypeScript with Bun and I’ve built them in Python, but TUIs in Go really changed the game for me on what is possible.
Tips for Building CLIs That Last
Output JSON Structured output is way easier to parse with scripts, and agents appreciate the additional context that JSON provides. If you’re building tools in 2026, you’re building them for humans and machines.
Wrap existing CLIs instead of re-implementing their APIs. You’re trading a raw API dependency for a versioned, maintained interface. Someone else is absorbing the upstream churn.
Prefer stdin/stdout over files where possible. This works better if you ever want to containerize your tool, and it plays nicely with Unix piping.
Logging matters. This kinda goes with JSON but any sort of logging is so important I’ll add it twice. Having some logging is non-negotiable, but structured logging really matters if you’re sending logs to a centralized provider.
Single binaries are way easier to distribute than a zip file or a bunch of code someone has to set up. It’s fairly straightforward to set up GitHub auto-releases, though there are some steps that can trip you up. One approach: auto-create a new patch version on every commit to main.
Three CLIs I Built (For Inspiration)
Here are some personal examples to hopefully inspire you to build your own:
-
lsm — A local secrets manager. Instead of
.envfiles sitting on disk, it decrypts and optionally injects secrets into your application runtime.lsm exec -- pnpm dev -
repjan — A Go TUI that wraps the
ghCLI to help you manage all your old repos. -
positive.help CLI — Personal tooling for managing a website entirely from the command line. No admin dashboard needed.
The Agentic Argument
In the age of agentic development, CLIs that your agents can call are incredibly useful. An agent can call a CLI a lot easier than they can click buttons in a GUI.
A GUI usually requires browser automation, maybe some scraping. It’s getting a bit easier now with APIs that return markdown from a site, but not everybody knows how to use those tools, and there’s usually a cost.
If you want a signal, look at the adoption curves of agent-focused CLI tools over the last six months. GitHub stars aren’t a perfect metric, but the direction is hard to argue with.
So this is your sign, you don’t need a framework. You don’t need a design system. If it’s good enough for
grep, it’s good enough for your tool./ Golang / Development / Cli / Tooling
-
-
Garbage Collection: How Python, JavaScript, and Go Clean Up After Themselves
It’s Garbage day for me IRL and I wanted to learn more about garbage collection in programming. So guess what? Now you get to learn more about it too.
We’re going to focus on three languages I work with mostly, Python, JavaScript, and Go. We will skip the rest for now.
Python: Reference Counting
Python’s approach is the most straightforward of the three. Every object keeps track of how many things are pointing to it. When that count drops to zero, the object gets cleaned up immediately. Simple.
There’s a catch, though. If two objects reference each other but nothing else references either of them, the count never hits zero. That’s a reference cycle, and Python handles it with a secondary cycle detector that periodically scans for these orphaned clusters. But for the vast majority of objects, reference counting does the job without any fancy algorithms.
JavaScript (V8): Generational Garbage Collection
Most JavaScript you encounter is running on V8… so Chrome based browsers, Node and Deno. V8 uses a generational strategy based on a simple observation: most objects die young.
V8 splits memory into two main areas:
- The Nursery (Young Generation): New objects land here. This space is split into two halves and uses a scavenger algorithm. It’s fast because it only deals with short-lived variables — and most variables are short-lived.
- Old Space (Old Generation): No, not the deodorant company. Objects that survive a couple of scavenger rounds get promoted here. Old space uses a mark-and-sweep algorithm, which is slower but handles long-lived objects more efficiently.
First it asks, “How long has this object been around?” New stuff gets the quick treatment, and anything that sticks around gets put out to the farm, to be delt where time is less of a premium. It’s a smart tradeoff between speed and memory efficiency.
Go: Tricolor Mark-and-Sweep
Go’s garbage collector also uses mark-and-sweep, but with a twist called tricolor marking. Here’s how it works:
- White objects: These might be garbage but haven’t been checked yet. Everything starts as white.
- Gray objects: The collector has reached these, but it hasn’t scanned their children (the things they reference) yet.
- Black objects: These are confirmed alive — the collector has scanned them and all their references.
The collector starts from known root objects, marks them gray, then works through the gray set — scanning each object’s references and marking them gray too, while the scanned object itself turns black. When there are no more gray objects, anything still white is unreachable and gets cleaned up.
Go’s approach is notable because it runs concurrently. This helps with latency while the GC is running.
Garbage, Collected
Each approach reflects the language’s priorities:
- Python optimizes for simplicity and predictability. Objects get cleaned up correctly when they’re no longer needed
- JavaScript optimizes for speed in interactive applications. Quick cleanup for short-lived objects, thorough cleanup for the rest
- Go optimizes for low latency. Concurrent collection is great for server side processes
References
- Design of CPython’s Garbage Collector — Python Developer’s Guide deep dive into reference counting and cycle detection
- Orinoco: Young Generation Garbage Collection — V8’s parallel scavenger and generational GC design
- A Guide to the Go Garbage Collector — Official Go documentation on the concurrent tricolor collector
/ Programming / Golang / Python / javascript / Garbage-collection
-
How Python and Kotlin provide structured concurrency out of the box while Go achieves the same patterns explicitly using errgroup, WaitGroup, and context.
/ Programming / Golang / Python / links
-
Introducing api2spec: Generate OpenAPI Specs from Source Code
You’ve written a beautiful REST API. Routes are clean, handlers are tested and the types are solid. But where’s your OpenAPI spec? It’s probably outdated, incomplete, or doesn’t exist at all.
If you’re “lucky”, you’ve been maintaining one by hand. The alternatives aren’t great either, runtime generation requires starting your app and hitting every endpoint or annotation-heavy approaches clutter your code. We should all know at this point, with manual maintenance, it will inevitably drift from reality.
What if you could just point a tool at your source code and get an OpenAPI spec?
Enter api2spec
# Install go install github.com/api2spec/api2spec@latest # Initialize config (auto-detects your framework) api2spec init # Generate your spec api2spec generateThat’s it. No decorators to add. No server to start. No endpoints to crawl.
What We Support
Here’s where it gets interesting. We didn’t build this for one framework—we built a plugin architecture that supports 30+ frameworks across 16 programming languages:
- Go: Chi, Gin, Echo, Fiber, Gorilla Mux, stdlib
- TypeScript/JavaScript: Express, Fastify, Koa, Hono, Elysia, NestJS
- Python: FastAPI, Flask, Django REST Framework
- Rust: Axum, Actix, Rocket
- PHP: Laravel, Symfony, Slim
- Ruby: Rails, Sinatra
- JVM: Spring Boot, Ktor, Micronaut, Play
- And more: Elixir Phoenix, ASP.NET Core, Gleam, Vapor, Servant…
How It Works
The secret sauce is tree-sitter, an incremental parsing library that can parse source code into concrete syntax trees.
Why tree-sitter instead of language specific AST libraries?
- One approach, many languages. We use the same pattern-matching approach whether we’re parsing Go, Rust, TypeScript, or PHP.
- Speed. Tree-sitter is designed for real-time parsing in editors. It’s fast enough to parse entire codebases in seconds.
- Robustness. It handles malformed or incomplete code gracefully, which is important when you’re analyzing real codebases.
- No runtime required. Your code never runs. We can analyze code even if dependencies aren’t installed or the project won’t compile.
For each framework, we have a plugin that knows how to detect if the framework is in use, find route definitions using tree-sitter queries, and extract schemas from type definitions.
Let’s Be Honest: Limitations
Here’s where I need to be upfront. Static analysis has fundamental limitations.
When you generate OpenAPI specs at runtime (like FastAPI does natively), you have perfect information. The actual response types. The real validation rules. The middleware that transforms requests.
We’re working with source code. We can see structure, but not behavior.
What this means in practice:
- Route detection isn’t perfect. Dynamic routing or routes defined in unusual patterns might be missed.
- Schema extraction varies by language. Go structs with JSON tags? Great. TypeScript interfaces? We can’t extract literal union types as enums yet.
- We can’t follow runtime logic. If your route path comes from a database, we won’t find it.
- Response types are inferred, not proven.
This is not a replacement for runtime-generated specs. But maybe so in the future and for many teams, it’s a massive improvement over having no spec at all.
Built in a Weekend
The core of this tool was built in three days.
- Day one: Plugin architecture, Go framework support, CLI scaffolding
- Day two: TypeScript/JavaScript parsers, schema extraction from Zod
- Day three: Python, Rust, PHP support, fixture testing, edge case fixes
Is it production-ready? Maybe?
Is it useful? Absolutely.
For the fixture repositories we’ve created—realistic APIs in Express, Gin, Flask, Axum, and Laravel—api2spec correctly extracts 20-30 routes and generates meaningful schemas. Not perfect. But genuinely useful.
How You Can Help
This project improves through real-world testing. Every fixture we create exposes edge cases. Every framework has idioms we haven’t seen yet.
- Create a fixture repository. Build a small API in your framework of choice. Run api2spec against it. File issues for what doesn’t work.
- Contribute plugins. The plugin interface is straightforward. If you know a framework well, you can make api2spec better at parsing it.
- Documentation. Found an edge case? Document it. Figured out a workaround? Share it.
The goal is usefulness, and useful tools get better when people use them.
Getting Started
go install github.com/api2spec/api2spec@latest cd your-api-project api2spec init api2spec generate cat openapi.yamlIf it works well, great! If it doesn’t, file an issue. Either way, you’ve helped.
api2spec is open source under the FSL-1.1-MIT license. Star us on GitHub if you find it useful.
Built with love, tree-sitter, and too much tea. ☕
/ DevOps / Programming / Openapi / Golang / Open-source