switch i {
case 0:
fmt.Print("None")
case 1:
fmt.Print("Single ")
fallthrough
default:
fmt.Print("Thing")
}
From Java to Go
2017-01-15 7li7w go
This is the second episode of my 7 languages in 7 weeks series of blog posts. After Ruby, a language which is more than 20 years old, I tried a more recent language, Go which was created in 2009.
Go gained in traction thanks to the Docker ecosystem: Docker container engine, Kubernetes, Traefik are mostly developed in Go.
The good
Simple to learn
Even if the language is pretty recent, there are many resources to learn Go. The first one I stumbled upon was Tour of Go, it allows to discover the Go features and test them online. You can even run the web server locally if you want to learn Go while traveling, it has even been translated in many languages. In a word, awesome!
I also tried the Go Koans, but the tests are less detailed and progressive than Ruby Koans. It took me an evening to complete these exercises.
There are several free e-books to get started with Go:
-
Introduction to Programming in Go by Caleb Doxsey
-
The Little Go Book by Karl Seguin
-
Go BootCamp by Matt Aimonetti
Go is mostly simple to grasp because there are few concepts. The Go Lang spec is less 100 pages long.
Obviously the "Go" name is not SEO friendly, you’d better google "GoLang" instead.
Rich tooling
As soon as you’ve installed Go and without any extra step, you can:
-
Get libraries (Go packages) with a sort of package manager
-
Format code to stick to Go formatting rules
-
Compile code and link dependencies. I was amazed to see how fast it was to compile a simple app and run it.
-
Run unit tests. Go provides a unit testing package, sadly it’s not as powerful as others XUnit libraries.
-
Analyze code and detect suspicious constructs.
-
Search and Read the base API documentation while being offline.
Sadly, there is no official tool to manage project dependencies and download them with Go Get.
switch
statement
Compare Go’s switch statement
With Java’s
switch i {
case 0:
System.out.print("None");
break;
case 1:
System.out.print("Single ")
default:
System.out.println("Thing")
}
The fallthrough
keyword does the exact opposite of break
.
This looks like a better default than Java to me,
because most of the time you don’t want to run multiple cases.
A missing break
can raise subtle bugs.
This switch
statement, can also replace a bunch of if
/else if
statements:
switch {
case i == 0:
fmt.Print("None")
case i == 1:
fmt.Print("One ")
case i > 1:
fmt.Print("Many ")
default:
fmt.Print("Invalid")
}
This looks like pattern matching, but simpler and less powerful.
Concurrency with channels and routines
First of all, a channel is a concept used for inter-thread communication. There are 2 types of channels:
-
Sync channels are similar to Java’s
SynchronousQueue
, the producer waits for the consumer to be ready:ch := make(chan string)
-
Async channels are similar to Java’s
ArrayBlockingQueue
, the producer waits for a free location to place data, while the consumer waits for a place filled with data:ch := make(chan string, 10)
Goroutines are lightweight threads, to run the doit
function in background:
go doit()
In the following example, a thread will generate and send numbers to another thread. The receiver thread will multiply the numbers and print them:
func generate(out chan [2]int) {
for x := 1; x <= 10; x++ {
for y := 1; y <= 10; y++ {
t := [2]int{x, y}
fmt.Printf("generate %dx%d\n", t[0], t[1])
out <- t (1)
}
}
fmt.Println("generate end")
}
func multiply(in chan [2]int) {
for {
t := <-in (2)
r := [3]int{t[0], t[1], t[0] * t[1]}
fmt.Printf("multiply %dx%d=%d\n", r[0], r[1], r[2])
}
}
func main() {
in := make(chan [2]int, 10)
go multiply(in) (3)
generate(in) (4)
time.Sleep(10 * time.Second)
}
1 | Generate and append data in the channel. This function will run in a first thread. |
2 | Pull data from the channel and process it. This function will run in a second thread. |
3 | Run the multiply function in a background thread |
4 | Run the generate function in the main thread.
I could have placed it in a separate thread as well. |
This multi-threading pattern, called CSP (communicating sequential processes) looks bit like Actor model (Akka and the like).
An actor is more or less an async channel with a goroute polling from this channel, like the multiply
function.
The bad
Error handling
There has been debate about how errors are handled. I hope not to start a new troll with this paragraph.
Prior to explaining my grief with error handling, let me talk about multiple values. A function can return multiple values, however there are no tuples like in Python, Scala…
file, err := ioutil.ReadFile("file.txt") // Correct
tuple := ioutil.ReadFile("file.txt") // Incorrect
I can not assign both values to a single variable.
Now, let’s come back to error handling. In Go there is no Exception concept, you must use multiple return values:
func Order(quantity int) (*Article, error) { (1)
if quantity <= 0 {
return nil, errors.New("Invalid quantity") (2)
}
return &Article{quantity, 10 * quantity}, nil (3)
}
func OrderAndPrint(quantity int) error {
article, err := Order(quantity)
if err != nil {
return err (4)
}
article.Print() (5)
return nil
}
func main() {
err := OrderAndPrint(-3)
if err != nil {
fmt.Errorf("Error %s", err) (6)
}
}
1 | Function can raise an error |
2 | Raise an error |
3 | Return normal result |
4 | Propagate the error |
5 | Handle normal flow |
6 | Handle error flow |
To me, this means multiple things
-
In the
Order
function, I must always return 2 things:nil+error
(1) orresult+nil
(2). I can even return bothresult+error
. -
I need to manually propagate the error when it occurs (3). Yet the function signature warns me that I may have to do something, and I can’t hardly forget to deal with it.
-
Even if I forward the error, the error doesn’t contain any call stack. As a result, it’s probably harder to debug since you don’t know at first sight who first returned the error (5). Hopefully the go-errors library may help.
-
I can not easily chain function calls because I can not write
Order(3).Print()
orPrint(Order(3))
wherePrint
is a function to display the result ofOrder
.
Pointers
Pointers are not a bad thing per se. In my case, C/C++ lectures were far away in my memory. It was not easy to remember the traps and tricks: Should I pass this variable by reference or by value?
func RaisePriceByVal(a Article) {
a.Price = (a.Price * 110) / 100
}
func RaisePriceByRef(a *Article) {
a.Price = (a.Price * 110) / 100 (1)
}
func newArticle(name string, price int) *Article {
a := Article{name, price}
return &a (5)
}
func main() {
a := Article{"Go in Action", 25}
fmt.Printf("Article %s %d\n", a.Name, a.Price)
RaisePriceByVal(a)
fmt.Printf("By val: Article %s %d\n", a.Name, a.Price) (2)
RaisePriceByRef(&a) (3)
fmt.Printf("By ref: Article %s %d\n", a.Name, a.Price) (4)
p := newArticle("The Little Go book", 10) (6)
RaisePriceByRef(p)
fmt.Printf("By ref: Article %s %d\n", p.Name, p.Price)
}
1 | Happily, I don’t have to convert the pointer into value to access the Price field.
I don’t have to write *a.Price or a→Price like in C++ |
2 | The article price is still 25 because it was passed by value |
3 | Here I have to convert the value into a pointer |
4 | The article price is now 27 because it was passed by reference |
5 | Unlike C, the function can return a pointer to the created article |
6 | p is a pointer to the created article |
Having to deal with pointers, I felt like doing a step backward.
make
, new
and New
To create, I mean allocate, something there are several operators depending on the type and whether you are expecting a pointer or not.
// Structs
struct1 := Language{"go", "Go Lang"} (1)
fmt.Println("struct1 ", struct1)
struct2 := new(Language) (2)
fmt.Println("struct2 ", struct2)
// Arrays
array1 := [5]Language{struct1, *struct2, Language{"java", "Java"}} (3)
fmt.Println("array1 ", array1)
array2 := new([5]Language) (4)
fmt.Println("array2 ", array2)
// Slices
slice1 := array1[1:3]
fmt.Println("slice1 ", slice1)
slice2 := make([]Language, 1, 5) (5)
fmt.Println("slice2 ", slice2)
// Maps
map1 := map[string]Language{ (6)
"go": struct1,
"java": Language{"java", "Java"},
}
fmt.Println("map1 ", map1)
map2 := make(map[string]Language, 3) (7)
fmt.Println("map2 ", map2)
// Other
error1 := errors.New("Sample error") (8)
fmt.Println("error1 ", error1)
1 | Create a new struct and initialize its fields |
2 | Create a new struct and return a pointer |
3 | Create a new array and initialize its elements |
4 | Create a new array and return an pointer |
5 | Create a new slice |
6 | Create a new map and initialize its elements |
7 | Create a new map |
8 | Create a new struct Error and initialize it |
The new
operator always returns a pointer, it’s like a malloc
operator in C.
The make
operator is used to create data structures like slices, maps…
Neither the new
nor the make
operators can be passed values,
they are initialized with zeros and empty strings.
The New
function is a factory, it can contain code to initialize or build something.
I find it a bit disturbing to have different syntaxes to do mostly the same thing. For a language which aims at simplicity, this is baffling.
Most of the time, when creating a struct, you will initialize its fields and get a pointer to avoid copying the value. As a result, the main syntax is:
struct := &Language{"go", "Go Lang"}
Harsh compiler
The Go compiler is very strict, this can be good thing at times as it may prevent bugs. But it can also be a bit too picky and annoying at times. In particular, it doesn’t accept unused variables and unused imports. This is a rather common scenario when you are refactoring, looking for the cleanest code design or trying to implement an idea. The compiler could just warn and skip unused declarations.
To skip a useless import, one can write (notice the underscore):
import _ "fmt"
Using an IDE (like Jetbrains Gogland) with an intelligent coding assistant which would fix automatically imports may help. I also found the goimports command line tool, but I didn’t try it.
The odd
Characters are called Runes in the Go terminology. But they don’t hide any secret magic ;-) .
if
and for
statements
The for
statement uses the usual syntax minus the parenthesis:
for i := 0; i < 10; i++ {
Like in the above for
construct, the if
can declare a variable before the mandatory condition:
if dice := rand.Int31n(6) + 1; dice > 4 {
fmt.Printf("%d You won!", dice)
} else {
fmt.Printf("%d You lost!", dice)
}
The dice
variable is visible in the while if
/else
bloc.
I find the if
statement to be less readable because
the dice
declaration adds noise, and the condition doesn’t catch my eye.
There is no while
statement, for
is the only loop keyword
var line = "start"
reader := bufio.NewReader(os.Stdin)
for line != "quit" { (1)
fmt.Println(line)
line, _ = reader.ReadString('\n')
line = strings.TrimSpace(line) (2)
}
1 | The for with a condition acts as like the while in most languages |
2 | I can not chain ReadString and TrimSpace functions in single line
because the read can return an error I should have handled.
This is the problem I explained with error handling and method chaining. |
The :=
symbol
The :=
symbol is called short variable declaration,
it triggers type inference so you don’t have to declare the type of your local variable.
var i int = 12
i := 12
Both variable declaration are the same.
I just wonder why there is a special symbol for that, even if I found it very efficient in practice.
At first, I thought it was to tell apart assignment from comparison (==
) like in Pascal.
Packages and imports
I wonder why imported packages are strings wrapped by double quotes:
import "fmt"
Like Java, Go’s package naming convention reflects domain names and folder paths:
import "github.com/go-errors/errors"
Moreover, when there are subpackages, only the last part of the package is used in the code:
import "io/ioutil" (1)
func main() {
data, err := ioutil.ReadFile("subpackage.go") (2)
1 | Notice that the imported package is named io/ioutil not io/util . |
2 | To use a function in this package, I use ioutil.Readfile and skip the io prefix. |
Naming a package util
( or common
or …) in Go is probably a poor idea.
For instance string/util
would be conflicting with date/util
,
unless you give an alias to the import, but it looks less convenient:
import stringutil "string/util"
The Go compiler doesn’t allow circular dependencies in packages which sounds like a good idea.
Structs and interfaces
Go doesn’t have classes, inheritance and OOP concepts, but it doesn’t matter, structs are powerful and can have interfaces.
type Person struct {
Name string
Age int
}
// NewPerson Constructor of Person
func NewPerson(name string) *Person { (1)
return &Person{name, 0}
}
// GetName Function on Person
func (p *Person) GetName() string { (2)
return p.Name
}
// Nameable interface
type Nameable interface { (3)
GetName() string
}
// SayName Function taking an interface as argument
func SayName(n Nameable) { (4)
fmt.Printf("My name is %s\n", n.GetName())
}
func main() {
p := NewPerson("John")
p.Age = 12
SayName(p) (5)
}
1 | I wonder why the NewPerson constructor and the GetName function can not declared inside the struct. |
2 | In the GetName function, the Go Linter warns that the Person argument shouldn’t be named this or self , this is why I named it p . |
3 | Notice the Person struct doesn’t tell it implements the Nameable interface. |
4 | A function should never take a pointer to an interface, because an interface is already a kind of pointer delegating to the original struct. |
5 | The variable p of type *Person is automatically converted into Nameable interface.
Go is doing duck typing, if it has a GetName function, then it is a Nameable thing. |
Conclusion
To me, the Go language is well suited for low level tools (container orchestrators, databases, proxies…), command line utilities as well as embedded and mobile applications. But it is not has expressive as many other languages for business applications.
I don’t pretend to be a Go expert at all, if I wrote something wrong, tell me.
Other posts
- 2020-11-28 Build your own CA with Ansible
- 2020-01-16 Retrieving Kafka Lag
- 2020-01-10 Home temperature monitoring
- 2019-12-10 Kafka connect plugin install
- 2019-07-03 Kafka integration tests