A Java developer’s adventures through the strange landscape of Go

GO Utveckling Teknologi

Uzi Landsmann

Systemutvecklare

This is the story about the rich soup of emotions and discoveries I paddled through when exploring this strange and wondrous language that is called Go. If you also come from the landscape of JVM and plan to take a look at this language, read on to see what might await for you in your travels.

What this is all about

I like learning new stuff, and especially new languages. It’s always interesting to find out how this particular language solves that particular problem and how the syntax differs from those you’ve seen before. Once you’re familiar with many patterns and seen enough different solutions to the same problems, it becomes easier to learn how to program in a new language and discover familiar patterns in the one you’re trying to learn.

At one time, I decided to take a look at Go, and was heading for some surprises. Programming in Go, after getting used to stuff like Java’s streams, Scala’s pattern matching and Kotlin’s functional library was like a travel to a parallel reality, where no lambdas exist and for loops are the VIPs. So let me show you some of the stuff I had to get used to, some that I was missing and how Go makes up for them, as well as some of the things I realized I was missing in Java.

Things that makes you go, hmm

Dev env

Setting up your development environment for Go is different than what you’re used to from the familiar world of JVM. All of your programs must be in one place, in a structure that looks like this:

Go ← this where your GOPATH environment variable must point to
| — bin
| — pkg
| — src
     | - github.com
            | - gosoup
            | - goworkshop
     | - myprivatestuff
            | - evilplans


src
is where you put all of your code. Usually under folders like github.com or myprivatestuff etc.

You also need a second environment variable named GOROOT. This one should point to wherever you installed Go. But please don’t take my word for it, I’m sure it’s not the whole story. Instead, follow the real instructions here: https://golang.org/doc/install

Exporting

Exporting variables and functions in Go means they are publicly available to other packages. Exporting is extremely simple: they need to start with a capital letter. That’s it. So for these two functions in package gosoup:

package gosoup
func mySecretLittleFunction() {}

func UseMeImExported() {}


…the first one is internal and the second one is exported. When I write:

strings.ToLower("HELLO")


…it means I’m calling the exported function ToLower in package strings.

Opinionated

Go is very opinionated. About many things. There are no recommendations here, there are rules. So there are two tools you should use in order to adhere to these rules, and these are gofmt and golint. You can run them in your terminal or integrate them inside you IDE, and they will complain whenever you step out of line when writing code that does not look proper.


Here’s a line of code:

 

package gosoup

func     Not_well_formatted() {println("hello")}


Running gofmt on that file will give us the following, which is the correct way to format the code:

 

$ gofmt gofmttest.go
package gosoup
func Not_well_formatted() { println(“hello”) }

 

Running golint on that file will give us the following comments:

 

$ golint gofmttest.go
gofmttest.go:3:1: exported function Not_well_formatted should have comment or be unexported
gofmttest.go:3:10: don’t use underscores in Go names; func Not_well_formatted should be NotWellFormatted

Testing

Go has a test package you can use to write your unit tests. There is also an assertion library, but in order to keep things simple, you can also simply compare your test expectations using if.

Writing tests, you have to follow these semi-peculiar rules:

  • Tests need to be in a file that ends with _test.go
  • Tests need to start with TestXxx where Xxx is whatever you’re testing, starting with a capital letter
  • Tests need to have argument t*testing.T

Here’s an example of a test that calls a function and expects the result to be 8:

 

func TestGetANumber_(t *testing.T) {
   want := 8
   got := GetANumber()
   if got != want {
      t.Fatalf(`Fail! Wanted '%v', got '%v'`, want, got)
   }
}

 

You can now run the test in your terminal:

go test ./gosoup_test.go
— — FAIL: TestGetANumber_ (0.00s)
gosoup_test.go:13: Fail! Wanted ‘8’, got ‘7’

 

Things that are simply not there and how to cope with their loss

Inheritance

No, you cannot inherit from classes in Go, simply because there are no classes in Go. There are structs and interfaces, which are probably the closest you can get to classes, but you can’t inherit from them. There is a great article about inheritance in Go that suggests, among other solutions, using composition when in need of inheritance, a technique you sometimes use in Java, too:

 

type Cat struct {
   name       string
   attributes string
}

type PersianCat struct {
   Cat
   specialAttributes string
}

func main() {
   plato := PersianCat{
      Cat: Cat{
         name:       "Plato",
         attributes: "four-legs, whiskery, destroyer of sofas",
      },
      specialAttributes: "long-haired, flat-nosed",
   }
   println(plato)
}

