Why I Don't Like Golang (2016)
source link: https://www.tuicool.com/articles/ZVJZzau
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
When I first started programming in Go, my summary of it was, “The good things are great and the bad things are weird and I can live with them.” After another three years and a few large projects in Go, I no longer like the language and wouldn’t use it for a new project. Here are 10 reasons why, in no particular order.
-
Go uses capitalization to determine identifier visibility. Those that start with a lower case letter are package-private, and those that start with a capital are package-public. The purpose presumably is to cut down on the
public
andprivate
keywords, but capitalization was already used to mean other things: Classes are capitalized and constants are entirely in upper case. It’s a recurring source of discomfort to me to name global constants entirely in lower case.Things get worse if you want to have a private struct, since it must be in lower case. For example you might have a
user
struct. What do you name the variable? Normally I’d call ituser
, but not only does that look confusing, but can cause parse errors when the compiler confuses them:type user struct { name string } func main() { var user *user user = &user{} // Compile error }
Idiomatic Go uses shorter names like
u
, but I stopped using one-letter variables 35 years ago when I left TRS-80 Basic behind.There are practical considerations, too. Frequently I start out with a private field or struct name and later decide to make it public, forcing me to fix all usage of the identifier. And even if you want to keep your fields private, you’re forced to make them public if you want to use the
json
package. In fact I had a struct with 74 private fields that I wanted to serialize withjson
and was forced to make all fields public and update all uses throughout the large app.Capitalization also restricts visiblity to two levels (package and completely public). I frequently want file-private identifiers for functions and constants, but there isn’t even a nice way for Go to introduce such a thing now.
-
Structs do not explicitly declare which interfaces they implement. This is done implicitly by matching the method signatures. This design makes a fundamental error: It assumes that if two methods have the same signature , then they have the same contract . In Java when a class implements an interface, it’s doing more than telling the compiler that its method signatures will match. It’s also making a promise that the contracts of the methods were implemented. If the method returns a boolean, the comments in the interface will specify what the value means. (E.g., true on success, false on failure.)
A Go struct might implement the same method signature but reverse the meaning of the return value. It’s free to do that – it never promised anything. The Java class can do this too, of course, but then it’s clearly a bug in the class. In Go the bug was introduced by whoever cast the object to the interface without first verifying the contractual compatibility of every method. This burden shouldn’t fall on every API user. It should be done once by the implementor of the struct and declared in the code.
-
Go doesn’t have exceptions. It uses multiple return values to return errors. It’s far too easy to forget to check errors:
db.Exec("DELTE FROM item WHERE id = 2")
The
DELETE
is misspelled, and there will be nothing to tell you that something went wrong. If this is part of a large transaction, the whole transaction will silently do nothing. Good luck figuring out why. Error return values are fine but the programmer should be forced to check them (or assign the error to_
).Additionally, the idiom of returning either a value or an error:
user, err := getUserById(userId)
invites bugs because there’s nothing to enforce the fact that exactly one of
user
anderr
contain valid values. With exceptions theuser
variable is never assigned-to (so reading from it will generate a warning), and with algebraic sum types (unions) the compiler will ensure that only the correct one can be accessed. -
There’s far too much magical behavior. For example, if I name my source file
i_love_linux.go
, it won’t get compiled on my Mac. If I accidentally name a functioninit()
it’ll get run automatically. This is all part of the “convention over configuration” movement. It’s fine for small projects but bites you on large ones, and Go was meant to address the problem of “programming in the large”. -
Partly because of the capitalization problem, it’s easy to end up with several identically-named identifiers. It’s actually quite easy to have a package, struct, and variable all called
item
. In Java the package would be fully-qualified and the class would be capitalized. Sometimes I find it hard to read Go because I can’t always tell at a glance what scope an identifier belongs to. -
It’s difficult to generate Go code automatically. The compiler is strict about warnings, meaning that unused imports and variables cause the build to fail. But when generating a large file it may not be initially clear which packages need importing. Furthermore you may have two packages whose names clash, and it’s not easy to resolve this automatically because you can’t even know the imported symbol if you only know the package name. (The package imported as
github.com/lkesteloot/foo
is permitted to actually be packagebar
, and some open source libraries do this.) Even if you could figure that out, the generating program would be forced to alias the imports to avoid the conflict. In Java all these problems are solved by importing nothing and always fully-qualifying all class references, something not permitted in Go. -
There’s no ternary (
?:
) operator. Every C-like language has had this, and I miss it every day that I program in Go. The language is removing functional idioms right when everyone is agreeing that these are useful. Instead of the functional and elegant:var serializeType = showArchived ? model.SerializeAll : model.SerializeNonArchivedOnly
you’re forced to this imperative verbosity:
var serializeType model.SerializeType if showArchived { serializeType = model.SerializeAll } else { serializeType = model.SerializeNonArchivedOnly }
I see no good argument for omitting this operator.
-
The
sort.Interface
approach is clumsy. If you have 10 different structs that you want to sort (in arrays), you have to write 30 functions, and 20 of those are trivially similar (length and swap). Also, they’re hard to compose: You can maybe delegate to anotherLess()
, but you have to trust that itsLen()
andSwap()
are compatible. And finally it just looks weird, because the cast looks like a function call:sort.Sort(sort.Reverse(UsersByLastSignedInAt(users)))
The tried and true approach of providing a compare method works great and has none of these drawbacks.
-
Import versioning and vendoring is terrible. This is well-covered ground elsewhere, but frankly it’s 2016 and it’s not acceptable to release a new language without a solution for this. And not only does Go not have a solution, but its import system is actively hostile to vendoring.
-
No generics. Also well covered elsewhere, but again I can’t really use a language that doesn’t let me implement a generic
Stack
class. The solution normally given is to open-code the stack using slice functions likeappend()
, but again it’s 2016 and I want to writepush()
andpop()
, not:stack = append(stack, object)
and:
object = stack[len(stack) - 1] stack = stack[:len(stack) - 1]
I was surprised how many third-party libraries use
interface{}
throughout. This is a sign of a poorly-designed type system. -
Okay a bonus reason! This is very minor, but points to a failure on the part of the designers to understand how programmers work. The
append()
function extends an array, returning the new array:users = append(users, newUser)
The problem is that the following code will nearly always work:
append(users, newUser)
The
append()
function modifies the array in-place when it can, and only returns a different array if it has no place left. You couldn’t ask for worse API design. How many bugs are caused by forgetting to assign the result? A lot, because initial testing may not trigger a resize. Either the construct should work differently (modifying the first argument in place) or should force the programmer to use the return value.Update: They later modified the compiler to generate an error if the
append()
function’s result is not assigned to anything.
Here’s a summary of my recommendations for Go usage: If your program is small and can mostly be described by what it does , and if it doesn’t interact much with data outside itself (databases, the web), then Go is fine. If it’s large, if it has non-trivial data structures (even something simple like a tree), or if it will be dealing with a lot of data from the outside, then the type system will fight you for no benefit and you’re better off using a different static language (where the type system helps) or a dynamic language (where it doesn’t get in your way).
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK