Go: A Beautiful Mess

The world which Go stands on, we all get happiness and pain from Go.

Arlo Liu
Analytics Vidhya

--

Photo by Mike Petrucci on Unsplash

A old school guy like me love statically typed language, even I tasted the convenience of dynamic typed languages for handling data with complex structure like JSON or operating data with SQL/NoSQL. The statically typed language avoids many ambiguous issues and let us figure out the mistake before before unit test reports it.

So I love Go in many pieces, it leverages many concepts from other languages, has a easy understanding struct types, great tool set for production…etc, and most importantly, it’s easy to write and performance will not let you down, but I also got confused sometimes and found many ambiguous parts in Go.

When I first started using Go, I was pleasantly surprised to find that Go had collected almost every thing for me. In most cases, all I need to do is focus on code itself. But after I used Go to write projects for a while, I started to feel their some designs in Go are too simple and lacks flexibility when dealing with larger project.

In Less is exponentially more, Rob Pike explains many design principles and many reasons why Go has become so, I can imagine the scene of some old-school engineers sitting together, talks about C++ , share their annoying experiences about C++, and deciding to make a simple language and create many convenience tools to help them live in happy coding world. The interesting part is, this might be also the reason why I think Go is beautiful and mess.

A Beautiful Mess — the world which Go stands on

At the time or writing this article, the latest release version of Go is v1.15. So some of opinions in the following may change in the future.

The Beautiful of Go

Easy learning curve

It might not the most important core feature of a programming language, but Go is amazing to me that I only took two days to learn the essential knowledge, and take one day to get used to typing declarations (like write num int instead of int num), and take about three days to understand about Go module, goroutine, context, channel…etc.

For an experienced programmer with C/C++/Java…etc, spending some time to finish all lessons in A Tour of Go will be enough to start writing a practical Go program, and the rest of time will be spent on reading library documentation, learning the idiomatic way, and meet gotchas from Go.

Go also provides a wealth of tools to help novices to write programs without learning much knowledge about compiling, glinting, packaging and deploying…etc

In many ways, Go is indeed an easy-to-learn language, and it feels more like an ecosystem letting people not need to spend lots of time on environment setup.

We just need to write code and type go run ...

No preprocessor directives

In traditional statically typed languages like C/C++, we often use pre-processor directives to define marcos or do conditional tests in compile time like this:

#define MIN(a,b) (((a)<(b))?(a):(b))
#define MAX(a,b) (((a)>(b))?(a):(b))
#ifdef __x86_64__
...
#else
...
#endif

We define marcos for many purposes(usually because they can do everything even though we know it’s evil), but the trade-off is lacking type safety and sometimes makes code be unreadable and hard to debug.

Go doesn’t provide preprocessor directives like #define , #if …etc., from my point of view, this is a symbol of progress, especially for modern languages.

Technically, Go provides some special preprocessor directives, not for people to define marcos, but only to let people provide hints for the compiler or build tool.

For example, Go provides some pragmas like go:noescape, go:noinline …etc, which provides hints to the compiler. And Go also provides build tags directive to constraint which files will be included in the package, it’s not a perfect solution and not functional as other build systems like CMake, Gnu-make..etc., but at all, it keeps things simple.

Further reading

Easy to define new types

This is the part that makes me feel comfortable, In Go, defining a new type from an existing type is very easy.

Distinct types for different purposes will make code more readable, it also helps to prevent passing the wrong order of parameters to a function, and provides the possibility to write checking methods for each type.

It’s very easy to create a new type from string and provide some methods for the new type, as shown in the following example:

The example shows an easy way to define two new types from string: UserID and UserName, and adds validation function for these types, the distinct type also prevents us from passing the wrong order of parameters.

Concurrent programming has never been so easy

I always think the best feature of Go is goroutine, and the concept of channel can be a bit confusing but also great.

In addition to the advantages of goroutine, such as lightweight and has a nice controlling mechanism by context, I think the biggest advantage of goroutine is that we can write code within goroutine with the synchronous model, and benefits the performance of the asynchronous model.

On the other hand, the memory overhead of a goroutine is only about 2~4 KBytes, and the overhead of threads is several megabytes, which shows that goroutine is indeed a lightweight solution compared to thread.

