Go: A Beautiful Mess

· 5035 words · 24 minute read

originally published on medium.com . I published it here because medium is blocked in some countries.

An 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 take two days to learn the essential knowledge, take one day to get used with type declaration(like write num int instead of int num), and take about three days to understanding about Go module, goroutine, context, channel…etc.

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

Go also provides a wealth of tools to help novices to write program without learning many knowledge about compiling, linting, 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 for 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 marco or do conditional test 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 it can do everything even we know it’s evil), but the trade-off is lacking type safety and sometimes makes code be unreadable and hard for debugging.

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 let people to provide hints for compiler or build tool.

For example, Go provides some pargmas like go:noescape, go:noinline …etc, which provides hints to compiler. And Go also provides build tags directive to constraint which files will be included in 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: Go’s hidden #pragmas , Build constraints .

Easy to define new types 🔗

This is the part that makes I feel comfortable, In Go, defines a new type from existing type is very easy.

Distinct types for different purposes will make code more readable, it also helps to prevent passing wrong order of parameters to 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 a 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 passing wrong order of parameters.

Concurrent programming has never been so easy 🔗

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

In addition to the advantages of goroutine, such as lightweight and has nice controlling mechanism by context, I think the biggest advantage of goroutine is that we can write code within goroutine with synchronous model, and benefits the performance of 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 mega bytes, which shows that goroutine is indeed a lightweight solution comparing to thread.

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

Further reading: Concurrency in Go

Context package in Go is great and elegant 🔗

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

When we write a large service program, the network operations could happens in deep stack of code, and we needs 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 it’s first parameter and create a derived context if necessary, so we can cancel or set timeout of these functions from the upper function, and control these tasks with a delightful way.

Further reading: Go : Context and Cancellation by Propagation

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

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

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

Range based for loop in Go looks nice and clean, unfortunately, Go doesn’t provide 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 tool chain rocks 🔗

How useful and convenience of standard tools provided by Go might the top 5 of parts that I love codding in Go.

For formatting code, Go provides gofmt, simple and fast, whether like it or not, it provides a 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 writing inline documentation, and has 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 amazes me that the compilation time is blazing fast, it only takes 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 object into static executable file, it means our program doesn’t depend on other shared libraries.

The library dependency issue might becomes very tricky on deployment stage, and in modern deployment environment, people increasingly use container tech. like Docker, the single static executable file depends nothing, so doesn’t need to install libraries into container, 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, 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 Go say they don’t want to support exception, so in the end, the error handling in our code everywhere like this:

Due to the fact that 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 it defines it, requires every custom error types implements method for stack tracing is not practical, it should letting a default implementation to handle stack tracing in error interface. Unfortunately, the role of Go interface need to be simple and pure, so the only thing that interface need to do is defining method’s name and signature.

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

Panic, recover, defer, makes people pain 🔗

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

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

When recover() added in Go, the panic and recover becomes a partner, its behavior is similar to try-catch exception handling, panic-recover look 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 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 deferred function are discarded when the function completes, and deferred function can access and modify named return values before they are returned. If we want to return something in deffered function, we need set data to named return value.

In order to accomplish 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 type of interface{}, so we need to check if the value thrown by panic is a 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 with recover() is like a block of finally, it doesn’t sure if there are panic throws or not, so it need to check the return value of recover() and see if there has any value thrown by panic().

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

Further reading: Panics, Stack traces and how to recover

Interface is simple, but too simple 🔗

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

Go is not a 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 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 dealing with interface in standard and other community packages, I found some interesting phenomenon.

  • 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 interface as a base class, but it’s not.
  • When things get worse, people use reflect package to solve problem at runtime, by a bunch of conditional statements.
  • People spend lots of times to figure out which struct implements which interface, because Go claims it doesn’t want to provide the keyword “implements”.

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

But generics is so quite useful in some scenarios which 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 implements 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 compiler know, then compiler can check whether the operation of interface is valid at compile-time.

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

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

