Comparing Go vs. C in embedded applications
Impossibly tight deadlines, unrealistic schedules, and constant pressure to develop and release applications on time, while at the same time achieving excellent quality. Sound familiar? Faster time-to-market for embedded applications is indeed a critical consideration for embedded development teams. But how do you develop an approach for delivering applications faster while maintaining quality and security as a priority? In this context, the product engineering team behind the OTA software update manager Mender.io, went through the process of selecting the best programming language for developing both the embedded client and server parts of Mender. In this evaluation process Go, C, and C++ were shortlisted as candidates. Some of the lessons learned from this evaluation process are explained and why Go was ultimately selected to develop Mender’s client embedded application.
While this can be quite subjective, Go can be a very productive language for embedded development. This is especially true when it comes to network programming, which is at some point a part of every connected device or application. Go was created by Google to meet the needs of Google developers, and it was mainly developed to try to fix the explosion of complexity within its ecosystem. So efficient compilation, efficient execution, and ease of programming were the three main principles behind developing Go as not all three were available before in the same mainstream language.
However, we want to stress that Go cannot be considered a replacement for C as there are many places where C is and likely will be needed, such as in the development of real time operating systems or device drivers.
Strict requirements for embedded development
After establishing the architecture, the Mender product engineering team evaluated which language would be the best fit to develop the Mender application. The system was supposed to consist of a client running on the embedded device and server acting as a central point where clients were connected to. As such, we had several requirements for the language:
- As the client application would be running on embedded devices, compiled binaries needed to be as small as possible.
- It needed to work with Yocto, the embedded distribution of Linux.
- The complexity of installation clients should be low, thus limiting external dependencies and libraries.
- As it would run on different devices, the language must compile across different architectures.
- The devices that the Mender client application would run on would be an IoT or networked device, so the language needed access to networking libraries.
The requirements for selecting a new language also covered a few non-functional requirements:
- As many programmers as possible within our organization needed to be able to understand the language.
- It had to be as easy as possible to share and reuse the existing code between existing applications written in C and reuse code between the client and server applications.
- Development speed was also considered—we have ongoing pressure to add new features quickly.
A comparison between Go, C, and C++ is outlined in Figure 1. From the comparison, Go was primarily selected for its support of buffer overflow protection, automatic memory management, use of standard data containers, and its integrated support for JSON, HTTP, and SSL/TLS libraries.
Requirement | C | C++ | Go |
Size requirements in devices | Lowest | Low (1.8MB more) | Low (2.1 MB more, however will increase with more binaries) |
Setup requirements in Yocto | None | None | None |
Buffer under/overflow protection | None | Little | Yes |
Code reuse/sharing from CFEngine | Good | Easy (full backwards compatibility) | Can import C API |
Automatic memory management | No | Available, but not enforced | Yes |
Standard data containers | No | Yes | Yes |
JSON | json-c | jsoncpp | Built-in |
HTTP library | curl | curl | Built-in |
SSL/TLS | OpenSSL | OpenSSL | Built-in (TLS is a part of crypto/tls package)* |
The evaluation continued to see how Go would stack up against C, C++, and C++/Qt for creating a full-blown custom Linux image using Yocto. Pure image size and image size with network stack were compared, as was how the end application would be delivered to a target image. As Go applications can be statically compiled to a single binary, there is no need for any extra virtual environment (like in the case of Java, which requires a virtual machine in addition to binaries) or additional dependencies running on the device to use the Go code.
C | C++ | C++/Qt | Go | |
Size overhead of the test component, and all the new libraries pulled in, when using the smallest available Yocto image as a basis | 8.4MB | 10.2MB | 20.8MB | 14.6MB |
Size with network stack | 13.4MB (curl) | 15.2MB (curl) | 20.8MB* | 14.6MB |
Shared dependencies | Yes | Yes | Yes | No |
Extra Yocto layer needed | No | No | Yes | No |
Deployment complexity | Binary | Binary | Binary + Qt | Binary |
Comparing Go vs. C
Go was also selected for development due to its extremely rich standard library, which allows for much faster development, especially when compared to C. Go inherits a lot of elements from C, C++, and Python including the expression syntax, control flow statements, data structures, interfaces, pointers, the pass by value concept, parameter parsing syntax, string handling, and garbage collection. With its optimized compiler, Go runs natively on embedded devices.
Go did have some disadvantages, which we’ll mention below. They didn’t weigh heavily enough to overcome its advantages, like the development speed and ease of building in the language, but they did factor into our decision. Here’s the other qualities that went into our decision.
Size comparison of Go vs. C
Go is bulkier than C when it comes to size, which was one of the few disadvantages it had. If you take “hello world” as the smallest application you can write in Go and use the built-in println
function, after stripping away debugging symbols it is just over 600 kB in size (snippet 1). If you include the fmt
package with its dependencies, then the size increases to 1.5 MB (snippet 2).
To compare this to C, if you are building a dynamically linked library, then it is a mere 8 kbytes. If you do it static, then the size increases to over 800 kbytes which is surprisingly even larger than the Go binary (snippet 3).
Snippet 1: smallest Go “hello world” code:
package main
func main() {
println("hello world")
}
$ go build
> 938K
$ go build -ldflags ‘-s -w’
> 682K
$ go build & strip
> 623K
Snippet 2: Go “standard” “hello world”
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
$ go build
> 1,5M
Snippet 3: C “hello world”
#include <stdio.h>
int main(void)
{
printf("hello world\n");
return 0;
}
$ gcc main.c
> 8,5K
$ ldd a.out
> linux-vdso.so.1
> libc.so.6
> /lib64/ld-linux-x86-64.so.2
$ gcc -static main.c
> 892K
$ gcc -static main.c & strip
> 821K
Speed comparison of Go vs. C
Compiled Go code is generally slower than C executables. Go is fully garbage collected and this itself slows things down. With C, you can decide precisely where you want to allocate memory for the variables and whether that is on the stack or on the heap. With Go, the compiler tries to make an educated decision on where to allocate the variables. You can see where the variables will be allocated (go build -gcflags -m
), but you cannot force the compiler to use only the stack, for example.
However, when it comes to speed we can not forget about compilation speed and developer speed. Go provides extremely fast compilation; for example, 15,000 lines of Go client code takes 1.4 seconds to compile. Go is very well designed for concurrent execution (goroutines and channels) and the aforementioned rich standard library covers most of the basic needs, so development is faster.
Compilation and cross compilation
There are two Go compilers you can use: the original one is called gc
. It is part of the default installation and is written and maintained by Google. The second is called gccgo
and is a frontend for GCC. With gccgo
, compilation is extremely fast and large modules can be compiled within seconds. As mentioned already, Go by default compiles statically so that a single binary is created with no extra dependencies or need for virtual machines. It is possible to create and use shared libraries using the -linkshared
flag. Ordinarily, Go requires no build files and a build can be carried out by a simple “go build” command, but it is also possible to use a Makefile for more advanced build procedures. Here’s the Makefile we use at Mender.
On top of the above, Go supports a large number of OSes and architectures when it comes to cross compilation. Figure 3 highlights the wide array of OSs and platforms supported by Go.
GO OS / GO ARCH | amd64 | 386 | arm | arm64 | ppc64le | ppc64 | mips64le | mips64 | mipsle | mips | wasm | risc64 |
aix | X | |||||||||||
android | X | X | X | X | ||||||||
darwin | X | X | ||||||||||
dragonfly | X | |||||||||||
freebsd | X | X | X | |||||||||
illumos | X | |||||||||||
ios | X | |||||||||||
js | X | |||||||||||
linux | X | X | X | X | X | X | X | X | X | X | X | |
netbsd | X | X | X | |||||||||
openbsd | X | X | X | X | ||||||||
plan9 | X | X | X | |||||||||
solaris | X | |||||||||||
windows | X | X | X | X |
Debugging and testing in Go
Many developers, in order to see what’s going on within a program while it executes, will use GDB. For heavily concurrent Go applications, GDB has some issues in trying to debug them. Fortunately, there is a dedicated Go debugger called Delve that is more suitable for this purpose. Developers only familiar with GDB should be able to use it in majority of cases without any issues
It is extremely easy to add testing and unit testing to Go code. Testing is built into the language—all that is needed is to create a file with a “test” suffix, add a test prefix to your function, import the testing package, and then run “go test”. All your tests will be automatically picked up from your source and executed accordingly.
Concurrency support
Concurrency is well supported in Go. It has two built-in mechanisms: goroutines and channels. goroutines are lightweight threads just 2 kB in size. It is very easy to create a goroutine: you only need to add the “go” keyword in front of the function and it will execute concurrently. Go has its own internal scheduler, and goroutines can be multiplexed into OS threads as required . Channels are “pipes” for exchanging messages between goroutines, which can be either blocking and non-blocking.
Static and dynamic libraries; C code in Go
Go has many less known features that allow you to pass instructions to the compiler, linker, and other parts of the toolchain using special flags. These include the -buildmode
and -linkshared
flags, which can be used to create and use static and dynamic libraries both for Go and C snippets. With a combination of specific flags, you can create static or dynamic libraries with generated header files for C, which can be invoked by C code later on.
That’s right! With Go we can call C code and have the best of both worlds. The cgo tool, which is part of the Go distribution, can execute C code, which makes it possible to reuse our own or system C libraries. In fact, cgo and implicit usage of C system libraries is more popular than one might think. There are cases when standard C library routines are picked over pure Go implementations by the Go language constructs themselves.
For example, when using a network package on Unix systems, there is a big chance that a cgo-based resolver will be picked. It happens to call C library routines (getaddrinfo
and getnameinfo
) for resolving names in cases when native Go resolver can not be used (for example on OS X when direct DNS calls are not allowed). Another common case might be when the os/user package is used. If you want to skip building cgo parts you can pass osusergo
and netgo
tags (go build -tags osusergo,netgo
) or disable cgo completely with CGO_ENABLED=0
variable.
There are tricky things to look out for when mixing C and Go together. Because Go is garbage collected but C is not, so if you are using any C variables within Go code that are allocated on the heap in the C code, then you need to explicitly free those variables inside Go.
C++ can also be used inside Go code, which is facilitated through a simplified wrapper and interface generator (SWIG) that binds C and C++ with languages such as Python.
Go advantages outweigh the disadvantages
Our experience using Go shows both advantages and disadvantages. Starting with the negatives, there are a lot of external libraries in the community but the quality varies greatly and you need to be very careful with what kinds of libraries you use. This is improving greatly as the language matures and gains adoption.
When you have some C bindings within your Go code, things usually get more complicated, especially cross compiling, which cannot be done easily without importing the whole C cross-compilation infrastructure.
There are still a lot of positives about using Go for embedded development. It’s quick and easy to transition from C and/or Python to Go and get productive within a couple of days. Go offers very nice tooling and a standard library with more than 100 packages. You can easily set and control runtime settings such as how many OS threads you want to use (GOMAXPROCS
), or if you want to switch on or off garbage collection (GOGC=off
). Libraries and code can be shared easily between the server and client development teams like in our case.
It may be controversial to say this, but the forced coding standard speeds up development. Regardless of the code you are inspecting, it always looks the same. There is no time wasted wondering which coding standard to use.
We developed a few demos of embedded Go applications that you can play with using popular hardware such as Beaglebone Black or Raspberry Pi: One is called “Thermostat” and is based on Beaglebone hardware. It uses infrared distance, temperature, and humidity sensors and exports all the sensor readings through a web interface. The other one is a PRi-based robot car with a camera that can be controlled using a provided web interface. Enjoy!
Marcin Pasinski is the Chief Embedded Engineer at Mender.io. If you want to learn more about the best approaches to over-the-air software updates for IoT gateway devices, visit Mender.io and try the OTA software updater for free.
Tags: c, embedded programming, go, golang, IoT
33 Comments
A PC in disguise is arguably not really an embedded system. Doing application layer programming for PC in pure C is indeed tedious, people came to that realization somewhere in the 1980s, long before Google existed.
It is hard to tell what is embedded and what is not nowadays when embedded devices are more and more powerful. Also people are still doing application level in C in 2020s for some reasons, aren’t they?
Because they need fast response that is why they are using C language in 2020. Probably they will use in 2100. It is same for C++ also. All languages have advantages, If you need speed then C/C++ is the one of the best solution. If you do not need speed or if you need productibility then there are better solutions then C/C++
Why use Go when there is Ada? Ada has been time-tested in the most demanding safety critical environments since the 1980’s and has some of the industry’s best tooling.
Because they aren’t designing actual embedded systems, but 64 bit PC applications? It would be risky to use something like x86-64 for safety-critical systems due to the complexity of the core alone. Same reason why you wouldn’t use C++ (or Go), too much complexity and poorly-defined behavior.
Whatever its other faults, Ada has never been guilty of “poorly-defined behavior.”
There are many things out there and different ones exist for a good reason. You don’t want to fix all the issues with the hummer even though you could.
Maybe because no one creates libraries for Ada. If you need any functionality besides standard library you should do it yourself from scratch. It’s not surprising that few people dare to invest resources into such an adventure. If you’re not a hobbyist you want to move fast — and not create the whole world.
What about Rust?
– Compiled binaries can be made small with various techniques: https://github.com/johnthagen/min-sized-rust
– I don’t know anything about Yocto, but I found various things online about using Rust with it
– Can be statically linked for easy installation
– Supports various architectures and platforms to varying degrees: https://doc.rust-lang.org/nightly/rustc/platform-support.html
– Has some built-in networking support (https://doc.rust-lang.org/std/net/index.html) and a great ecosystem (https://www.rust-lang.org/what/networking)
– No idea
– Rust has very good built-in C FFI support: https://doc.rust-lang.org/nomicon/ffi.html
– Rust helps you catch problems early in the development process and helps you fix them, especially if you use Clippy (https://github.com/rust-lang/rust-clippy)
– The primary goal of Rust is to prevent memory-safety errors such as buffer under/overflow, unless you explicitly opt-out in specific locations with `unsafe` blocks: https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html
– Rust determines at compile time when to automatically free resources such as memory: https://doc.rust-lang.org/rust-by-example/trait/drop.html
– Rust has various standard data containers, called collections: https://doc.rust-lang.org/book/ch08-00-common-collections.html, https://doc.rust-lang.org/std/collections/index.html
– crates.io, Rust’s standard library ecosystem, has multiple JSON implementations, of which serde_json (serialize/deserialize JSON) is recommended: https://docs.rs/serde_json/latest/serde_json/index.html, https://blog.logrocket.com/json-and-rust-why-serde_json-is-the-top-choice/
– Rust’s ecosystem has various HTTP libraries with different advantages: https://lib.rs/web-programming/http-client, https://blog.logrocket.com/the-state-of-rust-http-clients/
– Rust has various options for SSL/TLS, such as OpenSSL bindings (https://docs.rs/openssl/latest/openssl/ssl/index.html) and a pure-Rust implementation (https://github.com/rustls/rustls)
– Rust can be statically or dynamically linked: https://doc.rust-lang.org/reference/linkage.html, https://stackoverflow.com/q/31770604/5445670
– Rust has a small-ish standard library but an awesome package manager (https://doc.rust-lang.org/book/ch01-03-hello-cargo.html, https://doc.rust-lang.org/cargo/) and ecosystem (https://crates.io/, https://lib.rs/)
– Rust is about as fast as C and C++
– Rust supports cross-compilation: https://rust-lang.github.io/rustup/cross-compilation.html
– Rust should be supported decently by most debuggers
– Rust has good support for various kinda of concurrency built-in (https://doc.rust-lang.org/std/sync/index.html, https://doc.rust-lang.org/std/task/index.html, https://doc.rust-lang.org/std/thread/index.html, https://doc.rust-lang.org/nomicon/concurrency.html), plus various stuff in the ecosystem (e.g. https://tokio.rs/): https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html
Interesting and informative article, however the described system really stretches the meaning of “embedded”. Is my Raspberry Pi running a full Ubuntu distribution also “embedded”? At that point you might as well write the software in Python.
Also the executable size comparison is rather meaningless. If I wanted to make C look better I could compile a statically linked hello world program that weighs 8K. Of course, in the real world such comparisons dont make any difference.
P.S. there seems to be some rogue copy-paste going on in the hello world examples.
You don’t know about TinyGo? https://tinygo.org/
It is exactly made for such use cases (ressource constrained devices / embedded / WASM)
Snippet 2 isn’t Go, and Snippet 3 isn’t valid C (same code got copy/pasted twice), maybe it’s some sort of formatting error?
Why use Go, C or C++ when there is Rust? The Rust Embedded ecosystem is ready, and the binaries will be much smaller than the Go ones.
`gcc -static main.c`
You’re not enable optimizations so the output file is obviously large. Try `gcc -static -Os main.c` or `gcc -static -O3 main.c`
Nicely spotted. The robots in the picture should be doing a facepalm instead. Next time a blog like this is posted on a programmer site, maybe have it proof read by actual C programmers?
SO optimization newbie FAQ #1: “why is my program so big and slow?”. FAQ answer in a very tired voice: “Because you are benchmarking with optimizations disabled…”
Also, using glibc in an embedded environment is not a good idea. My static build of hello.c with MUSL libc is 83k…
Yeah, no. On the most recent project i am working, there are 2MB available for the bootloader+updater and the application, there is can communication, update feature for different touch controlled devices and the touchscreen controller, security functions, diagnostics, etc. I really can’t see go doing that in 2MB, and also being as bare metal and as fast as C. Yes if i am making some embedded application at home for an esp or raspberry pi, i might go with python, other than that, C is the main language i will use. There [insert language here] vs C in embedded types of articles are only pointing out how well adapted C is with regards to these other languages.
Over and out!
Rust would do within these resource constraints.
The rust compiler is huuuuuuge. On a Gentoo system, the compile time of the rust package usually exceeds 12 hours.
Yeah, no. On the most recent project i am working, there are 2MB available for the bootloader+updater and the application, there is can communication, update feature for different touch controlled devices and the touchscreen controller, security functions, diagnostics, etc. I really can’t see go doing that in 2MB, and also being as bare metal and as fast as C. Yes if i am making some embedded application at home for an esp or raspberry pi, i might go with python, other than that, C is the main language i will use. There [insert language here] vs C in embedded types of articles are only pointing out how well adapted C is with regards to these other languages. As far as portability goes, there is already a hw abstraction layer in most decently made systems, which go would also need if you want real portability so i don’t see the point of this article, other than saying that some company used go for an embedded application. Don’t even get me started on the overhead of processing json instead of a struct with simple tcp/udp communication
Over and out!
Quite strange to discuss Go for embedded development and not a single word about TinyGo, which even is supported by ARM directly on their IoT documentation.
https://developer.arm.com/solutions/internet-of-things/languages-and-libraries/go
https://tinygo.org/
Mentioned system is hardly an embedded system in the traditional sense. Standard Linux Kernel cannot event be RTOS. Claiming garbage collected language can be an embedded language when it’s system language at most is abomination.
And that on the StackOverflow… We live in strange times.
TinyGo is for embedded applications, but the uptake isnt as good as it can be. I love Go, but it’s not quite ready for MCU’s such as the esp 32. Doesn’t even support j2c yet.
So heap allocation is OK but garbage collection is slow? And no numbers to back that up, nor mention of latency, burstiness, etc. As if C’s time spent in malloc and memory pool fragmentation didn’t count?
Sounds like we’re back to the Java misinformation of the 2000’s.
If this was real embedded development I bet this vagueness would bite, hard.
You’re not supposed to use glibc for static compilation. If you had used, say, musl libc, your executable would be smaller than 6K.
Also, what C compilation flags did you use for your speed comparisons?
Finally – Please fix the recent breakages in the SO UI and also reform the Code-of-Conduct.
I can confirm, compiled with musl, the statically compiled and stripped” hello world” program is 12K on my machine.
Title should read “Comparing Go with C” (preposition is debatable) or simply “Go vs. C.” “Comparing A vs. B” is redundant.
Please build application for LLC resonant converter built on dsPIC33 with Go or C++ and then say that C is obsolete )))
The “embedded” you’ve mentioned is not so “embedded”…
And by my personal opinion on Go… it’s quite hard to work with binary protocols. And it’s not compensated with easy JSON usage..
But JSON and HTTP(S) is realy cool.
This is the reason I selected Go for web server and backend application on embedded linux system.
If we’d have to code in a garbage language nobody knows, python is already out there for this specific purpose, thanks anyway google
I think Rust is much better than Go, It’s fast, secure, and help to develop apps with minimum bug.
Think like a boss. The principle question for many managers when choosing a development language is the availability of staff. There is a rich pool of C developers to draw on, while Go – less so. I would not commit to using Go until I was certain I could get (and maintain) my supply of developers. Finding and keeping developers is very difficult at the moment. Selecting a languages should not focus purely on technical concerns.
Anyone else bothered by this table? Half of the “requirements” that are listed are not discussed in the requirements section of the post, they’re introduced solely to make Go look good. “Bugger under/overflow protection”…. Is this a joke? If someone presented me with this argument, I’d tell them to stop being a shill and stick to the requirements they identified,, rather than making up new ones because they love Go.
I love how people do performance comparisons with C by comparing optimized code compiled in other languages to non-optimized C compiled code. And C still ends up winning half the time. LOL