From C I Go
A low level journey
All through my undergrad, I struggled with understanding why C was the programming language chosen to teach 21st century electronics and telecommunications engineering students how to develop code for embedded systems.
Out there, in the real world, Rust was just popping up and seemed an interesting bet for systems programming, given it promised to maintain efficiency while adding memory safety. Single-board computers capable of running Linux, such as the BeagleBoard, were already a thing, but then came the new and inexpensive Raspberry Pi’s where Python became the standard choice for every DIY project.
Of course the need, and available tools, to cross-compile firmware imposed the use of C but this was only actually needed for a small number of projects.
Every time we would ask “why C?” the teachers would all reply, as probably it was said unto them: “it will give you the basics” or “it will help you think like the machine”.
So there we were left struggling with segmentation faults, memory leaks and double frees, trying to make our programs do what we wanted them too without destroying the underlying system, focusing sometimes more effort on fixing an unintended side effect than on the application’s logic or structure. But well, I’m not going to rant about how engineering course’s syllabuses don’t keep up with industry, or open-source, tools and needs so let’s skip that part.
All and all I ended up working as a systems engineer, programming applications in C as my day to day job, frequently banging my head against the walls because of those same segmentation faults and double frees and losing 2 or 3 days reorganizing my code to avoid such conundrums. But I had an amazing team of experienced and highly stubborn engineers that helped me sail through those perilous seas using code organization, low-level reasoning and, eventually, being able to catch risky patterns before even getting the code compiled.
I now kind of understand what my teachers meant but it took too much effort to get to a level of proficiency in that language in order to have confidence in the code I was producing.
Don’t get me wrong:
C is fast.
What you pay by using pointer arithmetic and memory allocation you gain with speed. Every low level interaction is made with small overhead.
C was made for UNIX programming.
Linux is running on millions of machines, and it was written in C, so just the standard library can get you working right on it and event the macOS is UNIX based.
C gets you close to the metal.
Given the Linux kernel is open sourced, any developer can perform low level optimization and bug fixing, contributing to the community, and even create new drivers and kernel modules for innovative hardware interactions.
C is old.
There’s a lot code written in C so there’s a high chance that someone already solved the issue you’re trying to solve. There’s a lot of bad and nonsensical code out there, but there are a lot of long running projects with great development practices such as cURL or even the Linux kernel.
More recently I got the chance to leave that cycle for a while and started a new project, still working on system applications, but now programming in Go.
Having yet to overcome C in the IEEE Spectrum programming language ranking, and barely passing it in the top fifteen most popular languages on GitHub, Go’s adoptance has steadily risen over the last 10 years, reaching over a million users, having lead to highly praised projects such as Prometheus, docker and Kubernetes.
Go is typically considered for web applications, not for embedded systems. But projects such as gokrazy and TinyGo might change that.
Go has a smooth learning curve.
You can set up a web server or reverse proxy in less than 15 minutes without using anything other than the standard library (just try doing that in C).
Go provides easy concurrency.
Be it using communicating sequential processes, through channels, or shared memory access, Go was designed with concurrency in mind. Launching a new thread is easy and cheap and the Go runtime will make sure to optimize them through the different cores.
Go is cross compilation friendly.
Without any complex compilation flags or makefiles, just a couple of environment variables (e.g. GOOS) and some build constraints (e.g. // +build linux,darwin), and you’re ready to compile your application for any target architecture.
Go has garbage collection.
This doesn’t save you from an application panicking due to an invalid memory access but it might save you from unwanted memory leaks. The garbage collector has a simple interface, through the environment variable GOGC, making it easy to define the initial garbage collection target percentage.
The binaries in Go will always be larger than those in C, but most systems today have more than enough space for them.
The move from C to Go was easy. Go applications may be architecturally more similar to those of object oriented languages, where the types are separated by packages instead of classes, methods are associated to a type and interfaces are passed all over the place. However, the code is written in a very imperative way being that each function, because of the possibility of having multiple return values, almost always returns an error.
The speed argument fell short in this one. Although C is fast, for the project at hand, Go is more than enough because every system interaction occurs every couple of seconds, or every hundred of miliseconds, apart.
Besides the language’s specifications, one of the greatest improvements I’ve felt has been with regards to the development time. Go provides built in access to more complex data structures, such as slices and maps, making it a lot easier and faster to implement fairly simple algorithms. For example, the first time I used kernel linked lists in a C application it took me, at least, an afternoon to get the code up and running where in Go, at most, half an hour would suffice to implement the same code using slices and writing the unit tests. Moreover, it’s easy to import external projects from public repositories, and vendor them in order to secure the build, and use dozens of complex libraries and frameworks with confidence.
Regardless of the language, I think the single most important thing that helped though the change was coding style. In C I followed the Linux kernel’s guidelines, and in Go I feel the language specification, in conjunction with Effective Go and Google’s Code Review Comments, is clear and more than enough. This makes communication between developers easier and lets each developer spend less energy thinking of variable naming and code organization and have more slack to think of the application’s business logic.
It was also fun moving from vim to VSCode, but I’ll leave that for another time.
In conclusion, both C and Go are great languages. Both C and Go are flawed. I believe that Go would be better to teach undergrads to program because it would leave them more time for testing and to improve code structure and architecture leaving aside all the overhead of memory management for another time, after those basics are set, and leading to less frustration and wasted time. However, which language is more appropriate will depend on the specifications of the project at hand. For my case? Go is just fine. And, if I miss the good old days I can always use cgo.