2018-03-26

Don't Go, let's Go

Go mascot

My few encounters with Go were pleasant and I stick to the idea that it is a good language.

Ten Reasons Why he doesn't Like Golang is a list of (guess!) ten points explaining why the author

wouldn't use [Go] for a new project.

I'm going to comment these points — likely you need to read the article.

Also, it appears on HN two times.

Most of these points, if not all, are non-issues or just the personal taste of the author.

O my, Capital and lowercase and clashes

Go uses the convention of the uppercase initial letter of a symbol to specify its visibility. Nobody should be in love with how a symbol looks and think like “hey, I won't use Sather because classes must be all uppercase!”, or things like this.

They exist languages which are case insensitive (Ada, Fortran…), or where the case of the first letter has other meaning (Erlang, Prolog…). Would you rule them out just because you can't use User and user the way you can in Java?

This is javaish:

   User user = new User();

In Go you don't do that, and one reason is that the first letter of a symbol specifies its visibility. Maybe the author would prefer a syntax like

private type User struct {
    name string
}

func main() {
    var user *User
    user = &User{}
    // ...
}

With this, he can imitate Java.

There are several possible alternatives, one of which is to use u instead of user: it is (he says) idiomatic Go to use shorter names:

Idiomatic Go uses shorter names like u, but I stopped using one-letter variables 35 years ago when I left TRS-80 Basic behind.

Needless to say, shorter doesn't mean one-letter only, and though its idiomatic, it isn't enforced, hence you can go for longer names! You can pick usr (very unixish), but also anUser or an_user.

You change your mind about the visibility of a symbol? You don't need to refactor your large project by hand. You can learn of automated renaming and tools like gorename.

“Capitalisation” says the author “also restricts visib[i]lity to two levels (package and completely public)”. Not sure of what's the problem here, maybe because I use C more than other languages. Large projects in C do exist, making it clear that this issue can't stop from making them.

No Java-like interfaces, no party

This point is very, very poorly stated (to say the least), and also it is about a non-issue: it is enough to think about it, or to read the Go FAQ.

When you have an interface like the following,

type geometry interface {
   area() float64
   perim() float64
}

every type which implements those two methods implicitly satisfies the interface. In Java you have to declare it explicitly:

public class Rect implements Geometry
{
  // ...
}

Now let's suppose we have an antigeometry interface:

type antigeometry interface {
  area() float64
  perim() float64
}

This has the very same methods with the same signatures, but in the antigeometric world area() is indeed the perimeter, and perim() is indeed the area. In Go, if a type satisfies geometry, then satisfies also antigeometry. It's like if in Java we have written

class Rect implements Geometry, Antigeometry
{
  // ...
}

But we have geometric figures which aren't antifigures, hence don't satisfy Antigeometry, and antigeometric figures which aren't figures, hence don't satisfy Geometry. In Java:

class Rect implements Geometry
{
  // ...
}

class Antirect implements Antigeometry
{
  // ...
}

How do you express the same in Go? As explained in the FAQ.

type geometry interface {
   area() float64
   perim() float64
   ImplementsGeometry()
}

// ...
func (r rect) ImplementsGeometry() {}

