days
-1
-8
hours
-1
-9
minutes
-1
-1
seconds
-5
-3
search
Why Gophers are lazy by nature

5 things you can hate about Go (but don’t have to)

Christoph Engelbert
#go
Go
© Shutterstock / Shkliaev

Go may not be much of a newcomer to the world of programming languages. But it’s developed into a very respectable, if opinionated language. Today, Christopher Engelbert goes over the five things you might hate about Go, but really don’t have to.

Among all newcomers of the programming languages, Go has made its way into the limelight in the recent years. But the term newcomer might not be appropriate here since Google introduced Go back in 2009 and released a first final version in 2012 (Go 1.0). In the meantime, Go has progressed to an impressive 1.10 version and gained a number of functionalities on the way.

So that’s the reason it’s called eGOtistic…

Go likes to go its own way when it comes to implementation or syntax and that’s something which widely known by now. In the English language, it’s quite affectionately described as “opinionated”.  The problem therein is due to the fact that many constructs from other programming languages simply don’t exist in this form in Go or they behave completely different than usual. In the latter case, this can lead to unexpected errors and even to incomprehension on the developer’s side.

The very strict Go syntax often leads developers to weary smile only. The Go compiler doesn’t appreciate unused imports and variables and shoots them down vehemently. Even an open, curly bracket put blatantly in a new line is unappreciated. In general, Go forces a relatively static but (almost) uniform programming style. When the Go compiler doesn’t like something it all comes down to one thing: compile error.

Go offers a very strict type security. It’s so strict that we can create special effects and programming errors just by using it. Some of which we’ll go over later in this article. Still, you rarely have to write types explicitly in Go since these are normally derived from the value assignment (Type Inference).

I don’t give a FAQ!

I started to work and develop more intensively with Go a little over a year ago. Go has not necessarily become my favorite language, but I admit that you can develop productively and relatively quickly. In fact, I’ve already done several little projects using Go. These are mainly applications for embedded systems. The cross compiler capabilities (i.e. compiling for another operating system or CPU platform) of the Go Toolchain are fantastic and far ahead of the competition.

But let’s get back to peculiarities that make Go so special. It’s quite easy to get started with Go; you might only need one weekend to understand the basics. It’s when you sit down and start to work on something bigger, when all the strange cases start to occur.

Sometimes these peculiarities are so strange that Google has set up a FAQ for Go in the sense of “why does X behave this way and that way?” No, I’m not kidding. The language does many things just so different. It feels like every programmer stumbles over them at some point. This is something, which is confirmed by the gopher slack channel, with statements like: “Now you’ve really got to know Go, because every developer asks this question in the course of his Go career”. It’s therefore often the case, that intuition doesn’t match the peculiarities of Go. For example, in Google’s C-Alternative, public types, functions, constants and other constructs become public by starting with a capital letter. A lowercase letter at the beginning of the identifier ends up in a private access modifier.

SEE MORE: Go has earned companies’ trust: More developers use it at work now, survey shows

Still, Go has to be credited for the fact that many of the decisions have been through lengthy discussions on the mailing lists or in the proposal documents. Reading this sheds some light on most points. Unfortunately the use cases behind the explanations are so special that it will remain unclear to many developers what exactly this has to do with their current problem.

My personal favorite is that Go does not offer a reentrant lock, i.e. a lock that can be recursively acquired from the same thread or Goroutine (the Go variant of Coroutines or Green Threads). However, without hacks there is no way to build such an implementation by yourself, since threads are not available in Go and Goroutines do not offer an identifier, which would allow you to recognize the same Goroutine you’ve seen before.

But well, that’s already pretty deep into the matter. In this article, I want to present five properties of Go and its syntax, which are not as obvious as a missing class.

 You cast a shadow, but a crazy one

Let’s start with something simple: Every good developer has heard of Shadowing. Usually within context of variables. A simple example only requires two scopes.

