Go Is Amazing, So Here's What I Don't Like About It



After my last post and generally the kind of indirect advertising I’m doing to the Go programming language for a few months now, I heard about and talked with a lot of people who started being interested in the language, so for once I decided to write what I don’t like about it instead, to provide a more balanced perspective of what’s my experience so far and maybe let some of those people realize that Go is not the right choice for their projects after all.

NOTE 1

It’s important to say that some, if not most of the things I’m about to write are purely subjective and related to my programming habits, they do not necessarily represent so called “best practices” and should not be taken like so. Moreover, I’m still a Go noob, some of the things I’m going to say might just be inaccurate / wrong, in which case feel free to correct me and teach me something new, please :D

NOTE 2

Before we start: I love this language and I already explained why I still consider it a better choice for several applications, but I’m not interested in an opinion war about Go vs Rust, or Go vs whatever … use what you think it’s best for what you have to do: if that’s Rust go for it, if you think it’s binary code you send to the processor by using your nipples to inject faults into some data bus, go for it, both cases, code and let code, life is too short for being a language hipster.

Let’s start from the smallest things to the more serious ones …

Plz Gimme a Ternary Operator

Writing mostly apps that run in a terminal emulator, I often find myself printing the status of the parts of the system I’m working on in terms of enabled / disabled (like enabling or disabling one of bettercap’s modules and reporting that information), which means most of the times I need to translate a boolean variable to a more descriptive string, in C++ or any other language supporting this operator it would be something like:

1
2
3
bool someEnabledFlagHere = false;

printf("Cool module is: %s\n", someEnabledFlagHere ? "enabled" : "not enabled");

Unfortunately Go does not support this, which means you end up doing ugly stuff like:

1
2
3
4
5
6
7
8
someEnabledFlagHere := false
isEnabledString := "not enabled"

if someEnabledFlagHere == true {
isEnabledString = "enabled"
}

log.Printf("Cool module is: %s\n", isEnabledString)

And this is basically the most elegant way you have to do it (other that actually having a map[bool]string just for that …) … is it less convenient? is it more? For me it’s ugly, and when your system is highly modular, repeating this stuff over and over again can considerably increase the size of your code base, basically for no valid reason but the lack of an operator. ¯\_(ツ)_/¯

NOTE Yes, I know you can do this by creating a function or aliasing the string type, there’s no need to post every possible ugly workaround on the comments, thanks :)

Auto generated stuff != Documentation

Dear Go experts, I’m really thankful for the code you share and the stuff I manage to learn everyday by reading it, but I don’t think this is of any real use:

1
2
3
4
5
// this function adds two integers 
// -put captain obvious meme here-
func addTwoNumbers(a, b int) int {
return a + b
}

As I do not think that things like these are valid substitutes for documentation, while it looks like this is the standard way gophers document their code (with some exceptions of course), even if it’s about frameworks with thousands of forks and users we’re talking about … not a fan of super detailed documentation myself and this is not necessarily a huge problem if you enjoy digging into the code itself anyway, but if you’re a documentation junkie, be prepared to a continuous disappointment.

Git repos as a Package System is nuts

I had an interesting conversation on Twitter a few days ago, I was explaining to someone why Go imports look like github URLs:

1
import "github.com/bettercap/bettercap"

Or simply what happens when you:

# go get github.com/bettercap/bettercap

Basically, in the simplest Go installation you might possibly use (not using vendor folders and/or not overriding $GOPATH), everything (not really but let’s pretend for the sake of simplicity) lives in this arbitrary folder you decided and with which you filled the $GOPATH variable, let’s say in my case it’s /home/evilsocket/gocode (well, it actually is). Whenever I either go get something, or I am importing it and using go get to automagically download the needed packages, what basically happens on my computer is:

# mkdir -p $GOHOME/src
# git clone https://github.com/bettercap/bettercap.git $GOHOME/src/github.com/bettercap/bettercap