In my experience, the Go scheduler works well with thousands of goroutines and frequent garbage collections.

Further reading

Context package in Go is great and elegant

When dealing with network programming, Go standard library provides a context package to manage tasks by an elegant propagation mechanism, it makes things much easier if we want to cancel or set a timeout for tasks without writing complicated. syncing and notification mechanism.

When we write a large service program, the network operations could happen in a deep stack of code, and we need to handle the timeout or network fail issue very carefully on every functions, or it will block the whole service or cause a chain crush.

Fortunately, most request-related functions in Go use context as its first parameter and create a derived context if necessary, so we can cancel or set a timeout of these functions from the upper function, and control these tasks in a delightful way.

Further reading

Use range for iteration as the default style, not iterator interface

Well, some people may disagree with this part, I know some programmers really like the iterator style, but in my opinion, I prefer range-based loop, eg.

Go uses range-based loop for string, array, slice, map, and channel, it’s a clean way to iterate items, The modern C++ also supports range-based loop, auto-typed variable, and other nice features(It makes me loving coding in C++).

Range-based loop in Go looks nice and clean, unfortunately, Go doesn’t provide a standard iterator interface like Java and C++, so the range operation only supports built-in types.

For example, when we want to iterate items insync.Map , we can’t range it directly, but needs to pass a callback function to Map.Range(), the poor support of container in Go makes no sense to me.

The standard toolchain rocks

How useful and convenient of standard tools provided by Go might be the top 5 parts that I love coding in Go.

For formatting code, Go provides gofmt, simple and fast, whether like it or not, it provides an official canonical format for code, and I found many projects use it, I guess it’s because vscode is the most popular editor in Go development, and the vscode’s go extension is really nice, Really.

For linting, Go provides golint, not powerful but simple, but there has golintci-lint and revive, both nice and fast.

For construct checking, it has go vec, for documenting Go code, Go defines godoc format lets us write inline documentation, and has an official https://pkg.go.dev package document portal.

And the best part is about testing, code coverage report, benchmark, and profiling, especially profiling, the built-in CPU and memory profiling feature is very powerful, it reduces lots of time for performance optimization.

Build fast, test fast, and die fast

This part is my personal preference, I have no patience for long compiling time, but if you live in C++ world, the only thing you can do is take a deep breath, and accept it.

When I start to use Go, it amazed me that the compilation time is blazing fast, it only takes a few seconds even for a large project. Fast compilation time brings more agile debugging efficiency, in other words, the code dies faster if you did something wrong.

Friendly for deployment

It’s another part of personal preference, default go compiler and linker(not gccgo) can compile and link objects into a static executable file, which means our program doesn’t depend on other shared libraries.

The library dependency issue might become very tricky in the deployment stage and in modern deployment environments, people increasingly use container tech. like Docker, the single static executable file depends on nothing, so doesn’t need to install libraries into containers, the result is we have a small footprint container, DevOps loves this.

The Mess of Go

Error handling of Go is staying on the age of dinosaurs

People complain about the Go’s error handling, and so do I.

The error handling of Go makes me feel like traveling back to the 90s when I wrote C code.

The development team of Golang says they don’t want to support exceptions, so in the end, the error handling in our code everywhere like this:

Due to the fact that the interface in Go doesn’t support default implementation for method, error is an interface type with Error() method, and doesn’t define any other methods to support stack tracing.

Even though it defines it, to ask every custom error implement method for stack tracing is not practical, it should let a default implementation handle stack tracing in error interface.

Unfortunately, the role of Go interface needs to be simple and pure, so the only thing that interface needs to do is define method’s name and signature.

So the thing becomes funny, the error type looks like just a string value. When an error happens, the only thing we know is there are “blah…blah..” error happens. We are not sure of the location of the error happens because this error might be reported in a deeper function instead of the function we called, and we need to spend lots of time figuring out the actual location that error happens if we did not use some stack traceable package like errors.

Panic, recover, defer, makes people pain

I said Go doesn’t support exceptions, but wait…it does!

Go has a function panic() that is designed to terminate the program like assert() in C. And for some reason, maybe about the dark force in the universe, Go v1.0 added a function recover() several years later, the related discussion is here.

When recover() added in Go, the panic and recover become a partner, its behavior is similar to try-catch exception handling, panic-recover looks like a weird version of try-catch, except Go calls it as panic and recover.

