Introductory Go Programming Tutorial

An Overview of the Core Language by Jay Ts

This article originally appeared in the October 2018 issue of Linux Journal.

Go Programming

Maybe you've heard of Go. It was first introduced in 2009, but like any new programming language, it took a while for it to mature and stabilize to the point where it became useful for production applications. Nowadays, Go is a well-established language that is used for network and database programming, web development, and writing DevOps tools. It was used to write Docker, Kubernetes, Terraform and Ethereum. Go is accelerating in popularity, with adoption increasing by 76% in 2017, and now there are Go user groups and Go conferences. Whether you want to add to your professional skills, or are just interested in learning a new programming language, you may want to check it out.

Why Go?

Go was created at Google by a team of three programmers: Robert Griesemer, Rob Pike, and Ken Thompson. The team decided to create Go because they were frustrated with C++ and Java, which over the years had become cumbersome and clumsy to work with. They wanted to bring enjoyment and productivity back to programming.

The three have impressive accomplishments. Griesemer worked on Google's ultra-fast V8 JavaScript engine, used in the Chrome web browser, Node.js JavaScript run time environment, and elsewhere. Pike and Thompson were part of the original Bell Labs team that created Unix, the C language, and Unix utilities, which led to the development of the GNU utilities and Linux. Thompson wrote the very first version of Unix and created the B programming language, upon which C was based. Later, Thompson and Pike worked on the Plan 9 operating system team, and they also worked together to define the UTF-8 character encoding.

Go has the safety of static typing and garbage collection along with the speed of a compiled language. With other languages, "compiled" and "garbage collection" are associated with waiting around for the compiler to finish, and then getting slowly-running programs. But Go has a lightning-fast compiler that makes compile times barely noticeable, and a modern, ultra-efficient garbage collector. You get fast compile times along with fast programs. Go has concise syntax and grammar with few keywords, giving Go the simplicity and fun of dynamically-typed interpreted languages like Python, Ruby, and JavaScript.

If you know C, C++, Java, Python, JavaScript, or a similar language, many parts of Go, including identifiers, operators, and flow control statements, will look familiar. A simple example is that comments can be included as in ANSI C:

// This comment continues only to the end of the line.

/* This type
   of comment
   can span
   multiple lines. */

In other ways, Go is very different from C. One obvious difference is its declaration syntax, which is more like that of Pascal and Modula-2. Here is how an integer variable is declared in Go:

var i int

This syntax may look "backwards" to a C or Java programmer, but it is much more natural and easier to work with. Declarations in Go can usually be directly translated to or from English or another human language. In the above example, the declaration can be read as, "Variable i is an integer." Here are some more simple examples:

var x float32	// "Variable x is a 32-bit floating point number."
var c byte	// "Variable c is a byte."
var s string	// "Variable s is a string."

And it is also possible to do this with more complex data types:

var a [32]int	// "Variable a is an array of 32 integers."

var s struct {	// "Variable s is a structure composed of
	i int	// i, an integer,
	b byte	// b, a byte,
	s []int	// and s, a slice of integers."
}

The idea of Go's design is to have the best parts of many languages. At first, Go looks a lot like a hybrid of C and Pascal (both of which are successors to Algol 60), but looking closer, you will find ideas taken from many other languages as well.

Go is designed to be a simple compiled language that is easy to use, while allowing concisely-written programs that run efficiently. Go lacks extraneous features, so it's easy to program fluently, without needing to refer to language documentation while programming. Programming in Go is fast, fun, and productive.

Let's Go

First, make sure you have Go installed. The Go Team has easily-installed distributions for Linux (including Raspberry Pi), macOS, FreeBSD, and Windows on the Go website at https://golang.org/dl/

You will also need to make sure that your PATH environment variable is set properly to allow your shell or command line processor to find the programs that make up the Go system. On Linux, macOS, or FreeBSD, add /usr/local/go/bin to your PATH, somewhat like this:

$ PATH=$PATH:/usr/local/go/bin

On Windows, setting a PATH environment variable is a little more complicated. In either case, you can find directions for setting PATH in the installation instructions for your system, which appear in a web page after you click to download the installation package.

On Linux, you may be able to install Go using your distribution's package management system. To find the Go package, try looking for "golang", which is a synonym for Go. Even if you can do that, it may be better to download a distribution from the Go website to make sure you get the most recent version.

When you have Go installed, try this command:

$ go version
go version go1.11.1 linux/amd64

