Hello, Go

In this article we will have a look at the history of Go, what makes go special and finally the problems and tasks Go is most suited for.

Why the world needed another programming language

Golang is relatively a young programming language. The first version was released in 2009 while the first stable version was released in 2012. However, the need for a such language arose in the market well before that.

Business requirements for software began to change with the advancement fo the internet. Webpages had to be created and served faster without sacrificing the security while the servers needed to serve simultaneously more and more clients. While the hardware that powered most software at that time still worked on single-core processors.

But in 2006 something changes, the advancement of multicore processor opened new opportunities which in turn required a new approach to software design. To utilize efficiently all these extra cores programmers needed to write code that supports multithreading.

What is a "thread" in the context of an operating system?

A native kernel thread is a process in which instructions can be executed independently and have access to shared resources. The OS has a thread scheduler that is responsible for managing the threads.

Multithreading exists at both hardware and software levels.

At the hardware level it allows multiple threads to be executed in parallel without interfacing with each other. Therefore, hardware multithreading allows parallel execution of instructions from different threads on the same physical core.

While at the software level, it allows the OS to schedule multiple threads on a single core and take advantage of the hardware multithreading capabilities. But, to achieve optimal performance, software needs to be designed to utilize multiple threads effectively.

In modern computing, optimal performance comes from the combination of both hardware multithreading capabilities and software designed to utilize multiple threads effectively.

One of first problems the engineers have to face when writing a multithreaded program is the access to shared resources, specifically the memory. Incorrect sharing of access to resources between threads can lead to data corruption. This is known as a race condition. For example, if two threads simultaneously and in parallel write their value to the same variable which value will be written as a result? The answer is that it depends on the order of execution of the threads which is unpredictable. In general thinking and writing multithreaded programs complicates understanding the program execution process.

Engineers want to use languages and tools that make it easier to write correct multithreaded programs. But since most of the languages existent at that time were not designed with multithreading in mind, the support for multithreading was added as an afterthought and thus was not designed to be easy to use.

Interpreted programming languages had additional complications. It was necessary to organize mechanisms for accessing the global state of the interpreter from several threads and for exchanging data between these threads.

It became clear that a new programming language was needed, one that would be initially created with multithreading in mind and would allow for quick and convenient writing of applications that utilize available processor cores.

Go as a response to market challenges

Go was designed from the beginning with multithreading in mind. It implements many solutions, built right into the language, that allow developers write easier multithreaded programs in a simple, safe, fast, and efficient manner.

Golang implements the CSP (Communicating Sequential Processes) model to address the sharing of resources between the thread. The CSP model is a set of simultaneously running subtasks that communication with each other through communication channels.

A CSP model diagram

In Go, the CSP model is implemented with the following built-in the language abstractions:

  • A subtask in Go is called a goroutine. Goroutines live in a layer between the program and the execution program environment called runtime. Amount many things the runtime is also responsible for organizing the concurrent access of multiple goroutines to common limited resources - processor time. Its task is to distribute this resource so that each non-sleeping goroutine can work for at least some time before the control is passed to the next one. This creates the illusion of parallel execution of tasks when the number of goroutines many times exceeds the number of available system threads.
  • A communication channel in Go is an abstraction called channel. All mechanisms of exchange and synchronization of threads in Go are build on channels. A channel is like a tunnel into which a goroutine can put a value and another one can pull it out on the other side.

Language features

Go is a compiled programming language with strong static typing, garbage collection, and a built-in package manager. It is designed with a focus on multithreaded programming.

The ideology of Go is based on minimalism, simplicity of syntax, high speed of assembly and execution, convenient abstractions for writing multithreaded code and efficient utilization of all available processor cores.

Syntax

Go has a C-like syntax with a minimum of non-obvious and implicit constructs. Readability and purity of code are encouraged, for this purpose a compiler is used that, for example, will refuse to compile code with unused variables. Strong typing forces all conversions (even aliases to the same types) to be performed explicitly.

There are also detailed guides on code styling. The language comes with a linter utility fmt that automatically brings the code to a generally accepted form. In practice, developers read much more than they write code, and styling conventions help save a lot of time.

Standard Library

