Go is a special language. It is very refreshing in its approach to programming and the principles it promotes. It helps that some of the language inventors were early C pioneers. The overall feeling of Go is 21st-century C.
In this tutorial, you’ll learn about three of the features that make Go unique: its simplicity, the concurrency model via goroutines, and the error handling mechanism.
1. Simplicity
Many successful modern languages like Scala and Rust are very rich and provide advanced type systems and memory management systems. Those languages took the mainstream languages of their time like C++, Java and C# and added or improved capabilities. Go took a different route and eliminated a lot of features and capabilities.
No Generics
Generics or templates are a mainstay of many programming languages. They often add complexity, and the error messages associated with generics can sometimes be obscure. Go designers decided to just skip it.
This is arguably the most controversial design decision of Go. Personally, I find a lot of value in generics and believe that it can be done properly (see C# for a great example of generics done right). Hopefully, generics will be incorporated into Go in the future.
No Exceptions
Golang’s error handling relies on explicit status codes. To separate status from the actual result of a function, Go supports multiple return values from a function. This is pretty unusual. I’ll cover it in much more detail later, but here is a quick example:
package main import ( "fmt" "errors" ) func div(a, b float64) (float64, error) { if b == 0 { return 0, errors.New(fmt.Sprintf("Can't divide %f by zero", a)) } return a / b, nil } func main() { result, err := div(8, 4) if err != nil { fmt.Println("Oh-oh, something went wrong. " + err.Error()) } else { fmt.Println(result) } result, err = div(5, 0) if err != nil { fmt.Println("Oh-oh, something iswrong. "+err.Error()) } else { fmt.Println(result) } } 2 Oh-oh, something is wrong. Can't divide 5.000000 by zero
Single Executable
Go has no separate runtime library. It generates a single executable, which you can deploy just by copying (a.k.a. XCOPY deployment). This is as simple as it gets. No need to worry about dependencies or version mismatches. It is also a great boon for container-based deployments (Docker, Kubernetes and friends). The single standalone executable makes for very simple Dockerfiles.
No Dynamic Libraries
OK. This just changed recently in Go 1.8. You can now actually load dynamic libraries through the plugin package. But, since this capability wasn’t introduced from the get go, I still consider it an extension for special situations. The spirit of Go is still a statically compiled executable. It is also available on Linux only.
2. Goroutines
Goroutines are probably the most attractive aspect of Go from a practical standpoint. Goroutines let you harness the power of multicore machines in a very user-friendly way. It is based on solid theoretical foundations, and the syntax to support it is very pleasant.
CSP
The foundation of Go’s concurrency model is C. A. R. Hoare’s Communicating Sequential Processes. The idea is to avoid synchronization over shared memory between multiple threads of execution, which is error-prone and labor-intensive. Instead, communicate through channels that avoid contention.
Invoke a Function as a Goroutine
Any function can be invoked as a goroutine by calling it via the go keyword. Consider first the following linear program. The foo()
function sleeps for several seconds and prints how many seconds it slept. In this version, each call to foo()
blocks before the next call.
package main import ( "fmt" "time" ) func foo(d time.Duration) { d *= 1000000000 time.Sleep(d) fmt.Println(d) } func main() { foo(3) foo(2) foo(1) foo(4) }
The output follows the order of calls in the code:
3s 2s 1s 4s
Now, I’ll make a slight change and add the keyword “go” before the first three invocations:
package main import ( "fmt" //"errors" "time" ) func foo(d time.Duration) { d *= 1000000000 time.Sleep(d) fmt.Println(d) } func main() { go foo(3) go foo(2) go foo(1) foo(4) }
The output is different now. The 1 second call finished first and printed “1s”, followed by “2s” and “3s”.
1s 2s 3s 4s
Note that the 4 second call is not a goroutine. This is by design, so the program waits and lets the goroutines finish. Without it, the program will immediately complete after launching the goroutines. There are various ways besides sleeping to wait for a goroutine to finish.
Synchronize Goroutines
Another way to wait for goroutines to finish is to use sync groups. You declare a wait group object and pass it to each goroutine, which is responsible for calling its Done()
method when it’s done. Then, you wait for the sync group. Here is the code that adapts the previous example to use a wait group.
package main import ( "fmt" "sync" "time" ) func foo(d time.Duration, wg *sync.WaitGroup) { d *= 1000000000 time.Sleep(d) fmt.Println(d) wg.Done() } func main() { var wg sync.WaitGroup wg.Add(3) go foo(3, &wg) go foo(2, &wg) go foo(1, &wg) wg.Wait() }
Channels
Channels let goroutines (and your main program) exchange information. You can create a channel and pass it to a goroutine. The creator can write to the channel, and the goroutine can read from the channel.
The opposite direction works too. Go also provides sweet syntax for channels with arrows to indicate the flow of information. Here is yet another adaptation of our program, in which the goroutines receive a channel they write to when they are done, and the main program waits to receive messages from all goroutines before terminating.
package main import ( "fmt" "time" ) func foo(d time.Duration, c chan int) { d *= 1000000000 time.Sleep(d) fmt.Println(d) c <- 1 } func main() { c := make(chan int) go foo(3, c) go foo(2, c) go foo(1, c) <- c <- c <- c }
Write a Goroutine
That's sort of a trick. Writing a goroutine is the same as writing any function. Check out the foo() function above, which is called in the same program as a goroutine as well as a regular function.
3. Error Handling
As I mentioned earlier, the error handling of Go is different. Functions can return multiple values, and by convention functions that can fail return an error object as their last return value.
There is also a mechanism that resembles exceptions via the panic()
and recover()
functions, but it is best suited for special situations. Here is a typical error handling scenario where the bar()
function returns an error, and the main()
function checks if there was an error and prints it.
package main import ( "fmt" "errors" ) func bar() error { return errors.New("something is wrong") } func main() { e := bar() if e != nil { fmt.Println(e.Error()) } }
Mandatory Checking
If you assign the error to a variable and don't check it, Go will get upset.
func main() { e := bar() } main.go:15: e declared and not used
There are ways around it. You can just not assign the error at all:
func main() { bar() }
Or you can assign it to the underscore:
func main() { _ = bar() }
Language Support
Errors are just values you can pass around freely. Go provides little error support by declaring the error interface that just requires a method named Error()
that returns a string:
type error interface { Error() string }
There is also the errors
package that lets you create new error objects. The fmt
package provides an Errorf()
function to create formatted error objects. That's about it.
Interaction with Goroutines
You can't return errors (or any other object) from a goroutine. Goroutines can communicate errors to the outside world through some other medium. Passing an error channel to a goroutine is considered good practice. Goroutines can also write errors to log files or the database or call remote services.
Conclusion
Go has seen tremendous success and momentum over the last years. It is the go-to (see what I did there) language for modern distributed systems and databases. It got a lot of Python developers converted.
A big part of it is undoubtedly due to Google's backing. But Go definitely stands on its own merits. Its approach to basic language design is very different from other contemporary programming languages. Give it a try. It's easy to pick up and fun to program in.