Further reading: Interface values are valueless , why Go doesn’t have generics , Effective Go : interfaces and types

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 default implementation of method, it let us hard to add new methods in interface without breaking compatibility of existing type which implemented this interface.

It might not a big deal if the interface is not exported outside of package because we can update all the types in 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 new method to this interface will break code of the person using our package.

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

When Go v1.13 released, it introduces error chaining mechanism by adding a new Unwrap method to errors package. Why not add this method to error interface but add it in errors package instead? The reason is I mentioned above, they can’t, because it will break all existing code.

Further reading: Go : the Good, the Bad and the Ugly

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 value unchangeable, the types of scale value 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 not be constants is: Go doesn’t support it 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 has many ways to not use exported variables within package, but in real world, sometimes we will need to export some variables for providing 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 for loop 🔗

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

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

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

Besides, these first-class members behaves 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 special one, it’s immutable, it is passed by value, but it only copy the length of string and the pointer of string. And it only create 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 a bad thing, except it is a bit annoying.

Go forces us to use upper case letter in 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 network by JSON/Protocol Buffers…etc, it is a bit annoying to add struct tags in struct fields.

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

Export it or not, no private attribute for struct field 🔗

In package scope, every thing is visible to each other like big brother C, but unlike C, Go also provides a simple visibility rule: makes it 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 called it as private structure field, is a common concept, and it really helpful 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 problem. But when we pass a slice into another function, and try to use append() or copy() to modify slice, we will encounter some confusing and unintuitive problems.

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

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

The structure of 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 which point to the slice.array, you can thinking of a slice value as unsafe.Pointer((*slice)(unsafe.Pointer(&A).array).

Let’s see a example:

When ‘A’ created with length and capacity of 4, and want to append a byte into ‘A’, append() detects the capacity of ‘A’ is not enough, so it allocate new memory for underlying array, So the address of underlying array of ‘A’ and ‘B’ is different, and the address 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 same underlying array and updated length value.

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

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

Technically, nothing wrong about 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: Go slices, functions, append and copy , introduction to slices

Beautiful and Mess 🔗

No function overloading and default parameters 🔗

According to the FAQ of Go, they said:

… Experience with other languages told us that having a variety of methods with the same name but different signatures was occasionally useful but that it could also be confusing and fragile in practice. Matching only by name and requiring consistency in the types was a major simplifying decision in Go’s type system.

When I read articles about GO, I have seen many times about the official obsession with the goal of simplifying, sometimes I agree, and sometimes I think it might just be paranoid.

In honest, I agree that force programmer to create different name of functions/methods if these functions accepts different parameters will be helpful to organize the code and avoids some terrible implementation.

But wait, Go provides support for variadic function, in a world without function overloading and default parameters, if we want to do something clearly, we can’t write like the following snippet:

func (meow *Cat) Feed(food GiantCan, qty uint8 = 1)
func (meow *Cat) Feed(food DryFood, gram uint32 = 300)
func (meow *Cat) Feed(food RawMeat, gram uint32 = 200, heat bool = true)

And we need to write like this:

func (meow *Cat) FeedGiantCan(food GiantCan, qty uint8)
func (meow *Cat) FeedDryFood(food DryFood, gram uint32)
func (meow *Cat) FeedRawMeat(food RawMeat, gram uint32, heat bool)

The above snippet looks silly, so someone might choose to use variadic function like this:

And because Feed(food Food, options …interface{}) doesn’t describe itself well, so we need to write a lot of content in the document to describe how to use this method. In this case, use function overloading and default parameters will be more elegant and reduce parameter check tasks.

Although I know that variadic function and function overloading are total different concepts, but, life will find its way out. and most important point is:

I think it should allow people to take the responsibility for good design, instead of avoiding abuse by not providing features.

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: v2 Go modules , Go modules with private Git repos

Conclusion 🔗

The 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.

Share:
waffarx cash back