Well, using recover() to catch errors from panic() is an acceptable way for me, even if we need to call recover() in deferred function.

But what if we want to catch error from panic then return error or some meaningful result?

The solution is that we need to use named result_parameters, aka. named return value. It’s because returning values within a deferred function are discarded when the function completes, and the deferred function can access and modify named return values before they are returned. If we want to return something in a deffered function, we need set data to a named return value.

In order to accomplish a such simple task, we need to write some weird code as follows:

The panic is thrown by throwPanic(), thencatchPanic() catches it by recover() within a deferred function, the return value of recover() is a value with a type ofinterface{}, so we need to check if the value thrown by panic is an error or not, then we can assign it to err.

Well, I need to admit, it’s not like try-catch mechanism in Java, it’s like try…finally mechanism!

The deferred function recover() is like a block of finally, it doesn’t sure if there is panic throws or not, so it needs to check the recover()return value and see if there has any value thrown by panic().

Anyway, I can’t understand why the Go team provides this weird solution if they think catching panic is a necessary feature, but not provides a more readable mechanism.

Further reading

The interface is simple, but too simple

The interface is the basis of the type system in Go, the function of the interface in Go is similar to other languages: defines behaviors.

Go is not an object-oriented language, but it borrows some concepts from modern languages. Go interface is a simple solution that provides polymorphism, and when the term polymorphism is mentioned, I would like to quote a sentence from the inventor of BASIC language.

“Polymorphism means that you write a certain program and it behaves differently depending on the data that it operates on.” — Thomas E. Kurtz

The concept of interface is fine for me, but when I dealt with interface in standard and other community packages, I found some interesting phenomenon.

  • The empty interface is overused, inteface{} appears everywhere because gophers use it as a Any/void * value to receive/pass concrete data.
  • People like to treat the interface as a base class, but it’s not.
  • When things get worse, people use reflect package to solve problems in runtime, by a bunch of conditional statements.
  • People spend lots of time figuring out which struct implements which interface because Go claims it doesn’t want to provide the keyword “implements”.

Except the last one, all these phenomenon are related to Generics, but the Go’s type system can’t provide an elegant way for generic programming at least in v1 of Go.

But generics is so useful in some scenarios that it makes people unable to give it up, so people try hard to write code as generics by defining methods, using type assertions, reflection …etc.

Let’s talk about the last one I mentioned, an interface needs a concrete type to implement it, no matter if this type is called class, struct, or other names. Therefore, when we define a type in statically typed language to implement an interface, we want to let the compiler know, then the compiler can check whether the operation of the interface is valid at compile time.

This brings complexity to the compiler, and brings the cost to compilation time, Go lets the type checking operations from compile-time to runtime, then tell programmers that your duty to check the type at runtime, and programmers say: “Oh, OK”, then some awkward codes appear.

Go will eventually have generics, the proposal about generics is here, hopefully it will happen in the near future.

Further reading

The interface doesn’t support default implementation of methods

As the section Interface is simple, but too simple mentioned, Go interface is so simple and it doesn’t support any other features besides defining method signature. So it can’t define a default implementation of the method, making it hard to add new methods in the interface without breaking the compatibility of existing types already implemented this interface.

It might not a big deal if the interface is not exported outside of the package because we can update all the types in the package ourselves. However, if we provide exported interface for others in the package, for example, we write an open source package and put it on GitHub, trying to add a new method to this interface will break the code of the person using our package.

So only thing we can do is upgrade the major version of package and set it as incompatible because we can’t provide a default implementation for new method. There is a famous example:

When Go v1.13 was released, it introduced an error chaining mechanism by adding a new Unwrap method to errors package.

Why not add this method to the error interface but add it to the errors package instead? It might be the reason I mentioned above: they can’t because it will break all existing code.

Further reading

Constants in Go only supports scalar types

Go has constants, but it’s not The constants as I thought.

The definition of constants in Go follows a simple principle, which makes scalar values unchangeable, the types of scale values are: bool, all number types, and string. Therefore, there is no const struct, const pointer, and const array.

But if we look at the standard packages provided by Go, you will find that there are a bunch of non-constant exported variables, and the reason these variables are not constants is: Go doesn’t support them to be constants.