Yes, Go actually uses Git repositories for packages, applications and everything Go related … which is very convenient in a way, but it creates a huge problem: as long as you don’t use different tools and / or ugly workarounds (more on this in a bit), everytime you compile a software on a new system which is missing a given package, the master branch of the repository of that package will be cloned, meaning you’ll potentially have different code every time you compile your project on a new computer even if the code of the application you’re compiling did not change at all (but the master branch of any of the packages did).

via GIPHY

Have fun when users will start reporting bugs about third party libraries and you have no idea at which commit the repos where at when they built their version of the software from source ^_^

Yes, yes, yes. You can use stuff like Glide or any other tool that will “freeze” your dependencies to specific commits / tags and use a separate folder for them … but that is an ugly workaround for a terrible design choice, we all know it, it works, but it’s ugly.

Pretty much like using URL redirectors in order to be able to import specific versions of a package … it works, but it’s ugly and maybe somebody might also be concerned about the security implications of that … who’s in control of those redirections? Does this whole mechanism make you feel comfortable with the stuff you’re importing in your code and compiling and running on your computer, maybe as root with sudo? It should not.

Reflection? Mmm not really …

When I first heard about Go having reflection and, being used to the concept of reflection from other languages such as Python, Ruby, but also Java, C# and so on, I had so many ideas on how to use it (or, how to use what I thought to be Go’s reflection), like automagically enumerate available 802.11 layer types and build packets out of those, resulting in automatic WiFi fuzzing or something very close to that … it turns out, reflection is a big word when it comes to Go :D

Yes, given an opaque obj interface{} you can get its original type and you can also list the fields of a given object, but you can’t do simple stuff like enumerating the objects ( structs and generally types ) that a given package exports, which might seems trivial, but without it you can’t do stuff like:

  1. Build a plugin system that autoloads stuff from a given package without explicit declarations.
  2. Basically everything you can do with dir in Python.
  3. Build the definitive 802.11 fuzzer I had in mind.

So yeah, reflection is kind of limited compared to other languages … I don’t know about you, but it bothers me …

Generics? Nah

While most people coming from object oriented languages will complain about the lack of generics in Go, I personally don’t find that a big issue not being a super fan of OOP-at-all-costs myself. Instead, I do think Go object model (which is basically not an object model) is simple and slim, this design is inconsistent with the complexity that generics would add IMO.

NOTE

With this I don't mean "generics == OOP", but just that the majority of developers expecting generics is because they replaced C++ with Go and expect something like templates, or the Java generics ... we can surely talk about the small minority coming from functional languages with generics or whatever, but for my experience those are not statistically relevant.

On the other end, this simplistic object model, which is quite close to just using function pointers and structs in C, makes something else less simple and immediate than the average language.

Let’s say you’re developing a software that has many modules (I like modularity in case that wasn’t clear already :D), all of them derive from the same base object (so you can expect a given interface and handle them transparently) which also needs to have some default functionality already implemented and shared among all derived modules (methods all the derived modules would use so they’re directly implemented in the base object for convenience).

Well, while on other languages you’d have abstract classes, or stuff that is partially implemented (the common and shared methods) and partially only describes an interface (pure virtual methods):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class BaseObject {
protected:
void commonMethod() {
cout << "I'm available to all derived objects!" << endl;
}

// while this needs to be implemented by every derived object
virtual interfaceMethod() = 0;
};```

It happens that Go simply does not support this, something can either be an `interface` or a base `struct` (object), but it can't be both at the same time, so we'd need to "split" this example in this way:

```go
type BaseObjectForMethods struct { }

func (o BaseObjectForMethods) commonMethod() {
log.Printf("I'm available to all derived objects!\n")
}

type BaseInterface interface {
interfaceMethod()
}

type Derived struct {
// I just swallowed my base object and got its methods
BaseObjectForMethods
}

// and here we implement the interface method instead
func (d Derived) interfaceMethod() {
// whatever, i'm a depressed object model anyway ... :/
}

