Practical introduction to Go: Language basics (2/3)

ยท 15 minute read

This introduction to Go is geared towards anyone, with

If you don’t have a running Go development environment yet, please check out the previous article of this series.

Outcome of of this article will be:

This part will prepare you for the next part in the series: Intro to application development in Go.

A hello-world HTTP server

As this is a show-and-tell introduction let’s first have a look at the code of a small, working Go program. I will guide you through the lines so that you can become familiar with the essentials on the way.

Without further ado here is a minimal HTTP server:

// returns a `200 OK` response with the body `Hello World` to any incoming request
package main

import (
	"fmt"
	"net/http"
)

const address = "127.0.0.1:12345"
var counter = 0

func main() {
	fmt.Println("Starting HTTP server at", address)
	http.HandleFunc("/", helloWorld)
	err := http.ListenAndServe(address, nil)
	if err != nil {
		panic(err)
	}
}

func helloWorld(writer http.ResponseWriter, request *http.Request) {
	counter++
	response := fmt.Sprintf("Hello World (%d)", counter)
	writer.Write([]byte(response))
}

You can find all the Go code also in http://github.com/ukautz/go-intro

Run Go code

Copy the above code and store it in a filed named main.go (wherever you prefer on your file system). Then open up a terminal, navigate to the directory the file is stored in, then execute:

$ go run main.go
Starting HTTP server at http://127.0.0.1:12345

Once that happened, you can visit the URL http://127.0.0.1:12345 either in your browser or with curl (from another terminal). In any case, you should see: Hello World (1) with increasing numbers in parenthesis with each request.

Congratulations! You ran your first Go HTTP server ๐Ÿ˜ ๐ŸŽ‰

Hint: You can stop your HTTP server from the terminal by hitting ctrl + c.

Compile into binary

While we are at it, let me show you how to compile your HTTP server into a binary file, so that it can be deployed to other systems (which do not have the go compiler installed) and executed there.

The difference is minimal. Instead of go run you execute go build, like:

$ go build main.go

This will create a binary, executable file named main in the same directory, which you can then execute as any other executable file:

$ ./main
Starting HTTP server

Hint: The file name main is automatically derived from main.go. You can specify an alternative with the -o <name> parameter, as in: go build -o http-server main.go

Understanding the code

Now that you saw it working, let us go through the code block by block:

Writing code comments

Let’s start out easy, albeit with a very important feature of any language: The code comment. The first line in the example program reads as following:

// returns a `200 OK` response with the body `Hello World` to any incoming request

This is a comment in Go. For multiline comments you can also use the /**/ as in:

/*
This is a
multiline
comment
*/

Declaring the package

Continuing with the next line:

package main

Go orders code into packages. The first line of code in every .go file is always the package <package-name> declaration (aside from comments or whitespace).

The package name should be concise and is by convention a lower case, single-word name, without underscores or mixedCaps. Basically: keep it short & simple.

The package name main which is used here is special: In Go all execution starts in the main package. Since we are writing a simple, executable HTTP server, all the code can live in a single file: the main.go. With larger projects, you would structure your code into multiple files, which would live in multiple directories and thereby multiple packages.

Note: Every .go file within the same directory must be in the same package. If you have multiple .go files in a directory and not all have the same package <name>, then Go will refuse to compile! Also: Usually, the package name matches the name of the directory the file is in. However, this is a convention and can be broken when necessary (more on that in the next article of this series).

Importing other packages

import (
	"fmt"
	"net/http"
)

As mentioned before Go structures code into packages. To use any code from another package you first need to explicitly import the package. The above code block imports the two packages fmt and net/http - from the Go standard library to be precise. The standard library is part of the Go SDK and comes with Go itself - no need to install it separately.

Note: Packages from the standard library have very short import names. You will also use 3rd party libraries (=modules), which are often hosted on public repositories, and which have a longer name of the form <public-location>/<repository>, as in github.com/aws/aws-sdk-go - more on that later.

Package documentation

You can investigate all publicly available Go packages using the https://pkg.go.dev website (formerly known as https://godoc.org). Just visit: https://pkg.go.dev/<package-path> to display the documentation of the exported interface of the package.

In practical terms, here is the documentation for the two packages we imported above:

Note: as this documentation is auto-generated from comments in the source code, this also works with 3rd party packages, like the aforementioned AWS SDK: https://pkg.go.dev/github.com/aws/aws-sdk-go. Of course it would not work with privately hosted (access restricted) packages.

Constants & Variables

const address = "127.0.0.1:12345"
var counter = 0

Most higher level languages differentiate between constants and variables. So does Go. In the above code snippet you see first the declaration of a string-type constant named address then the declaration of the int-type variable named counter.

As the name implies a constant is unchangeable (immutable) after declaration, whereas a variable can be modified (mutated) at leisure. To make that clear:

// declare a variable with an initial value
var iAmChangeable = "foo"

// change the value of the variable -> no problem
iAmChangable = "bar"

// declare constant with value
const iAmUnchangable = "foo"

// try to change constant after declaration -> failure at compile time
iAmUnchangable = "bar"

However, while you can change the content of a variable after declaration, you cannot change the type! Which means the following will fail:

// declare a string variable with initial value
var iAmChangeable = "foo"

// attempt to assign an int value to variable after declaration -> failure at compile time
iAmChangeable = 123

Same as with the import code from above, Go knows two notations. The following is equivalent and makes sense when more than one constant (or variable) needs to be declared:

const (
	address = "127.0.0.1:12345"
)

var (
	counter = 0
)

About types

Go is a strong typed language. This means: every constant or variable is of a specific, unchangeable type.

The type of a variable or constant can be provided implicitly (as above) or explicitly. The above could be written explicitly like so:

const address string = "127.0.0.1:12345"

var (
	counter int = 5
)

As Go is perfectly able to figure out the types in the above example, I omitted them. However, this works for a small set of types, namely: string, int, float64 and bool. Here an example of types Go can “auto detect”:

var aString = "123"
var anInt = 123
var aFloat = 123.456
var aBool = true

There are, roughly speaking, two “kinds of types”:

built-in (primitive) types

user defined (complex) types

Function declarations

Going back to the above main.go file, let’s have a look at the main function therein:

func main() {
	fmt.Println("Starting HTTP server at", address)
	http.HandleFunc("/", helloWorld)
	err := http.ListenAndServe(":12345", nil)
	if err != nil {
		panic(err)
	}
}

Ok, this is a bigger piece of code, let’s go through it slowly:

func main() {
	// ..
}

This is a minimal declaration of a parameter-less function which does not return anything.

The general pattern for functions in Go looks as following:

func nameOfTheFunction(param1 int, param2 string) (string, error) {
	// body
}

To put that in words:

The main function is special

The main function, with an empty signature, within the package main is a special case. It indicates to Go that this function is an entry-point for execution. Basically what is being called when running go run from the shell (or executing the compiled binary).

Having a main function in any other (named) package has no consequence at all. Only in the main package it becomes a special case.

Using packages

Let us investigate the the first line of the body of our main function in detail:

fmt.Println("Starting HTTP server at", address)

You can probably guess what this line is doing: It prints out Starting HTTP server at 127.0.0.1:12345 (on STDOUT, to be precise). The prefix fmt. refers to the previously imported fmt package. Println is a public (exported) function from this fmt package.

Access modifiers (public / private)

You might have noted a difference in spelling of our functions (main and helloWorld) and that Println function from the fmt package. The uppercase P is intentionally, as this is how you specify in Go the accessibility (visibility) of, well, anything. In this case a function, but this goes for types, constants, variables etc.

To be clear, the difference is:

Or even shorter: Only Uppercase is part of the public interface of a package.

Setup HTTP routing

Coming back to the next line of code in the main function:

http.HandleFunc("/", helloWorld)

Go comes with a very powerful HTTP server (and client) out of the box. It is highly configurable and allows you to run anything from a simple hello world to a high-load, high-concurrency, next-facebook-like application.

Luckily, most of the complexity that this entails is hidden until you need it. There are two things you have to do to run a simple web server in Go:

  1. Tell Go what do with incoming requests (aka routing)
  2. Tell Go on what address to start the HTTP server on

The HandleFunc function from the http package handles the first part: configures the routing of the HTTP server.

For that it requires:

Note: Functions can be passed as arguments the the same way as variables or constants can.

Start the HTTP server

Next up in the main function:

err := http.ListenAndServe(address, nil)