Let’s take a look on net package, this package exports IPv4bcast, IPv4allsys, IPv4allrouter and IPv4zero variables. So it is easy to do some evil thing:

Strictly speaking, this issue is an implementation issue, and there have many ways not to use exported variables within a package, but in the real world, sometimes we will need to export some variables to provide predefined information.

And in that case, we will want to set these variables to be constants, if we can.

Only built-in types are iterable by range-based loop

All again we are back to talk about the interface problem, Go provides a first-class member badge for built-in container types: array, slice, map, channel(uhh, why are you here, just because you behave like queue? but your father call you channel.)

Only these first-class members can enjoy sitting in the range-based loop, other container types are members of economy-class, and need to find their own way to provide the iteration.

In list.List package, needs to use for e := l.Front(); e != nil; e = e.Next() {…} to iterate items, and sync.Map needs to use Range() method and passing callback function.

Besides, these first-class members behave different than others, when they are passed as parameters, they behave as if they are passed by value, in fact, they are passed by pointer, so they look like they passed by reference.

BTW, string type is a special one, it’s immutable, it is passed by value, but it only copies the length of string and the pointer of string. And it only creates a new memory space to store new content if we modify it.

The naming convention of visibility

The naming convention of visibility in Go is not bad, except it is a bit annoying.

Go forces us to use upper case letter in the first character for exporting variables, structures, and methods, it’s not a problem if we always live in Go world.

But when we want to exchange data over the network by JSON/Protocol Buffers…etc, adding struct tags in struct fields is a bit annoying.

The meta format of the struct tag is a bit…hmm..too simple!?, and it is easy to make mistakes accidentally. Another thing about struct tag is that we need to use reflect package to fetch it and parse it by ourselves.

Export it or not, no private attribute for struct field

In package scope, everything is visible to each other like big brother C, but unlike C, Go also provides a simple visibility rule: makes it is visible outside the current package or not.

The same situation happened again, it’s suitable for small packages, but it would be nice if there has a fine-grained visibility control for large packages, so:

How about providing private attribute to the structure field so that struct field is only visible in the structure?

The private member data, or we can call it a private structure field, is a common concept, and it really helps to prevent someone from tampering with the data.

Slice builds a trap and waits for you

Slice is widely used in Go, when we use it simply as a read-only view of the underlying array, it will not encounter problems. But when we pass a slice into another function and try to use append() or copy() to modify the slice, we will encounter some confusing and unintuitive problems.

It needs to take some time to understand the internal operation principle and structure of the slice. From my point of view, many problems and pitfalls have nothing to do with the slice itself, but people are used to thinking of a slice as a value or a reference/pointer value because it looks like a value! but the nature of the slice is the view of the underlying array.

Another problem is about append() function, append() will try not to allocate new memory for the underlying array if the capacity of the destination is enough, and allocate new memory when necessary to avoid runtime panic, then always return a new slice that might point to the same address or not.

The structure of the slice is like this:

type slice struct {
array unsafe.Pointer
len int
cap int
}