Exceptions

I have a love/hate relation with exceptions in Java. They are good to have but are sometimes very sneaky to catch. In later years, every library I know has moved to using runtime exceptions, which makes your code more readable, and the exceptions even sneakier. Go doesn’t use exceptions, but instead allows functions to return multiple values: one that is the actual result of whatever the function does, and the other an error flag. When calling this function you assign two variables to the result of the function, and check the error variable to see if everything went well. However, as with runtime exceptions, this requires the developer’s discipline to check those errors and to act upon them:

func Divide(x float32, y float32) (float32, error) {
   if y == 0 {
      return 0, errors.New("division by zero") // an error occurred
   }
   return x / y, nil // result and no error
}

func DivisionRunner(x float32, y float32) {
   result, err := Divide(x, y)

   if err == nil {
      fmt.Printf("%v / %v = %v", x, y, result)
   } else {
      fmt.Printf("error: %v", err)
   }
}


Generics

Generics are a great way to avoid having to write multiple versions of the same function that does the same thing using different types. Collections like arrays, slices and maps support generic types in Go, but writing your own generic functions is complicated. There are some articles, like this one, or this one, who can give you some hints on how to create generic-like functions using the interface type. However there are quite a few proposals to adding generic support in Go, so time will tell if we’ll stick to the workaround or get some real generics in Go in the future.

Lambdas

We all love using lambdas and especially stream methods like map, filter and collect. But how would you do that in Go? The short answer is that you don’t. Go is not a functional language and it took me some practice to get used to that. Go’s idiomatic way to such operation is to keep things extremely simple, using for loops and if/else conditions. Below is an example, written in typical Go style: start with an empty variable, iterate through some collection, check for a condition and add items to the previous list, then return that list at the end.

 

func filterRhymingWords(words []string, suffix string) []string {
   var rhymingWords []string
   for _, word := range words {
      if strings.HasSuffix(word, suffix) {
         rhymingWords = append(rhymingWords, word)
      }
   }
   return rhymingWords
}


The point is that code that looks standardized is easy to read and maintain and is familiar to other developers. Go developers are usually proud of their simple syntax and predictable code.

Things you wished existed in Java

Benchmarks

Benchmarks are an awesome build-in Go feature that lets you run performance unit test on a single function. Suppose you have a function that looks like this:

 

func GetANumber() int {
   rand.Seed(time.Now().UnixNano()) // different seed every time
   return rand.Intn(100)            // generate a random number
}

 

You can write a benchmark test for it, which looks like this:

 

func BenchmarkGetANumber(b *testing.B) {
   for i := 0; i < b.N; i++ {
      GetANumber()
   }
}

 

This will run your function a lot of times, until the run time has stabilized. Go will then let you know how it went:

 

BenchmarkGetANumber_-12 169064 7059 ns/op

 

This means it ran 169064 times on 12 processors and took in average 7059 nanoseconds to run (which is quite a long time, because of the random seeding). You can (and should) create benchmark tests for your functions and monitor them to find suspiciously slow functions, in order to maintain your service’s performance.

 

Multiple return values

Methods in Java always return a single value. If you want to return many values, you have to create an object and encapsulate these values inside it. Other JVM languages like Scala and Kotlin have tuples, which allow you to create ad-hoc objects with those values inside them. This feature in Go is very practical and is used, for example, to return a value and an error. But you can also use it any way you want, for example:

 

func GetProtocolDomainAndPort(url string) (string, string, int) {
   // ...find those parts
   return protocol, domain, port
}


Adding functions to existing structures

Once again, you can’t do this in Java, but Kotlin (and other languages like Javascript) lets you add functionality to already defined structures. In the case below, we define a struct and then add a new function to it. Lastly, we create an instance of the struct and call the new function on it:

 

type Person struct {
   firstName string
   surname   string
}

func (person Person) fullName() string {
   return fmt.Sprintf("%v %v", person.firstName, person.surname)
}

func main() {
   arnold := Person{"Arnold", "Schwarzenegger"}
   println(arnold.fullName())
}

 

Final words

Go praises itself on the simplicity and the clearness of its code, which helps to make developing and maintaining easy and predictable. Unlike some other languages, coming back to your (or someone else’s) code after a few days, weeks or months, you can dive right in and fully understand it. No time wasted on trying to decipher complex one-liners that will cost you a day’s work. Go also helps you develop programs that are easy, reliable and performant. If these attributes are important to you and to your team, then perhaps Go is the right tool for you.

Fler inspirerande inlägg du inte får missa