And something similar for an antirect. With this idiom the interfaces aren't “compatible” anymore and you can't cast one into another. You can't do it in Java, though, and the compiler won't stop you (but you'll obtain a run-time exception.)

    static void measure(Geometry g) {
       // ...
    }

    static void measure(Antigeometry g) {
       // ...
    }

   // ...
   Circle c = new Circle(radius);
   measure((Antigeometry)((Geometry)c));
   measure((Anticircle)((Antigeometry)((Geometry)c)));

In Java, Geometry and Antigeometry are “compatible”, so casting Geometry into Antigeometry is OK (and you can continue casting into any class with an implements Antigeometry declaration). At run-time, a concrete object knows what its class declared to have implemented, and an exception is thrown.

To spot this error at compile time, you need to make Geometry and Antigeometry not compatible: change the names of the methods (from area() to antiarea()) — a trick you can use for Go, too —, or add an unique method (a method signature none of the other interfaces have).

In Go there's another idiosyncrasy.

type rect struct {
    width, height float64
}

type antirect struct {
    width, height float64
}

These two types are identical (see type identity and conversions), so you can convert a figure (rect) into an antifigure (antirect) as easy as this:

   antimeasure(antirect(r));

Same idea used for interfaces applied to “compatible” structs:

type rect struct {
    width, height float64
    idRect interface{}
}

type antirect struct {
    width, height float64
    idAntirect interface{}
}

More on the reason why there isn't an implements clause:

No exceptions, no party

Handling errors have been done without exceptions since forever. Go has no exceptions, as C and other languages.

This in intentional, of course, and another matter of tastes if you don't like it or you think exceptions are the path towards better software (are they?).

According to the author, it's

far too easy to forget to check errors.

Exceptions aren't born to fix programmers' deficiencies or memory problems. Every programmer should know that each “operation” may fail and that we should deal with this someway.

When you make a call, you must know what the function returns, if it can fail, and of course what it does. You don't write a code like

  db.Exec("...")

unless you are a total rookie, or you are accustomed to air-headed, I-don't-care kind of programming — exceptions won't help you to make better software then, because you won't care to use them correctly, too.

Go has even a built-in error interface, and the doc for DB.Exec clearly says it returns two things, Result and error.

This “it's far too easy to forget to check errors” is pretentious, to say the least.

Error return values are fine but the programmer should be forced to check them

Programmers should be forced to avoid bugs, but this isn't possible. I don't think that it would make sense to force a check; there are cases when I don't do that, e.g. if the call is the last one in a function and all I need to do is to propagate the results up to the caller.

Another critique addresses the idiom

user, err := getUserById(userId)

Because according to the author it invites bugs.

because there’s nothing to enforce the fact that exactly one of user and err contain valid values

If err isn't nil, then user hasn't a valid value. Simple rule. You can learn it and get used to it. If you access user before checking err, or if you use it even if you checked and err isn't nil… Then I suppose you can do all kind of horrific programming mistakes in Java too, and I won't trust your software.

With exceptions the user variable is never assigned-to (so reading from it will generate a warning),

Never? It depends on how you write the code… And where in the code you would try to read this non-assigned variable?

What if the user variable was used before and contains already a valid value?

        User u = getUserById(0); // user id 0 always exists...
        // do something with u, then get the next users
        try {
            for (int j = 1; j < 1000; ++j) {
                u = getUserById(j);
        // do something with u
            }   
        } catch (Exception e) {
            System.out.println("Exception " + u.id());
        }
}

The user u variable is assigned-to user 0 or to the user before the one on which the call has thrown. It is far too easy to write crappy code like this, and also to use generic Exceptions…

Not everyone misses exceptions; plus, there are reasons to avoid them.

Conventions bite me

There's this _arch.go convention to say that the file must be build for the architecture “arch”.

So i_love_linux.go don't get compiled on a Mac. Is it that hard to avoid file names ending in _arch.go, _os.go, or _os_arch.go? Which kind of (large) project requires a file name with those endings without them named so because of Go's conventions?

And what about init()?

The name init() is special, as it is main() — also, there are reserved keywords… Every language has this kind of thing you must know. You DON'T accidentally give name to functions. You choose a name, fully aware of its meaning and of the conventions of your language of choice.

It isn't hard to remember that a name like init() has a special meaning.

It is absolutely ridiculous to believe this can hurt a large project.

This is all for this fourth point.

I don't name thing differently

This is another foggy point to me.

Partly because of the capitalisation problem, it’s easy to end up with several identically-named identifiers.

The “capitalisation problem” is this: you are bound to use lowercase or uppercase first letter according to the desired visibility. Now, given a symbol N characters long, you are a little bit less free on the case of the first one, that is, on 1/N of the total length.

If you have to name a source of clash for identifiers, would you mention that convention? I think the answer is NO. Rather, think about the other N-1 characters!

Fully-qualified names are a pain. It's unbelievable Java doesn't allow aliasing/renaming. (Other JVM based languages do, e.g. Kotlin…)