And eventually your derived object will implement the interface and extend the base structure … it might look like the same or also that this is a more elegant and decoupled approach, but it can get messy quite fast when you try to push Go polymorphism a little bit further than this ( here a more realistic example ).

Go stuff is easy to build, CGO is hell.

Building (and crosscompiling) Go apps is incredibly easy, no matter for what platform you’re building it for or from. Using the same Go installation you can compile the same app for Windows, or macOS, or Android or some MIPS device with GNU/Linux if you want, no toolchains needed, no exotic compilers, no OS specific flags to remember, no weird configure scripts that never really work as we expect them to … HOW COOL IS THAT?! (if you come from the C/C++ world and used to cross compile your stuff a lot, you know this is huge…or if you’re a security consultant who needs to quickly cross compile his agents for both that tasty Windows domain controller and the crappy MIPS IP Cam he infected yesterday).

Well, it happens this is simply not the case if you’re using any native library which was not originally implemented in Go, and you probably will unless you won’t just use Go for “hello world”.

Let’s say your Go project is using libsqlite3, or libmysql, or libwhatever because whoever wrote that neat ORM you’re using in your super fast Go API did not bother reimplementing the whole DB protocol in Go (of course) but just used some nice, default, standard and well tested system library wrapped in a CGO module … so far so good, all languages have some wrapping mechanism for native libraries … and also, all is good as long as you’re just compiling your project for your host system, where libsqlite3.so, or libmysql.so, or libwhatever.so are available via some apt-get install precompiled-swag thing, but what happens when you have to crosscompile, let’s say, this project for Android? What if the destination system does not have libXXXXXX.so as default? Of course, you’ll either need that system’s C/C++ toolchain and compile the library yourself, or just find a way to install the compiler direcly on that system and compile everything there (using your Android tablet as a build machine basically). Have fun with that.

Needless to say, if you want / need to support several operating systems and architectures (why you shouldn’t given one of Go biggest strength, as we said, is exactly this?) this adds a huge amount of complexity to your build pipeline, making a Go project at least as complex to cross compile (sometimes, ironically, even more) than just a C/C++ codebase.

For some project of mine at some point I just fully replaced the sqlite database I was using with JSON files, that allowed me to get rid of the native dependency and have a 100% Go app, which made crosscompilation super easy again ( while this is the hell you’re going to have to manage if you just can’t avoid having native dependencies … sorry about that :/ ).

If your super-smart-self is now screaming USE STATIC BUILDS!!!! all over (statically compile libraries in order to at least have them -inside- the binary), just don’t. If you compile everything statically with a given version of glibc the binary will not work on systems with a different glibc.

If your even-smarter-self is now screaming USE DOCKER FOR BUILDS!!!!!, find a way to do it correctly for -every- platform and -every- arch and then send me an email :)

If your but-i-kinda-know-go-for-real-self is about to suggest some exotic glibc alternative, see requirements for his brother, Mr even-smarter-self :D

ASLR? Nope! -troll face-

So ok, this is kind of controversial, Go binaries have no ASLR, BUT, given how Go manages memory (and mostly, given it doesn’t have pointer arithmetic) that should not be a security issue, as long as you do not use bindings to native libraries with vulnerabilities, in which case the lack of Go ASLR would make exploitation way easier.

Now, I kind of get Go developers point and I kind of don’t: why adding complexity to the runtime just to protect the runtime from something it is not vulnerable to in the first place? … but considering how often you end up using native libraries (see the previous section of this post :P) just ignoring the problem is not a wise approach regardless IMHO.

Conclusions

There are many other small things I don’t like about Go, but that is also true for every other language I know, so I just focused on the main things and tried to skip stuff like i don't like this syntax X which is completely subjective (and I do like Go syntax btw). I saw many people, blindly embracing a new language just because it’s trending on GitHub … on one hand, if so many developers decided to use it, there are indeed good reasons (or they’re just compile-anything-to-javascript hipsters), but the perfect language which is the best option for every possible application does not exist (yet, I still have faith in nipples and fault injection U.U), always better to double check the pros and cons.

peace