A slice value is not a slice instance or a pointer of slice, it’s more like a value that point to the slice.array, you can think of a slice value as unsafe.Pointer((*slice)(unsafe.Pointer(&A).array) .

Let’s see an example:

When ‘A’ is created with a length and capacity of 4, and want to append a byte into ‘A’, append() detects the capacity of ‘A’ is not enough, so it allocates new memory for the underlying array.

The addresses of the underlying array of ‘A’ and ‘B’ are different, and the addresses of ‘A’ and ‘B’ are always different, because they are different values.

The interesting part is about ‘C’ and ‘D’, ‘C’ is created with zero length and 4 bytes capacity, so when append a byte into ‘C’, append() is not allocate new memory, it returns a new value with the same underlying array and an updated length value.

Let us observe ‘C’ and ‘D’, and we find that the memory address of these two values is the same, when we think of slice as a pointer, it will mislead us into thinking that both ‘C’ and ‘D’ point to the same structure, so the length of ‘C’ should be updated from 0 to 1, but it doesn’t.

As I mentioned above, the slice is a View of the underlying array, it is a pointer that points to the underlying array, not a pointer that points to the internal structure itself.

Technically, nothing wrong with slice and append() function, but it might be better to provide a slice method like A.append(B) , and update length, capacity, and data address in this method.

Further reading

Beautiful and Mess

godoc is simple, but again, too simple

When I started learning Go, I felt dizzy while reading API document. I remember my thoughts at the time:

  • Where are description about input parameter?
  • Which paragraph is about the return value?
  • Why they don’t mark parameters but make look like ordinary words?
  • Why so many methods have error as return value but I don’t often see description about it?

After I started to read the godoc document, I finally solved my confusion, ohh…It is a plain text format, nothing else, excepts that godoc parser can parse url.

Even UNIX manual page like Grandpa describe things more clearly

The plain text format is easy to write, it’s really a nice thing that Go provides official tool to support documenting, but writing unmarked format requires writing more to better describe parameters and return values, people need to act as lexical parser to search the keyword about parameters and return values, sometimes it’s a bit tired due to shorthand naming convention in Go.

In my point of view, it would be great if there has a official way to mark parameters and return values , such as javadoc.

Go module is nice, and can be further improved

Go module is probably the most exciting thing when Go v1.11 introduces it.

It saves the hell of dependency management, and I like the the idiomatic way to use URL as module namespace, looks clear and avoids naming conflict issue.

Unfortunately, because people use GOPATH for many years, so Go module needs to be as compatible as possible, and for backward compatibility, when we were about to release a new major version, thing becomes a bit awkward.

The recommended strategy is to develop v2+ modules in a directory named after the major version suffix. — Go Modules: v2 and Beyond

In Go Modules: v2 and Beyond , it explains why this is a recommended way instead of the common way that create v+ branch for major versions.

There has topic that I hope Go module can handle it better.

Provide an easy way to get modules from private repository

The popular package manager npm handles this topic very well. It support various way to access git repositories, it supports access via ssh, git, and https with or without access token, it supports fetch package from repository directly, from gzipped tarball, from local directory, from registry…etc.

In short, npm provides a more flexible way to fetch packages then Go module.

For Go modules that hosted in private repositories with authentication enabled, we need setup some stuffs first:

  • Set GOPRIVATE environment variable to bypass official proxy
  • Do some tricks in git configuration.
  • Set GONOSUMDB environment variable if necessary.

Generally speaking, Go module and module proxy might need a more flexible notation for different module sources.

Further reading

Conclusion

The real world, the coexistence of beauty and mess is not a contradiction, but a normal state and everyone has different views on beauty or mess.

When I started to write this article, it originally expected it to be a 3 ~ 4 pages article expressing my personal opinions, and tried to be neutral and fair. But as you saw, a opinion will bring other opinions, it’s like the positive side always has another side.

I have mixed feelings about Go, my personal preference is more clear for or other languages. For example, I like C++, even if it’s super complex and the syntax doesn’t look pretty; I don’t like Java even if I respect it’s many good language design concepts; I like Javascript(modern one) even if it’s so chaos; I don’t like Python because it’s slow.

Go is like a friendly uncle, humorous, energetic, but also paranoid and stubborn, sometimes he likes to wear fashionable outfit, but sometimes he insists that old sweaters are the best.

The unique charm and characteristics of Go make you like it quickly, and start to expectations for it, then you may start to find it a bit stubborn, and finally you learn to get used to it.

Simplicity is beautiful, less is more, I have seen the Go team put in a lot of efforts to keep it simple. It quite a fact, Go is easy to learn and can be easily used in production environment. But If we always talk about simplicity, then focusing on simplicity will bring confusion, then thing will become complicated.

There always a reason why a programming language becomes popular. For example: when node.js meets express framework, when Ruby meets Rails, when PHP…hmm, let us skip it. The performance, concurrency and cloud-friendly features make people start to switch from other languages to Go.

The same fact applies to me, in my regular usage scenarios, Go can save lots to time to implement high performance low latency and real time web services. I like to use node.js to provide RESTful API at front end, let node.js to do data validation and other I/O intensive stuffs then dispatch tasks to backend that writing in Go to compute tasks concurrently.

I hope this article can give you some hints and provide some points you may not aware of before. It would be my honor if you can avoid falling into some pitfalls after reading.

--

--

Arlo Liu
Analytics Vidhya

A programmer since the age of seven, a cat lover, and lives in a beautiful country called Taiwan