The output shows that I have Go version 1.11.1 installed on my 64-bit Linux machine.

Hopefully, by now you've become interested and want to see what a complete Go program looks like. Here's a very simple program in Go that prints "hello, world".

package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n")
}
Play with this code at the Go Playground

The line package main defines the package that this file is part of. Naming main as the name of the package and the function tell Go that this is where the program's execution should start. We need to define a main package and main function even when there is only one package with one function in the entire program.

At the top level, Go source code is organized into packages. Every source file is part of a package. Importing packages and exporting functions are child's play.

The next line, import "fmt", imports the fmt package. It is part of the Go standard library, and contains the Printf() function. Often you will need to import more than one package. To import the fmt, os, and strings packages, you can either type

import "fmt"
import "os"
import "strings"

or

import (
    "fmt"
    "os"
    "strings"
    )

Using parentheses, import is applied to everything listed inside the parentheses, saving some typing. You will see parentheses used like this again elsewhere in Go, and Go has other kinds of typing shortcuts, too.

Packages may export constants, types, variables, and functions. To export something, just capitalize the name of the constant, type, variable or function you want to export. It's that simple.

Notice that there are no semicolons in the "hello, world" program. Semicolons at the ends of lines are optional. Although this is convenient, it leads to something to be careful about when you are first learning Go. This part of Go's syntax is implemented using a method taken from the BCPL language. The compiler uses a simple set of rules to "guess" when there should be a semicolon at the end of the line, and it inserts one automatically. In this case, if the right parenthesis in main() were at the end of the line, it would trigger the rule, so it's necessary to place the open curly bracket after main() on the same line.

This formatting is a common practice that's allowed in other languages, but in Go, it is required. If we put the open curly bracket on the next line, we will get an error message.

Go is unusual in that it either requires or favors a specific style of whitespace formatting. Rather than allowing all sorts of formatting styles, the language comes with a single formatting style as part of its design. The programmer has a lot of freedom to violate it, but only up to a point. This is either a straitjacket or godsend, depending on your preferences! Free-form formatting, allowed by many other languages, can lead to a mini Tower of Babel, making code difficult to read by other programmers. Go avoids that by making a single formatting style the preferred one. Since it's fairly easy to adopt a standard formatting style and get used to using it habitually, that's all you have to do to be writing universally-readable code. Fair enough? Go even comes with a tool for reformatting your code to make it fit the standard:

$ go fmt hello.go

Just two caveats: Your code must be free of syntax errors for it to work, so it won't fix problems such as failing to put an open brace on the same line. Also, it overwrites the original file, so if you want to keep the original, make a backup before running go fmt.

The main() function has just one line of code to print the message. In this example, the Printf() function from the fmt package was used to make it similar to writing a "hello, world" program in C. If you prefer, you can also use

fmt.Println("hello, world")

to save typing the \n newline character at the end of the string. This is another example of Go being similar to C and Pascal. You get formatted printing and scanning functions closely resembling the ones C programmers are used to, plus simple and convenient functions like Println() that are similar to writeln() and other functions in Pascal.

So let's compile and run the program. First, copy the "hello, world" source code to a file named hello.go. Then compile it using this command:

$ go build hello.go

And to run it, use the resulting executable, named hello, as a command:

$ hello
hello, world

As a shortcut, you can do both steps in just one command:

$ go run hello.go
hello, world

That will compile and run the program without creating an executable file. It's great for when you are actively developing a project and you are just checking for errors before doing more edits.

Next, let's look at a few of Go's main features.

Concurrency

Go's built-in support for concurrency, in the form of goroutines, is one of the language's best features. A goroutine is like a process or thread, but is much more lightweight. It is normal for a Go program to have thousands (or maybe even a few millions) of active goroutines. Starting up a goroutine is as simple as

go f()

The function f() will then run concurrently with the main program and other goroutines. Go has a means of allowing the concurrent pieces of the program to synchronize and communicate using channels. A channel is somewhat like a Unix pipe; it can be written to at one end, and read from at the other. A common use of channels is for goroutines to indicate when they have finished.

The goroutines and their resources are managed automatically by the Go runtime system. With Go's currency support, it's easy to get all of the cores and threads of a multi-core CPU working efficiently.

Synchronization with Channels

Any language that supports multi-threaded or concurrent processing needs to have a mechanism that allows simultaneously or concurrently-running code to coordinate modifying and accessing data. For this purpose, Go has channels, which are first-in-first-out (FIFO) queues that function in a manner that allows them to be used to synchronize goroutines.

