Shifting More to Go
Sure, I’m behind the times. I think more people are moving from Go to Rust lately, but here’s why I use the languages I do and why Go is swallowing more and more of those use cases.
I fit in an odd space in development. I’m an ops guy. I don’t write business applications and when I do low-level stuff I’m writing in C. That’s not changing.
I do write Java, etc. but only as small bad examples to reproduce production problems or illustrate the over used “antipatterns”.
But mostly I write integrations, system tools, and CLIs.
My decision tree used to go Shell -> Python -> C as increased complexity was needed. Now it’s not that simple. Here are when I pick which language and why.
I still write a lot of shell scripts. They’re great for simple tasks and text processing. Pipes are beautiful. The language (if you can call it that) is an ugly evolved vs designed, but insanely practical beast.
I mostly stick to POSIX. I don’t care if it’s ksh88, ksh93, bash, zsh, or whatever. It’s an old habit, but it usually helps me steer clear of bugs. I’m showing my old Unix guy side here.
I do sometimes resort to Bashisms and GNUisms when I know I’m only
targeting Linux > some certain point. Expecting to be able to do arrays
consistently, and having things like
grep -q and
sed -i is nice.
But it’s very easy to get to the point of having something that either is jumping through hoops or unreadable by anyone with less than many years of experience here.
So beyond simple tasks, or where it’s just necessary to target a wide variety of systems (like every *nix host in my company), I don’t go there much anymore.
I love Python. The language is simple and expressive. I’ve found that even folks unfamiliar with the language are able to pick up and maintain my code pretty easily. I rarely care enough about performance for its issues to matter.
But reproducing the environment just sucks. Pipenv made things better, but even then I have to
- ensure I have a reasonable version of Python (say 3.6+)
- ensure I have Pipenv
- install all my dependencies
- slap together something to actually run the thing
I’m really only using it at this point for 2 things:
- The annoying times I have to make a web app, because I still prefer avoiding JS frameworks and do more server side with Flask and Jinja. I try to host these on a PaaS to make the above steps simpler than deploying to a VM.
- When I need to do a lot of mathy stuff. Usually usage/licensing projections and things like that to make finance people happy. All the ML and stat people using Python have made this easy.
One callout, though. I hate the way ML/math people write code. It’s barely readable. Way too many single letter variables, aliasing imports, and so on. I hope one day that field develops to the point where they actually expect someone to need to maintain their work after they move on to the next thing. It’s almost as bad as PERL that got dumped on me.
Aside: I dislike C++/Java/C#
I don’t really touch traditional OO languages. The basic premise of OO code reusability, etc. just never panned out. The result is typically overly complex code with a lot of boilerplate and the need to flip between so many files to see how something actually works.
And worse than the actual languages was how their usage, especially in “enterprise” development evolved. Getters and setters for everything. Mixing Interfaces, abstract classes, and inheritance. 1000 ways to defensively write abstract code for something someone might want to do one day, but never does that just adds unnecessary complexity.
Add to that magical frameworks and generics. Spring, JEE, etc. magically plug this class in over there assuming the incantation is performed correctly. Generics have some use cases, I will admit, but are so abused and generally break the whole type safety, causing issues that should be found at compile time at runtime. If I wanted that I’d write in something much more ergonomic like Python or Ruby.
Aaannnnd then you have religious adherence to Design Patterns as handed down from God by the Gang of Four. It’s usually awkward, unnecessary, and again, adding complexity to defend against a possible use cases that never happen. If the world changes, just change the code then. Don’t make it difficult now. The only Gang of Four I love is the band. Entertainment is a masterpiece.
Finally, they’re just complicated languages. A bajillion keywords, constant addition of new syntax to support the new cool thing from other languages. Rust falls in the language complexity problem too, but I think there’s good reason there with the safeties it provides for low-level C replacement activities.
I do write some C. I like C. It was my first real programming love. K&R was inspirational. But it is a giant foot gun. Any sufficiently large codebase will have regular bugs (buffer overflows and such) or bizarre undefined behavior. And folks can walk into requiring particular compilers quickly by being clever. And the C preprocessor is a dark tempting hole.
But usually my C-ing is where it just makes the most sense to directly use C dependencies or modifying/maintaining something already in C. I’ve played with things like integrating C and Python, but it’s just not worth it unless someone else has done it for you.
My shift to Go
I really do love writing Go. It’s enjoyable. It’s the things I love about C with less footguns. I like the concurrency model. I love the way structs and interfaces provide everything I want from OO, but none of the baggage.
Then combine that with one of the bigger selling points: I get binaries I can just copy and run. No setting up a runtime environment. I even cross compile for other OSes and architectures easily.
The 3 things I typically do for “production” releases are
- Statically compile to avoid glibc compat issues
- Don’t include debug symbols to drop binary size
upxto compress the binary
You can see from 2 and 3, one of the bigger problems is how large the binaries get. Sometimes it just doesn’t matter. But when I’m targeting a lot of servers, then size and bandwidth matter. Or when I’m building a toy project for a small system like a Raspberry Pi, then also size matters.
The one thing that keeps me using shell in places where I’m just targeting
x86-64 Linux is the lack of
epoll() in older kernels. There’s no real way
around that. It makes me sad. I’ve had to write 1000 line shell scripts where
a 500 line Go program would be cleaner and easier.
I also love that, unlike say Java or C#, it’s more like C and Python where I can use things with normal Unix semantics. Most syscalls are represented sanely. Sockets make sense.
And then writing something like a webserver with just
net/http or Gin is nice
like in Python. Writing an http client is ergonomic. Dealing with JSON is
nice. All those modern expected things are usually out-of-the-box. When you
need to do something fancier, there’s usually a widely used library to support
catch. I like being able to pass by reference.
I like the ease of multiple return codes. I like the static typing catching
nearly all my issues before I compile. I love the opinionated formatting (I
black with Python now, because I’m so in love with the idea).
I do dislike the baked-in logging framework. I’ve played with a few, and am
zerolog right now. I like log levels, and optionally having JSON
output is really needed when log aggregation is being used but I also like
being able to NOT use it when I’m working on my laptop or a CLI.
I dislike the
flag library. I still use it for simple cases, but use
for more sophisticated stuff, even though it’s usually overkill for that.
I like just using
net/http for writing a Prometheus exporter but moving up to
Gin for a more sophisticated web service.
It’s that nice middle ground between my old love of C and the ergonomics of Python. I don’t have 20+ years of experience with it like I do with C, shell, and Python. But I’m having to fall back to the docs less and less.
There still are some warts. Dependency management is getting better with Go
modules, but could be better still. The binary size thing can be annoying.
The immaturity of 3rd party libraries can be a pain for long-term projects
where whatever is cool at the time you start is deprecated 2-3 years later.
I’ve been bit by
kingpin, and other random libraries.
I should say, I haven’t limited myself to these in the past. I’ve done plenty of real and toy code in other languages. A bunch of different LISPs, plenty of Ruby (still in production use), some PERL (shudder), Pascal & Delphi way back.
I’ll try more. I’ve false started on Erlang, Haskell, and Rust. I feel every time I even get the basics down on one of these, it helps stretch my brain a bit and write better code in the languages I use daily.
And I always try to write idiomatic code in whatever I’m using. The point is to leverage the tool, not force the tool to be something it’s not. It kills me to see a Python app written as Java or something that’s barely C++, or the worst, someone turning a strongly typed language into a “stringly” typed language.
When picking up something new, free your mind and your ass will follow. Sorry, went Parliament there. No, but use it as an opportunity to learn a new approach not force it into what you already know. It’s hard, but it’s enjoyable after you get over the curve, and it’ll make you better at what you do.
This is falling apart at the end like a bad comedy. Sorry.
But I’m finding that Go is swallowing a big set of use cases I used other languages for. It’s a relatively small language, covers a lot of ground, is safe, and easy to deploy. And I really do enjoy using it.
It strikes a nice balance for me avoiding the run time gotchas of a scripting language or OO languages with magical inversion of control frameworks. But it’s low boilerplate and provides easy native feeling access to the Unix operating system I’m used to and leverage so much as an operations/systems guy.
It feels both modern and old school. I still can get a lot better at it, but I’m having fun.