I've written some negative comments about the Go language before. In hindsight, while most of those reviews were true, they were not convincing to some people because they were so vocal and did not point out specific problems. After a few months of actually using Go to build websites, I think it's time to give it a more "objective" review.
Go does have its advantages over C and C++, that's obvious. It also has a few advantages over Java, but it's relatively more of a shortcoming. So my preference for Go is a little bit lower than Java.
The strengths of the Go language over C and C++ are of course its simplicity and garbage collection. Since C and C++ are designed with a lot of historical legacy issues, Go does seem more elegant and simple. The code in Go also seems to be simpler than Java code that makes heavy use of design patterns. Also, Go's garbage collection mechanism is much less taxing on the programmer's mind than the full manual memory management of C and C++.
Note, however, that these "advantages" are relative to languages like C. If compared to some other languages, Go's advantages may be insignificant, or even a step backwards in history.
Syntax
Go's simplicity is reflected in some aspects of its syntax and semantics, Go's syntax is slightly better than C and has a few more convenient designs than Java, but there are also "regressions". And these regressions are not seen by many as regressions, but rather as progress. I will now list a few that come to mind at the moment.
Progress: Go has syntax support for a struct literal-like construct, for example, you can write code to construct an S struct like this:
S { x: 1, y: 2, }
This is a nice convenience improvement over Java's ability to create objects only with constructors. These things may be borrowed from the design of languages like JavaScript.
Backwards: the type is placed after the variable without the separator. If the variable and its type were written like Pascal, e.g. x : int, that might be fine. Go, however, writes x int without the colon and allows writing like x, y int. This syntax, when combined with var, the function argument, has a disruptive effect. For example, you can write a function that starts like this:
func foo(s string, x, y, z int, c bool) {
...
}
Note the position of x, y, z. It's actually quite confusing. Because when I see x, I can't immediately see what type it is from the symbol (, y) that follows it. So the way I recommend writing it in Go is to keep x and y completely separate, like in C and Java, but with the type written after it: x, y, z.
func foo(s string, x int, y int, z int, c bool) {
...
}
This makes it clearer, although I'm willing to write more colons. Each argument is in "name type" format, so I can see at a glance that x is int. It's a few more words, but it saves the overhead of "eyeball parse code".
Backwards: type syntax. go uses syntax like []string to represent types. Many people say that this syntax is very "consistent", but after some time I have not found what they call consistency. It's actually hard to read because there is no clear separator between the parts of the type, and when paired with other symbols like *, you need to know some precedence rules and then go through a lot of trouble to do the "eyeball parse". For example, you often see types like []*Struct in Go code, and note that *Struct has to be combined before it can be used as a "type parameter" for []. This syntax lacks enough separators to act as "boundary signals" for reading, which makes it hard to read once the types that follow become complex. For example, you can have types like *[]*Struct or *[]*pkg. So it's not really as clear and simple as writing it like C++'s vector<struct*>, and even less clear and simple than Java's or Typed Racket's type writing.
Backwards: excessive "syntax overloading" with keywords like switch, for, etc. The switch keyword in Go actually contains two different things. It can be a normal switch (Scheme's case) in C, or it can be a nested branching statement like Scheme's cond. These two statements are actually semantically completely different, but Go's designers have combined them to make it look simple, which actually causes more confusion. This is because, even if you combine them, they are still two different semantic constructs. The result of combining them is that every time you see a switch you need to distinguish the two different structures from each other by the difference in their "heads", adding to the overhead of the human brain. The right way to do it is to separate them, like Scheme does. In fact, I sometimes make the same mistake when I design languages, thinking that the two things are "essentially" the same, so I combine them into one, only to find out after a while that they are actually different. So don't underestimate Scheme, many things you think are "new ideas" have actually been abandoned by its very strict committee in the long history.
There are other syntax design issues in Go, such as forcing { after a line and not allowing line breaks, nesting assignments at the beginning of if statements, and so on. These attempts to make the program look short actually reduce the fluency of the program.
So all in all, Go's syntax can hardly be called "simple" or "elegant", it's actually below Java in simplicity.
Tool Chain
Go provides some convenient tools. For example, gofmt, godef, etc., make Go code programming an improvement over editing C and C++ with Emacs or VIM alone. Using Emacs to edit Go already enables some IDE features, such as precise definition of jumps, etc.
These tools are nice to use, but they are quite different from IDEs like Eclipse, IntelliJ and Visual Studio. Compared to IDEs, Go's toolchain lacks various basic features, such as listing all the locations where a variable is referenced, renaming and other refactor features, a good debugger (GDB is not so good), and so on.
Go's tools don't feel very mature, and sometimes you find that there are several different packages for the same problem, and it's hard to figure out which one is better. And they are not so reliable and easy to configure, they need to be tossed around. Every little feature you have to find a package from everywhere to configure. Sometimes a tool doesn't work after it's been configured, and you have to wait a long time to find out what the problem is. This kind of unorganized and unplanned tool design is hard to surpass the consistency of professional IDE vendors.
Go provides a convenient package mechanism for importing Go code directly from a GitHub repository. But I've found that often this package mechanism is more of a hassle and a dependency. So Go advocates have designed tools like godep to get around these problems, and godep itself has caused some odd problems, causing new code to not actually compile and generating inexplicable error messages (probably due to a bug in godep).
I find that a lot of people who see these tools are always very enthusiastic that they will make the Go language dominant, but they are still very far from it. And with so many problems with such a young language, I think all these troubles will add up to a lot of trouble over the years.
Memory Management
Go has a garbage collection (GC) mechanism compared to the completely manual memory management approach of C and C++. This mechanism greatly reduces the burden on the programmer's mind and the chance of program errors, so Go is an improvement over C/C++.
Go's garbage collector is a very primitive mark-and-sweep, which is still in its infancy compared to language implementations such as Java, OCaml, and Chez Scheme.
Of course if you do run into GC performance problems, you can partially improve the efficiency of memory collection by doing a lot of tuning. I've seen articles written about how they do this, but the existence of such articles shows that Go's garbage collection is still very immature, and GC is something I don't think should be left to the programmer most of the time, otherwise it loses a lot of its advantages over manual management. So Go code still has a long way to go if it wants to be in a more real-time situation.
The Go language is becoming more and more ambiguous to me because of the lack of advanced GC with high level abstraction, so Go can't really replace C and C++ for constructing the underlying system.
No "generics"
Go lacks generics compared to C++ and Java, and while some people hate Java's generics, it's not a bad thing in itself. generics is actually what's called parametric polymorphism in functional languages like Haskell, and it's a very useful thing, but it's been copied by Java and sometimes doesn't do everything right. It's a very useful thing, but it's been copied from Java and sometimes not done all right. Because generics allows you to use the same piece of code to handle many different data types, it provides a convenient way to avoid duplication, replace complex data structures, etc.
Since Go doesn't have generics, you have to write many functions over and over again, each with a different type. Or you could use the empty interface {}, which is really the C equivalent of the void* pointer. With it, the type of the code cannot be checked statically, so it is not as rigorous as generics.
Compared to Java, many of Go's data structures are "hard code" into the language, even creating special keywords and syntax to construct them (such as hash tables). Once you need to define similar data structures yourself, you need to rewrite a lot of code. And since there is nothing like Java collections, there is no easy way to swap out complex data structures. This is a big obstacle for constructing programs like PySonar that require a lot of experimentation to choose the right data structure and need to implement special hash tables and other data structures.
The lack of generics is one problem, but a more serious problem is the blind rejection of such language features by Go's designers and its community. When you mention them, Go supporters will tell you with disdain, "I don't see the point of generics!" This attitude is more harmful than the shortcomings of the language itself. After a long time the designers of Go began to think about adding generics, and then because Go's syntax was designed to cut corners, and because the exceptions created by the lack of generics (such as Go's map syntax design) are already heavily used, I think it has become very difficult to add generics.
Go, like Unix, has been burdened with a heavy history of not learning from its predecessors in its early days.
Multiple Return Values
Many people think that Go's multi-return-value design is an improvement, but there's a lot of fishy stuff going on here. Not to mention that it's not new at all (Scheme has had let-values for a long time), Go's multi-return values are used in a lot of wrong places - Go uses them to represent error messages. For example, the most common structure in Go code is:
ret, err := foo(x, y, z)
if err != nil {
return err
}
If the call to foo produces an error, then err is not nil. go requires you to use it after defining the variable, otherwise it reports an error. This way it "happens" to avoid the case where an error err occurs and is not checked. Otherwise, if you want to ignore errors, you have to write:
ret, _ := foo(x, y, z)
This way, when foo goes wrong, the program will automatically pawn off at that position.
I have to say that this "misrepresentation", while seemingly feasible, is very uncritical from a type system perspective. It is not designed for this purpose at all, so you can easily find ways to make it fail. And since the compiler only checks to see if err is "used", it doesn't check to see if you've checked "all" possible error types. For example, if foo may return two errors, Error1 and Error2, you can't guarantee that the caller has completely eliminated the possibility of these two errors before using the data. So this error checking mechanism is not as rigorous as Java's exceptions.
Also, ret and err are defined at the same time, and only one of them is not nil at a time. This "or" relationship is not guaranteed by the compiler, but by programmer's "convention". These combinations create quite a bit of confusion, making you unsure whether return is intended to return an error or a valid value every time you see it. If you realize that this "or" relationship actually means that you should only use one return value for them, you know that Go is actually misusing multiple return values to represent possible errors.
In fact, if a language has a "union type" type system like Typed Racket and PySonar support, this multiple return value doesn't make sense. Because with a union type, you can use only one return value to represent valid data or errors. For example, you could write a type called {String, FileNotFound} to indicate that a value is either a String or a FileNotFound error. If a function is likely to return an error, the compiler forces the programmer to check for all possible errors before using the data, thus avoiding all of the above confusion. For those interested in union type, you can check out Typed Racket, which has the most powerful type system I've seen to date (beyond Haskell).
So it's safe to say that Go's multiple return values are actually "mistyping" half of what you hit, and then continuing to mistype instead of aiming for the bullseye.
Interface
Go uses an object-oriented design based on interfaces, which you can use to express concepts that you want to abstract.
However, this interface design is not without its problems. For one thing, unlike Java, implementing a Go interface does not require explicit declarations (implementations), so you may "happen" to implement an interface. This uncertainty is counterproductive to understanding the program. Sometimes you modify a function and find that it doesn't compile, complaining that some location is not passing the required interface, but the error message doesn't tell you exactly why. It takes a bit of poking around to find out why your struct no longer implements an interface that was previously defined.
Also, some people use interfaces, many times just to pass some function as an argument. I sometimes don't understand why something that is so simple for a functional language has to be implemented in Go by defining a separate interface. This makes the program less clear than a functional language, and it's also very inconvenient to modify. There are a lot of redundant names to define and redundant work to do.
A related example is Go's Sort function. Every time you need to sort an array of some type T, like []string, you need
- Define another type, usually called TSorter, such as StringSorter
- Define three methods for this StringSorter type, called Len, Swap, Less
- Cast your type like []string to StringSorter
- Call sort.Sort to sort this array
Think about how simple sort is in a functional language. For example, Scheme and OCaml can both be written directly like this:
Here Scheme passes the function < directly to the sort function as an argument, without wrapping it in any interface. Did you notice that the three methods in Go's interface are supposed to be passed directly to Sort as three arguments, but due to design pattern and other limitations, Go's designers "packaged" them as an interface? And since Go doesn't have generics, you can't write these three functions like a functional language, accepting the "elements" of the comparison as arguments, but must use their "subscripts". Since these methods only take subscripts as arguments, Sort can only sort arrays. Also, since Go is designed to be more "low-level", you need two other arguments: len and swap.
In fact, this interface-based design is a far cry from functional languages. It's also a step backwards from Java's interface design.
goroutine
Goroutine is arguably the most important feature of Go. Many people use Go because they hear that goroutine can support so-called "big concurrency".
First of all, this kind of concurrency is nothing new. Everyone who understands programming language theory knows that goroutines are really just user-level "continuations". System-level continuations are often called "processes" or "threads", and continuations are something that functional language experts know all too well, such as my former mentor Amr Sabry, who was one of the top experts on My former mentor Amr Sabry was one of the top experts on continuation.
The Node.js kind of "callback hell" is actually a common technique within functional languages called continuation passing style (CPS). Since Scheme has call/cc, it can theoretically achieve large concurrency without the CPS style code. So as long as functional languages support continuation, it is easy to achieve large concurrency, and perhaps even more efficient and useful. For example, an implementation of Scheme, Gambit-C, could be used to implement something with large concurrency, and Chez Scheme might be possible, but that remains to be confirmed.
Of course there may be differences in efficiency, but I'm just saying that goroutines aren't really as new, revolutionary, or unique as many people think. Other languages can add it if they have enough motivation.
defer
Go implements a defer function to avoid forgetting to cleanup after a function error. However, I've found that this defer function has a tendency to be abused. For example, some people make defers for actions that are not cleanup, and after a few defers have accumulated, you can no longer tell at a glance which piece of code runs first and which runs second. The position in the front of the code can actually run later, violating the natural position of the code order relationship.
Of course you can blame the programmer for not understanding the true purpose of defer, but once you have something like this in place, someone will want to abuse it. People who are eager to try to take advantage of every feature of a language are particularly fond of doing this sort of thing. I'm afraid it will take many years of experience before someone writes a book to educate people about this problem. Until there is a unified "code specification", I predict that defer will still be heavily abused.
So we should think about whether defer has more advantages or disadvantages to avoid possible resource leaks.
Library Code
The design of Go's standard library has a strong Unix flavor in it. The library code has a lot of inconveniences compared to Java and other languages. Sometimes it introduces functional language approaches, but due to the limitations of Unix thinking, not only does it fail to take advantage of the benefits of functional languages, but it also leads to a lot of complexity in understanding them.
One example is the way Go handles strings. In Java, each character in a string is by default a Unicode "code point". However, in Go, each element of the string type is a byte, so you have to cast it to a "rune" type each time to properly traverse each character and cast back. This way of thinking about everything as a byte is the Unix way of thinking, and it causes overly primitive and complex code.
HTML template library
I have used Go's template library to generate some pages. It's a "mostly usable" way of templating, but it's quite inadequate compared to many other mature technologies. I was surprised to learn that the code enclosed in Go's template is not the Go language itself, but a rather weak language, sort of a degenerate Lisp, except that the parentheses are replaced with things like { {...} }.
For example, you could write a web template like this:
{{define "Contents"}}
{{if .Paragraph.Length}}
<p>{{.Paragraph.Content}}</p>
{{end}}
{{end}}
Since each template accepts a struct as populated data, you could use code like .Paragraph.Content, however this is not only ugly, but makes the template inflexible and poorly understood. You need to put all the data you need into the same structure to access it from inside the template.
Anything longer than one line of code, while perhaps expressible in this language, is generally avoided by writing some "helper functions" in the .go file to avoid the weaknesses of the language. Use them to generate data to put into a structure and then pass it to the template to be able to express some of the information that the template needs. Each of these helper functions requires a certain amount of "registration" information to be found by the template library. So all this complexity adds up to make Go's HTML template code quite cumbersome and confusing.
I've heard that someone is working on a new HTML template system that supports direct Go code embedding. These efforts are just getting started, and it's hard to say what they'll end up being. So to make a website, I'm afraid it's best to use a more mature framework in another language.
Summary
Elegance and simplicity are both relative. While Go surpasses C and C++ in many respects, and is better than Java in some respects, it does not compare to the elegance of Python, which is inferior to Scheme and Haskell in many respects, so all in all, Go's simplicity and elegance are on the lower end of the spectrum.
Since there are no obvious advantages, but there are various problems that are not found in other languages, I prefer to use a language like Java for practical projects at the moment. I don't feel that Go and its toolchain can help me write sophisticated code as fast as PySonar. I've also heard of people using Java for large concurrency and don't see any significant shortcomings compared to Go.
Alan Perlis says that language design should not be about piling up features, but about trying to reduce weaknesses. From this perspective, the Go language introduces one or two new features, while introducing a fair number of weaknesses.
Go may temporarily have particular strengths in some individual cases that can be used alone to optimize certain parts of a system, but I would not recommend using Go to implement complex algorithms and entire systems.