This call to the public function ListenAndServe of the net/http package starts an HTTP server and binds it to the provided address.

The first input parameter address (above declared as a constant with value: 127.0.0.1:12345) provides the address the HTTP server must listen on.

The second input parameter nil is commonly seen in Go. This is equivalent to null in many other languages and is an “empty value” for many (non-primitive) types in Go. As ListenAndServe supports multiple use-cases, it needs that second, optional parameter. We don’t need that option here and hence can (or rather have to) provide nil.

The return parameter err (of the type error) will only be returned if the listening and serving fails. This could happen, for example, if the port 12345 is already in use on the address 127.0.0.1. If no error occurs then ListenAndServe will block and keep running until you abort it (i.e. hit ctrl + c).

Error handling

The last code block in the main function will soon become a common occurrence to you:

if err != nil {
	panic(err)
}

Every language has their own way do deal with errors at runtime. Many do exception handling, which delegates runtime errors until something “catches” them (like throw <error> and try { something } catch <exception> { handle }).

Go does not have any of that. Instead it solves error handling by convention. This convention is to return any error that happens in the function and then appropriately handle the error in the caller.

Hence you will very often find the if-err-return pattern like so:

func SomeFunction() error {
	value, err := someCall()
	if err != nil {
		return err
	}
}

Now, the caller of SomeFunction must take care to delegate the error “further up” as needed. Usually this means the error is handed up and up, until someone writes a main function and uses panic to act on the error, as we are doing in the example.

The panic function is one of the few built-in functions. It prints out a message (here: the error) then exits the program with a non-zero exit code. Depending on compilation it also prints a stack-trace from the panic location, so that developers - like you - can roll up our their sleeves and dig into debugging with a good idea where to start.

There is also a more condensed form of the if-err-return pattern, which looks like:

if err := anotherCall(); err != nil {
	return err
}

HTTP handler function

This last piece of code implements our HTTP handler function, which was associated earlier with http.HandleFunc() call:

func helloWorld(writer http.ResponseWriter, request *http.Request) {
	counter++
	response := fmt.Sprintf("Hello World (%d)", counter)
	writer.Write([]byte(response))
}

In short what is does:

The function input parameters refer to custom types declared in the http package. As we are using this function as an argument for http.HandleFunc above it must have this exact form:

func handler(http.ResponseWriter, *http.Request) {
	// body
}

Note: The example code is not concurrency-safe. If you were to send requests to our HTTP server in parallel, it would eventually die and exist with a panic, since concurrent attempts to increment counter and / or read from counter would fail. This can be easily safe-guarded in Go, but I chose not to do it here for the sake of readability and brevity. If you feel like exploring, have a look at the sync or sync/atomic packages in the standard library.

Calling methods on instances of types

writer.Write([]byte(response))

The above expression in the body of the helloWorld method shows how a method of a type (=http.ReponseWriter) is called on the instance of a type (=writer).

It would be pronounced like so: calling the Write method on the writer variable, which is an instance of the http.ResponseWriter type.

While this looks somewhat similiar to the http.HandleFunc and http.ListenAndServe called in the main function, the difference is that the Write call is “executed” on an instance (writer), not a package (http). It’s a method, not a function.

Without going into too much detail (more on that in the following article of this series), know that custom types can have methods. You can then call these methods on the instances of those custom types. As in many other languages.

To investigate what methods are available for a type, you can again use pkg.go.dev and find out. In the case of the http.ResponseWriter have a look here:

Type conversion

[]byte(response)

This expression shows a type conversion. The inner part (response) is a string-type variable. As with in many other languages, strings and byte-arrays are closely related. Hence Go allows you to convert the string on-the-fly into []byte (which Go calls slice of bytes, not byte array).

The conversion needs to be done because the Write() method expects a single []byte input parameter and not a string.

Go allows you to convert from and to various built-in types. For example:

Summary

Ok, this was quite a bit. You can find the full source code of everything above here: https://github.com/ukautz/go-intro/tree/master/beginner/http

A quick review on what you learned:

If you are curious, check out another small command line application, which creates a table of content (toc) for Markdown files: https://github.com/ukautz/go-intro/tree/master/beginner/mdtoc

Next up

Learn writing full fledge command line applications along with diving deeper into the Go language in the last article of this series.