This Java code doesn't show a feature, but a problem to me:

my.pack.has.the.MyClass i1 = new my.pack.has.the.MyClass();
my.pack.has.that.MyClass i2 = new my.pack.has.that.MyClass();

Usually you don't do that. Instead you do:

import my.pack.has.the.MyClass;
//...
MyClass instance = new MyClass();

But in this case you can't import and use both unqualified, and aliases aren't possible. You must use the fully-qualified name, which adds a lot of noise and diminishes source readability.

Go chose a more practical and convenient way:

import "fmt"
import "long/path/to/your/pack/mypack"
import mypack2 "long/path/to/your/alt/mypack"

// ...
   fmt.Println("hello")
   mypack.SomethingUseful()
   mypack2.SomethingUnuseful()

You read the source and you understand what's what.

Sometimes you can't tell at a glance what scope an identifier belongs to, but you take a look at the imports and everything will become clear.

Don't spy over my shoulder

Go wants you to keep your code as clean as possible, without garbage which should have been temporary but then stays there forever.

Now, if you put an import you don't use, go build complains.

import _ "archive/tar"

This “fixes” the “problem”. If you need to.

What if then I want to actually use it? Just do it and remove _, or replace it with tar or another alias you like.

No ternary, Father, Son, Holy Host

Conditional expressions are nice, I like them (though you shouldn't abuse them), but if a language hasn't them, this isn't a serious reason to dislike that language. This is a minor point, and a matter of taste.

Anyway, what I do every time or everywhere I don't want to use, or I can't use a conditional expressions, is pre-assigning the most likely value1, or the default value; to be overwritten if the condition is true:

serializeType = model.SerializeAll
if !showArchived {
  serializeType = model.SerializeNonArchivedOnly
}

Sorting needs specialisation

This could be the first actual point.

However, it isn't really an issue the fact that casting looks like a function call. You can think it is, and it won't change what it does.

Import versioning and vendoring is terrible

I don't have a clue of the issue, I suppose I haven't crashed into it, nor I have analogies from other programming languages for the import versioning and vendoring. But I suppose we can live in Go even if these features are behind its peers.

Who wants to live on generics?

I put the generics requirement on the same level of the exceptions requirement. It's plenty of things you can do very well without them.

Eleven, the strangest thing

This is very minor… why, the others were HUGE?!

Since I am mostly a C programmer, and I like it (which doesn't mean that I don't like other languages or that I believe C has everything perfect), this bonus point made me laugh.

Have you ever heard of realloc?

It can extend the block or move it somewhere else. It isn't hard to handle this freak correctly, even if indeed many programmers are careless, or they ignore its quirks — if a language isn't fool-proof, you don't blame the language, do you?

  y = realloc(x, double_size)
  if (y != NULL) {
    x = y;
  } else {
    free(x);
    x = NULL;
  }

Likely you wrap this into a function, unless you want to do something different when y == NULL.

Anyway, the following can often work, but it's wrong and horrific…

  realloc(x, double_size);
  // do something with this enlarged buffer pointed by x

We know that, and we don't do that — even if indeed some careless programmer does this:

   x = realloc(x, double_size);

Which doesn't work when realloc fails2. (But often one thinks that if realloc fails, it means the world is ending… so why should we care about realloc?)

Final words

If you use a language long enough, you can compile a list of ten or eleven points to make it look ugly, not the best fit for whatever you tried it on.

Maybe it happens because it really isn't the right tool for the kind of projects you worked on. But it could also be because it isn't the right tool just for you and the way your programmer mind works, or your team works.

Eleven points, and programmers with different tastes or background or mindset can make fun, so to speak, of at least eight of them.


  1. Not saying it makes sense or is some kind of meaningful optimisation; it's just a choice.

  2. What I mean here is that x will be NULL; you check it (you don't forget to check it) and do whatever you need to do; but realloc() doesn't free the original segment, therefore you have a memory leak. If you're going to terminate the program, this won't be an issue on many systems because all the memory will be taken back (by the system) once the program ends. You shouldn't rely on this, though.

No comments:

Post a Comment