Despite its compact size, the Go standard library provides a solid solution to a lot of day-to-day tasks without needing a third-party libraries. The standard library is divided into several packages, each of which is responsible for a specific task here are some of the most important ones:

  • fmt - provides functions for formatting and printing data to the console
  • os - provides functions for working with the operating system
  • io - provides unified interface for streaming I/O
  • net - provides tools for simple and fast implementation of servers and clients (both TCP/UDP and HTTP);
  • sync - provides synchronization primitives for goroutines
  • encoding - packages for serialization/deserialization of data into popular formats;
  • crypto - provides cryptographic primitives (hash functions, encryption, etc.)
  • time - provides functions for working with time and date
  • testing - provides tools for quickly and easily writing unit tests and benchmarks out of the box;
  • flag - provides functions for parsing command-line arguments
  • template - provides its own template language for code generation and server-side rendering of HTML pages.
  • other - helper interfaces and functions for handling and wrapping errors

OOP

The language follows partially the OOP paradigm. The interface mechanism is designed to somewhat weaken strict typing. It makes it possible to set restrictions on the type in the form of a list of methods that must be implemented.

Go follows a duck typing model of interfaces implementation if it walks like a duck and it quacks like a duck, then it must be a duck. There is no need to disclose or even explicitly specify the specific interface implementation on a type. It is enough to implement a set of methods for a type so that it automatically begins to satisfy all interfaces with similar method signatures:

Exceptions

Error handling has a special place in Go. In many other languages, errors are handled using the exception mechanism. If an error occurs during the execution of a function, a special event called an exception is thrown, which will either be handled immediately, at the point where the function is called, or pushed up the stack until some function catches it. Usually a try — catch mechanism is used to "catch" this event.

Go uses a different approach, which brings more clarity and explicitness to the error handling process. Functions in Go can return more than one value. This property is actively used by developers, using the error interface as the last return value to return error:

func foo() (int, error) { 
    // assume some error occurred
}

result, err = foo()
if err != nil {
    // handle error
}

The error is a built-in type in Go, meaning you don't need to import any package to use it. Having a separate type allows you to handle errors the same way you are working with a standard library function or a third-party library function. You always work with the same error interface, which means you can compare two errors or use the library's error introspection functions on errors.

Since the possibility of an error occurring during the execution of a function is reflected directly in its signature (the last return value has the type error), the user who calls this function is always forced to handle or ignore the error explicitly, otherwise the code will not compile. It is common practice to add an error as the last (usually second) return argument wherever an error can occur. Most often, this concerns functions in whose body input/output operations occur.

Panic

If the above construct is a typical way to check the execution of a function, then panic is thrown only when the executing code gets into a non-standard situation that cannot be handled. One of the most common causes of panic is dereferencing nila pointer or going beyond the array. By default, panic will go up the stack and terminate all functions until it terminates the function main, and with it the entire process.

However, it can be caught and handled using the construct defer and the built-in recover function. A defer — is another unusual language concept that allows for delayed execution of code blocks: for example, to close files when finished working with them. It can be considered defer as a replacement for context destructors or context managers in other languages.

func foo() {
    // this function will panic
    panic("unexpected!")
}
//...
    // this will run afther the panic is thrown
    defer func() {
        if r := recover(); r != nil {
            // handling panic, r will be equal to "unexpected"
        }
    }()
    // calling the function that will panic
    foo()

It may seem that panic is very similar to the exception mechanism, but it is not. When a function throws an exception, it usually expects the exception to be caught by an exception handler above (try — catch). However, panics are not always caught. The code that triggers the panic usually does not expect it to be handled. Therefore, there is a chance that the process will terminate.

Testing

In Go, it is common to place files with unit tests directly in the package whose functions you are testing. For example, if the code you want to cover with tests is located in a file foo.go, you need to create a file for the tests foo_test.go:

foo/ # package foo
    foo.go # contains the code to be tested
    foo_test.go # contains the tests

The foo.go file contains the following code:

package foo

func Foo() string {
    return "bar"
}

The foo_test.go file contains the following code:

package foo

import (
    "testing"
)

func TestFooFunc(t *testing.T) {
    expectedFooResult := "bar"
    if actualFooResult := Foo(); actualFooResult != expectedFooResult {
        t.Errorf("expected %s; got: %s", expectedFooResult, actualFooResult)
    }
}

You can execute them by simply calling the go test command:

Concurrency

As mentioned earlier, multithreading in Go is implemented according to the CSP (Communicating Sequential Processes) model. With this approach, a program is a set of simultaneously running subtasks that communicate using communication channels. Tasks in Go are goroutines, communication is organized through channels.

Go originally implemented cooperative multitasking: until the code in a goroutine itself transfers control (for example, by attempting to perform a blocking operation), which makes it impossible to take control away from a running goroutine. Since version 1.14, the scheduler has also become preemptive.