foo("foo")
func foo(var1 string) {
  for {
    var1 := "bar"
    fmt.Println(var1)
    break
  }

As a short introduction for non-Gophers: the := assignment creates a new variable and identifies the type of variable by its assigned value (type reference). In this case, a string. So, we create a variable in the inner scope (the for-loop) with the same name as the function’s parameter. We overlay (or shadow) the parameter and our function outputs bar.

So far so good. However, in Go it’s necessary to specify the package name for properties of other packages (i.e. structs, methods, functions, …). This can be seen at the fmt package, which provides the Println function.

So let’s rebuild our example a little:

foo("foo")
func foo(var1 string) {
  for {
    fmt := "bar"
    fmt.Println(var1)
    break
  }
}

This time, we’re ending up in a compiler error. And it’s kind of obvious because we are now trying to call the Println function on a string. Unfortunately it isn’t always as obvious as it’s here. Even just a few lines of distance can lead to lots of fun when the code suddenly stops compiling.

It gets really uncomfortable if a Struct is superimposed. Let’s build a somewhat strange design as an example, just because we’re good developers (and because we can):

type task struct {
}
 
func main() {
  task := &task{}
}

So we build a struct called task and create an instance of it. The lowercase task for the struct name is intentional, since Go,  as already mentioned, uses the first letter to determine the visibility, in this case private.

SEE MORE: From Python to Go: Why Stream switched to Go

So far it looks good and Go compiles our lovingly created task. However, the situation unexpectedly changes when we add a line.

type task struct {
}
 
func main() {
  task := &task{}
  task = &task{}
}

A new compilation process now fails with the error message that task is not a type. At this point, Go is no longer able to see the distinction between the type and the variable. In the end, this might be a logical step, but it just worked. While one could justify that in JavaScript that the variable task could now be a reference to a type, this is not possible in Go, as no type can be assigned to a variable as a value.

With a question mark above our heads: Is this tragic or not? Generally spoken no, but it often has happened to me that without thinking about it, I’ve given a variable the name of a struct. Eventually, just a few lines below, desperately trying to access the struct or package of the same name. It always takes a few minutes each time, until it clicks again.

Well, speaking of Types, let’s go over to the next one.

 Type or no type, that is the question!

We have already seen how structs and functions are created. But wouldn’t it be nice to “rename” a type once in a while?

type handle int

This will create a type called handle that behaves like int. In general, such a feature is known as a type alias. You’d think, but not in Go. At least since Go 1.9, this has been crystal clear.

Let’s see what beautiful things we can do with Go:

type handle int
 
func main() {
  var var1 int = 1
  var var2 handle = 2
  types(var1)
  types(var2)
}
 
func types(val interface{}) {
  switch v := val.(type) {
  case int:
    fmt.Println(fmt.Sprintf("I am an int: %d", v))
  case handle:
    fmt.Println(fmt.Sprintf("I am an handle: %d", v))
  }
}
I am an int: 1
I am an handle: 2

In this example, we use several very cool features of Go. The switch-type-case statement is a kind of pattern matching on the type and works similar to an instanceof or typeof in Java or JavaScript. We also simply accept the interface{} as equivalent to the Object in Java, because it’s an empty interface, which every Go type implements automatically.

What’s interesting is the fact, that a Java developer would expect that a handle is also an int. And with this, the first case should occur. Unfortunately this is not the case, since Go doesn’t know any type inheritance in the classical, object-oriented sense.

An alternative expection would be that handle is an alias for int, just like a typedef in C/C++, but even this isn’t the case here. The Go compiler creates a new TypeSpec, a clone so to speak, of the original type. And with that, these are completely independent from one another.

SEE MORE: The meteoric rise of Go and why HashiCorp is betting on it

However, since Go 1.9 there now is also a real type alias. The following example is only minimally modified to make the code compile again.

type handle = int
 
func main() {
  var var1 int = 1
  var var2 handle = 2
  types(var1)
  types(var2)
}
 
func types(val interface{}) {
  switch v := val.(type) {
  case int:
    fmt.Println(fmt.Sprintf("I am an int: %d", v))
  }
  switch v := val.(type) {
  case handle:
    fmt.Println(fmt.Sprintf("I am an handle: %d", v))
  }
}
I am an int: 1
I am an int: 2
I am an handle: 1
I am an handle: 2

Did you notice the difference? Exactly, instead of type handle int we now use type handle = int and create an additional name (alias) for int, namely handle. This means that the switch statement must also be adapted, since int and handle are the exact same type for the compiler in this case, otherwise you have a double case and thus again a compiler error. Since the type alias has only existed since Go 1.9, many thought that the Type Cloning described above was a type alias. The problems this caused did not always make sense at first glance.

To demonstrate, let’s define a type called Callable, which consists of a simple function without parameters or return value.

type Callable func()

Now you want to create a corresponding function.

func main() {
  myCallable := func() {
    fmt.Println("callable")
  }
  test(myCallable)
}
 
func test(callable Callable) {
  callable()
}

Easy. Thanks to the type inference of Go, the compiler automatically realizes that myCallable corresponds to the function signature of Callable. This allows the compiler to implicitly cast myCallable to a Callable. Subsequently, myCallable can be passed to the test function. This is one of the few exceptions where an implicit cast is performed, usually all forms of cast must be explicitly written out in full.

SEE MORE: On the road to Go 2: Goals, constraints and roadmap

Now we’re reaching the point where it becomes unexpected withReflection coming into play. Reflection, like in other languages, provides functionality to analyze or change behavior at runtime. Type information is often used to change the runtime behavior according to the value’s data type.

type Callable func()
 
func main() {
  callable1 := func() {
    fmt.Println("callable1")
  }
 
  var callable2 Callable
  callable2 = func() {
    fmt.Println("callable2")
  }
 
  test(callable1)
  test(callable2)
}
 
func test(val interface{}) {
  switch v := val.(type) {
  case func():
    v()
  default:
    fmt.Println("wrong type")
  }
}
callable1
wrong type

Here callable1 has now the function type func(), while callable2 is explicitly a Callable. Callable is a separate TypeSpec and therefore no longer the same type as func(). Both cases must now be intercepted separately by our Reflection handler. The whole thing can be fixed again with the type alias, which was added in Go 1.9.

type Callable = func()

Anyways, finding this bug is pure debugging fun because after all there isn’t even an error message, it just doesn’t work.

 Gophers are lazy by nature!

One of my favorites and the first entry on this list, which can also be found in the FAQ, is the Lazy Evaluation, i.e. the delayed execution of code. Java developers are well aware of this since the introduction of the Stream API.

Let’s take a look at the following code snippet:

func main() {
  functions := make([]func(), 3)
    for i := 0; i < 3; i++ {
      functions[i] = func() {
      fmt.Println(fmt.Sprintf("iterator value: %d", i))
      }
    }
 
  functions[0]()
  functions[1]()
  functions[2]()
}

It’s not witchcraft, is it? It’s an array with three elements, a loop and closures, which catch the variable i. But what’s the result?

iterator value: 3
iterator value: 3
iterator value: 3

Of course it’s 0, 1, 2! Wait, it’s actually 3, 3, 3. That’s right! Welcome to the “What, that’s possible?”-world, as one large German cable provider so beautifully puts it.

Other programming languages such as Java capture the value of the variable when a closure is created, while Go only captures the pointer to the variable itself. The problem, during the for-iterations the value of the variable changes continuously. After the loop is completed, we execute the closures and only ever see the last value, which is somewhat unexpected. Knowing that we only have the pointer, the behavior suddenly makes sense. Once again, it’s just not intuitive.

SEE MORE: New Go package versioning proposal promises compatibility without complexity

Since we want to store the value, we need to find out how we can evaluate the value when creating the closure.

The syntax is of course an artistic masterpiece in this matter.

func main() {
  functions := make([]func(), 3)
  for i := 0; i < 3; i++ {
    functions[i] = func(y int) func() {
      return func() {
        fmt.Println(fmt.Sprintf("iterator value: %d", y))
      }
    }(i)
  }
 
  functions[0]()
  functions[1]()
  functions[2]()
}

We create a temporary function which takes the variables as a parameter and returns our closure. And we call it up immediately. Since the value of the variable must be evaluated when calling the external function, our inner closure now catches the correct value. We get 0, 1, 2.

Shortly, before this article was finished, I got a hold of another alternative. To do this, you create a variable with the same name in the loop and assign the actual value to it. This also captures the value of the variable, since this method creates a new variable (and thus a new pointer) in each pass of the loop.

func main() {
  functions := make([]func(), 3)
  for i := 0; i < 3; i++ {
    i := i // Trick mit neuer Variable
    functions[i] = func() {
      fmt.Println(fmt.Sprintf("iterator value: %d", i))
    }
  }
 
  functions[0]()
  functions[1]()
  functions[2]()
}

Lazy Evaluation is in general an interesting topic from the point of view of the speed of execution. After all, I could make up closures without ever using them. So why evaluate? Nevertheless, this is my number 1 when it comes to “counterintuitive”.

 Aren’t we all a little bit Gopher?

Let’s remember what we already learned: interface{} in Go is something like Object in Java – an empty interface that every type in Go implements automatically. Automatically implementing an interface does not only apply to an empty interface. Every Struct and type which implements all methods of any interface automatically fulfills that specific interface.

Let’s have a look at the following example for better clarification:

type Sortable interface {
  Sort(other Sortable)
}

A Struct that defines this method is therefore automatically a Sortable.

type MyStruct struct{}
func (m MyStruct) Sort(other Sortable){}

Apart from the syntax of the receiver type, which does not deter us and is used to bind a function to a type (in this case to the struct), we have implemented all methods of the interface Sortable. We are a Sortable!

var sortable Sortable = &MyStruct{}

SEE MORE: Go is being used to create smart contracts in building blockchain — That might explain its recent demand surge

Even if automatically implemented interfaces seem to make sense at first glance, this becomes complicated and, above all, opaque for larger applications where several interfaces have the same methods. Which interface did the developer actually want to implement? Hopefully, he left us with some comments in the code! More funny side effects of the type system and the automatic (or not automatic) implementation of interfaces can be read here.

Go also has a solution to ensure that a type implements an interface, a simple implements statement like in Java would’ve been too simple.

type MyStruct struct{}
func (m MyStruct) Sort(other Sortable){}
var _ Sortable = MyStruct{}
var _ Sortable = (*MyStruct)(nil)

There you go, isn’t that great? Again, I can only address the artistic design of the source code of everyday problems once again. Thanks, Go!

 Of nil and nothing

But wait, we’re not done yet! We still have one here!

It should be known by now, that “null or nil” is the billion dollar mistake. But it’s probably not so well known, that “nothing” does not always literally mean “nothing”. Go shows us otherwise! To proof, we set up our own error type (exception).

type MyError string
func (m MyError) Error() string {
  return string(m)
}

In this case, we create a new type, which is cloned from the string-type. We just want a message for the error, so that’s perfectly fine. To implement the interface error (yes, in lower case, theoretically this shouldn’t be public, but Go can do anything), the method Error has to be implemented.

Next, we need another function that will always return Nil.

func test(v bool) error {
  var e *MyError = nil
  if v {
    return nil
  }
  return e
}

This function returns nil, regardless if we pass true or false in the function, or not?

func main() {
  fmt.Println(nil == test(true))
  fmt.Println(nil == test(false))
}
true
false

When e is returned, the *MyError pointer becomes an instance of the interface type error and it’s not nil! Not logical yet? It’s logical when you know how interfaces, and such is error, are represented in Go.

Internally, an interface is a struct of the actual target instance (here nil) and the interface type (here error), and according to the Go specification, an interface instance is only nil if both values of this struct are nil. Therefore, always return nil explicitly when you mean nil! You can read it again in the FAQ.

SEE MORE: Translate Go into JavaScript with the all-new Joy compiler

Special Gophers

One further point that should not go unmentioned. As mentioned before, Go infers the visibility for types and functions from its assigned name. If the first letter is an uppercase letter (Foo), then the function or type is public; if the first letter is a lowercase letter (foo), then the result is private. Site note: when it comes to private, Go only actually knows package-private, assuming the Java terminology.

Okay, so far so creepy, but let’s leave out personal taste. In general, one gets along with this visibility rule quite well, except that in Go everything uses CamelCase, no matter if function, struct, constant, etc. But we all have IDEs with syntax highlighting anyways, so who cares!

The interesting fact that Go accepts the Unicode Identififiers. Ergo, 日本語 (Nihongo = Japanese) is a perfectly legal identifier, but always infers to private visibility. Why? Well, there are no capital letters for Japanese characters.

SEE MORE: “More recently, a language that is getting a lot of traction on DevOps teams is Go”

The GOzilla sends his regards

Go is a very peculiar language in some places. You can have a lot of fun with Go in your daily work. If you know of the pitfalls we mentioned here (and some more), even larger programs run smoothly. Nevertheless, there’s this constant reminder that something is wrong with the language. Somehow, it just doesn’t feel like home and we all know: “There’s no place like home”.

A lot has happened in Go in the recent years. New functionalities have been added and many points to be improved are on the list for Go 2, including some inconsistencies in syntax and runtime behavior. However, it’s still unknown when Go 2 will come out. A roadmap is not yet available.

If you want to work with Go today, I can only recommend it – despite the horror stories. However, you should be prepared for the fact, that there will be confused looks at times, a (longer) debugging session and possibly a visit to the FAQ or the Gopher Slacks. Despite that:

Happy Gophing!

Author

Christoph Engelbert

Christoph Engelbert is an open source enthusiast and always interested in “Java Next”. In his spare time, he struggles with performance optimizations, the JVM internals or the garbage collector, whereby he is also available to answer questions on these topics as a freelance consultant. He also firmly believes in Polyglot and is familiar with Java, Kotlin, Go, TypeScript and other languages.


Leave a Reply

Be the First to Comment!

avatar
400
  Subscribe  
Notify of