So here's what hopefully won't be considered a trolling question: I have seen a lot of "100% Go" projects over the past several years and that's usually presented as a big feature. Some pretty trivial things have been redone as brand new in Go, and then suddenly gain lots of attention. What is so magical about a project written in Go vs C, Python, Ruby, Rust, JS, etc.? As a user of the software I won't care what it's written in, if it's done well. If it's done poorly, I am much more likely to look for better alternatives than to fix it (if only I had about 240 hours in a day...), so what's the advantage?
To me, an advantage in usability is having a PPA with properly built .deb packages. If I have to use a language-specific package manager that I don't already use regularly, you've likely lost me, unless I really need this functionality. If it doesn't come with a proper daemon mode (correct forking, PID file support, proper file or syslog logging), sample config file, man page, or an init file, that's even worse. I am much less likely to use this in any type of "production" environment if I have to maintain those pieces myself. Running things in a screen session is so "I'm running a Minecraft server".
That is not to criticize your work. You've done a great job! serve2d looks very interesting and I might actually have to give it a try sometime.
I think the the "100% Go" stuff is appealing in that you just have one file that works across OSes, with minimal screwing around.
For things that become part of the OS, yes, I'd rather they come via some install approach that includes the necessary integration. But for anything else, I think a lot of our packaging approaches are dedicated to saving disk space and RAM, which is something that matters way less to me now than it did 15-20 years ago when CPAN and APT were designed. In 2000, disk prices were circa $10/GB [1]; now we're looking at $0.50/GB of zippy SSD [2] or $0.03/GB of spinning rust [3]. RAM is similarly about 2 orders of magnitude cheaper. [4] Given that, it makes a lot more sense to burn space to minimize the chance of a library version conflict or other packaging issue.
Another thing that has changed greatly is the pace of updates. 15-20 years ago, weekly releases sounded impossible to most. Now it's common, and some places are releasing hourly or faster. [5] Thanks to things like GitHub, the whole notion of a release is getting hazy: I see plenty of things where you just install from the latest; every merge to master is in effect a new release.
Given that, I think both Go and Docker are pioneering approaches that are much more in sync with the current computing environment. I'm excited to see where they get to.
The number 1 selling point of something written in Go is that it's much easier to package. The result of a compilation is a standalone binary that can be copy-pasted everywhere, as long as the architecture matches what was input at compilation time. This means:
- no more having to deal with dependencies at packaging time, which makes packagers' job simpler because all they have to care is the one and only standard way to retrieve dependencies and build the binary. (Much like the standard way of doing things in C would be ./configure && make && make install, with the added bonus point that the dependencies are also taken into account). This also means that there's a higher chance that the software will be packaged in the distribution of your choice, because the bar is lower
- no more having to deal with dependencies at runtime, because each binary has everything it needs inside of itself. In practice this means "scp as a deploying method". It's an even lower common denominator than packages.
> If it doesn't come with a proper daemon mode (correct forking, PID file support, proper file or syslog logging), sample config file, man page, or an init file, that's even worse.
This is orthogonal to the choice of programming language, though. On top of that, I believe the application shouldn't deal with forking, it's the job of your supervision system to deal with daemons. All an application has to do is log whatever happens on STDERR and let the system handle that.
How exactly do you not have to worry about dependencies? Does the Go SDL include every routine you could possibly need and is always 100% correct and bug free? If you build your static binary today and tomorrow there is a vulnerability in your libssl dependency of choice, don't you now have to recompile and redistribute a new binary? Seems like a terrible and insecure way to do things. Instead of a distro developer worrying about security updates, you have signed up to do that yourself.
As for logging, there are loads of logging libraries that support both stdout and file logging. My policy is to support both for my project and it has been almost no burden so far (in Python and in C). Not everything is containers, and having a feature like logging does not mean it cannot be used in a container.
> If you build your static binary today and tomorrow there is a vulnerability in your libssl dependency of choice, don't you now have to recompile and redistribute a new binary
Technically there's only one ssl library you should use, it's the standard one. This doesn't change your overall point that when a part of the program must be upgraded, the whole binary must be upgraded as well and re-deployed, which I totally agree with. If your software is a server that you host yourself and you have full control over the deployment chain, as is the mindset behind Go, then re-deploying a dependency or re-deploying a binary is more or less the same.
Regarding logging, I'm really partial to the approach advertised by 12 factors (http://12factor.net/logs): let your software handle the business, let the supervisor handle the software's lifecycle, and handle the logfiles outside of the software, because there are factors specific to the hosting machine that in my opinion shouldn't be the concern of the software.
Not sure what you mean by standard ssl library. There's OpenSSL, GNU TLS, LibreSSL, and a number of other implementations. If you mean the one that comes with the OS, then wouldn't that by dynamic linking?
Re-deploying the binary, and getting the updated binary are two different things. When OpenSSL has a vulnerability, they announce it after the fix is out, distro maintainers release updated packages, I run `apt-get update && apt-get upgrade` or equivalent. It is on OpenSSL and the distro maintainers to release the update, and on me to apply the update.
When we are talking about static linking, it's now suddenly on the software developer to release a new binary or on me to rebuild the binary I have from source. Now I have to keep track of (a) which dependencies each project uses, and (b) which vulnerabilities come out. Not being familiar with Go, does it have such dependency tracking framework where I can update all packages where dependencies have been updated? Of course once I know that I need to perform an update, it doesn't matter if I run `apt-get upgrade` or `go build foo`.
As for logging, I advocate doing both. I really should make a separate blog post about it, but here's what I expect a well-behaved daemon to do:
- Always start in foreground and log to stdout (otherwise it seems like it exited without any output)
- Use the -d and --daemon flags to go into background
- Use the -p and --pid options for specifying the PID file
- Use the -l and --log options for specifying the log file location (if not specified or is - use stdout)
- If uses a config file, use -c or --config for the location of the configuration file. Default to standard OS location.
This way all possible modes are supported (running under a supervisor process, in a container, as a stand-alone daemon, or in the foreground while in development/testing/experimentation), and it is very easy, even in C to write software that meets these requirements.
> This also means that there's a higher chance that the software will be packaged in the distribution of your choice, because the bar is lower
Static linking and bundling of dependencies is a no-no in most distributions. If anything, the Go model is a headache for package maintainers to deal with.
Have you ever actually tried producing a statically linked C/C++ binary? I've been programming in C/C++ for 10+ years. Static linking is a huge pain. My latest efforts have led me to create holy build boxes inside carefully controlled Docker-based environments just to be able to produce binaries that work on every Linux. With Go you can just run a single command to cross-compile binaries that work everywhere. Minimal setup required, no expert knowledge required.
> Have you ever actually tried producing a statically linked C/C++ binary?
Many times actually, I prefer to deploy just one file whenever I can.
> just to be able to produce binaries that work on every Linux
That's a Linux design/decision thing. Linux binary compatibility is ... well ... challenging. In Windows is not hard at all (Not sure how is it in MacOS since I have worked mostly for iOS in Apple's world).
That's a very weird way to look at it. Static linking was there before everything else. gcc/ld and, well, any other C/C++ toolchain can do that as well. There is a reason this isn't usually done. It's like you are trying to spin a bad thing into something good.
The reason this isn't usually done is that executable size was significant relative to storage capacity up until the early 2000s or so, and people tried to economize by deduping common parts of their executables via shared libraries / DLLs. This worked well enough to catch on, but came with an extremely high cost in added complexity, and over the years a whole layer of additional infrastructure was created in order to manage it. The industry progressed, storage capacity grew dramatically, and executable sizes stopped mattering, but the use of shared libraries / DLLs continued out of inertia. As time passed, people started asking - why are we doing all this? And some of them invented a reason, which was the idea that one could swap out pieces of existing executables after installation, and thereby fix security problems in an application without needing to involve the application's developer in the process. This works about as well as you'd expect if you had spent years trying to fit all the rough edges of various third-party libraries together with varying degrees of success, but the idea caught on as a popular post-hoc justification for the huge layer of complexity we're all continuing to maintain long after its original justification became obsolete.
As is no doubt obvious from my tone, I'm not buying it and am very happy to see signs of a pendulum-swing back toward static linking and monolithic executables.
It's hard to be sure why exactly shared libraries and dynamic linking appeared. Your explanation about reducing file size for smaller HDD and RAM footprint is probably one of the reasons, but I don't believe it's the only one - I don't remember many shared libraries from MSDOS days (where with 2MB of RAM and 40MB of HDD, storage was really scarce). In fact - I don't remember any libraries! To run Doom you just borrowed the floppies from a friend and it would 100% work. The same for Warcraft 1.
I believe it's more similar to Database Normalization from RDBMS world than to anything else. And the most important objective of Database Normalization is considered "Freeing the database of modification anomalies".
My own experience with shared libraries is pretty positive. I have fixed OpenSSL vulnerabilities many times by just updating the OpenSSL library and restarting all services. Compared to my own experience with docker where after waiting for a few weeks I had to change my base images (as nobody was updating them) updating just a single shared library and having the vulnerability fixed is way easier!
This, of course, is true if those who maintain the software you use do care about backwards compatibility (which tends to be true for the "boring" stuff and false for the stuff considered "cool" - looking at you nodejs library developers who break my build at least once a month).
There are other important downsides to static linking. Namely, critical security updates to shared components. It's better to have to update your tls library once per system, than to update every app that came with it. And that's the best case scenario where the developer notices and the app is actually repackaged.
Well, you wouldn't want every executable to copy some unknown version of OpenSSL, and you'd get into all kinds of problems if you had several different versions of glibc around.
Just because you don't like the single binary that works everywhere, doesn't mean that others find it a problem. One approach doesn't fit every possible situation.
Even if you think the wasted ram and the security issues isn't a problem, why is that an argument especially for Go, when almost every other language can be built into a single static file as well?
I've done it for Windows, Linux and Mac before. Note that these solutions freeze the Python side of things but do not freeze the platform side. For example they do not include the system libraries. Consequently running the frozen python app on a system that is a different distro, older or newer OS version, or has different system packages installed often leads to the frozen python app not being able to start.
A staid, solid, conservative outlook, a good perspective for others to realize is out there. I would say that concerns like daemon mode and logging are a lot less in vogue these days- a program ought concern itself with running, and outputting to stdout, and if you have needs past these it's expected you have tooling you can deploy that makes that happen.
Daemonization is at least a fairly standard feature, but with logging there's so many people with such varied concerns that getting fancy, trying to meet people's many needs, can lead to a lot of program bloat very quickly. Instead of going at these on a case-by-case basis, and now that we are more container-centric, it makes sense to run in the foreground and put your output on stdout, let the rest of the system support that utterly uncomplex pattern.
I think we're learning pretty quickly that 100% Go (or Rust, or Python, or Perl, or OCaml, or ...) is a good idea for security. Especially is you're dispatching between ssh and ssl services.
Go has advantage over scripting in speed, over C/C++ in memory management, over strict FP languages in popularity, and over Rust in being stable and known for longer.
This advantages of serve2 is that it is 100% go and can be integrated and used without any extra dependencies or multi-setup :)