In the simplest case, a channel is unbuffered and allows only one pending operation at a time. Therefore, it is either empty or has a read or write that is waiting for another goroutine to perform the corresponding action.

Synchronization is performed as in this example: Suppose a goroutine attempts to receive (read) from an empty channel. The goroutine is put to sleep by the Go runtime system until another goroutine sends on (writes to) the channel. Then there is something there for the first goroutine to receive, so it's awakened and the receive operation succeeds. This mechanism allows a channel to be used to make a goroutine wait for another to signal it by performing an operation on their shared channel. Go's channels can be used to implement mutually-exclusive locks (mutexes) that are found in other languages, and the sync Go package does exactly that.

Using channels is very simple and concise. Before they are used, channels are allocated using Go's built-in make() function. For example,

var intch chan int	// intch is a channel of integers
intch = make(chan int)	// allocate a new channel of integers

declares and then creates a new channel named intch for sending and receiving integers. Sending on a channel is as simple as

intch <- 1	// send the integer 1 on the channel

and receiving from a channel (in another goroutine) is just as simple:

var num int	// num is an integer
num = <-intchan	// receive an integer from the channel

Here's a complete example that shows a channel in use:

package main

import "fmt"

var intch chan int		// intch is a channel of integers

func main() {
	intch = make(chan int)	// allocate a channel of integers
	go print_number()	// start the goroutine
	intch <- 37		// send 37 on the channel
	<- intch		// wait for a response before exiting
}

func print_number() {
	var number int		// number is an integer

	number = <-intch	// receive an integer from the channel
	fmt.Printf("The number is %d\n",number)  // print it
	intch <- 0		// send a response
}
Play with this code at the Go Playground

Types, Methods, and Interfaces

You might wonder why types and methods are together in the same heading. It's because Go has a simplified object-oriented programming model that works along with its expressive, lightweight type system. It completely avoids classes and type hierarchies, so it's possible to do complicated things with datatypes without creating a mess. In Go, methods are attached to user-defined types, not to classes, objects, or other data structures. Here's a simple example:

package main

import "fmt"

// make a new type MyInt that is an integer

type MyInt int

// attach a method to MyInt to square a number

func (n MyInt) sqr() MyInt {
    return n*n
}

func main() {
	// make a new MyInt-type variable
	// called "number" and set it to 5

	var number MyInt = 5

	// and now the sqr() method can be used

	var square = number.sqr()

	// the value of square is now 25

	fmt.Printf("The square of %d is %d\n",number,square)
}
Play with this code at the Go Playground

Along with this, Go has a facility called interfaces that allow mixing of types. Operations can be performed on mixed types as long as each has the method or methods attached to it, specified in the definition of the interface, that are needed for the operations.

Suppose we've created types called cat, dog, and bird, and each have a method called age() that return the age of the animal. If we want to add the ages of all animals in one operation, we can define an interface like this:

type animal interface {
    age() int
}

The animal interface then can be used like a type, allowing the cat, dog, and bird types to all be handled collectively when calculating ages.

Unicode Support

Considering that Ken Thompson and Rob Pike defined the Unicode UTF-8 encoding that is now dominant worldwide, it may not surprise you that Go has good support for UTF-8. If you've never used Unicode and don't want to bother with it, don't worry; UTF-8 is a superset of ASCII. That means you can continue programming in ASCII and ignore Go's Unicode support and everything will work nicely.

In reality, all source code is treated as UTF-8 by the Go compiler and tools. If your system is properly-configured to allow you to enter and display UTF-8 characters, you can use them in Go source file names, command-line arguments, and in Go source code for literal strings and names of variables, functions, types, and constants.

Just below, you can see a "hello, world" program in Portuguese, as it might be written by a Brazilian programmer.

package main

import "fmt"

func faça_uma_ação_em_português() {
	fmt.Printf("Olá mundo!\n")
}

func main() {
	faça_uma_ação_em_português()
}
Play with this code at the Go Playground

In addition to supporting Unicode in these ways, Go has three packages in its standard library for handling more complicated issues involving Unicode.

Summing it Up

By now, maybe you understand why Go programmers are enthusiastic about the language. It's not just that Go has so many good features, but that they are all included in one language that was designed to avoid overcomplication. It's a really good example of the whole being greater than the sum of its parts.

To learn more about Go, visit the Go website and take A Tour of Go.

Contact the author