Go: A Comprehensive Introduction

28 minute read Published:

A comprehensive introduction to the go programming language
Table of Contents



At first: Go is a fascinating language. The language addresses many issues other languages are having and as you may have noticed there got a bunch of new programming languages released in the last years like Rust(2010), Elm(2012) or Crystal(2014) for example, which all try to do something better than the ancient ones. So it’s important to keep track of actual language benefits to pick the right tool for the job.

Go was designed at Google in 2007 to improve programming productivity in an era of multicore, networked machines and extremely large codebases.

source: Wikipedia

In simpler terms: the goal was to design a simple, easy to understand and fast language.

Google had problems to onboard new employees. It took a massive amount of time until they got productive in Google’s huge codebases full of C++ and Java. They wanted to address the issue with a simpler programming language. That’s how Go was born.

The most famous people who helped designing the Go-language are Robert Griesemer who is known for his work on Hotspot and the JVM, Rob Pike who is known for his work on the Plan 9 operating system and UTF-8 and last but not least Ken Thompson who is primarily known for the development of the UNIX operating system and co-authoring the C programming language together with Dennis Ritchie. But also the predecessor, the B programming language, early artificial chess computers and also, together with Rob Pike, he was involved in the development of UTF-8 and Plan9.

To summarize: People who seem to know what they are doing.

After two years of development, Go reached the first stable version and got released in November 2009.

Characteristics of Go

The language is reduced down to 25 keywords only. If you compare that to the count of other languages it is significantly smaller.

I’ve found this comparison here, but I must admit that I don’t know how accurate it is

comparison of keywords

source: Github

It has a C-like syntax and is build in Assembly and C++. Go provides imperative patterns as well as object-oriented patterns you will see later how this plays together. Go enables you to easily write concurrent code with just one keyword, called goroutines which you see in detail later too.

Go compiles to a single binary, which includes everything you need to run it. You don’t need any system library on your machine or anything else. That means that the binary is relatively big, a simple hello world example is 1.9 megabytes in size. However, even in real life projects, I’ve never got as big as 10 megabytes or more. I know this sounds like a lot, but I think when considering the specifications of today’s computers this isn’t a problem at all.

You can also easily cross-compile Go programs from any machine for different platforms. It’s all handled by two environment variables. More on that later.

And it’s fast, of course, that was one of the goals when developing the language.


Go has batteries included.

Standard Library

Go has a huge standard library and you can write very complex tools without using any third party dependency. If you want to have a look at the standard library you can find every available package as well as their documentation at the official website golang.org.


It comes shipped with a formatter for your go code called <gofmt|go fmt>. It’s not like in the Javascript world where half the users are using prettier, others using some other linter, xo for example, which also helps to format code and some others don’t use anything at all. If you find a Go repository somewhere in the wild it is most likely formatted with gofmt. There are editor plugins for every editor which runs the program on your code on save. Personally, I’ve never seen Go code which wasn’t formatted with gofmt.

go vet

Go also ships with go vet which is a linter to prevent you from common errors. The description of the tool itself:

Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string. Vet uses heuristics that do not guarantee all reports are genuine problems, but it can find errors not caught by the compilers.

This is really a short summary and it catches many more errors even if not all. It is a great help in development.

Testrunner + Benchmarker

Go has a test runner and benchmarker included. Everything you need to do is if you have a main.go file to create a main_test.go file. Then you can run go test or go test -bench=. if you are in the same directory. How a test file looks like we will see later.

package management

Go also has a simple mechanic to use dependencies. You import them as every core library but instead of only the name you are using the GitHub link like import "github.com/<user>/<repository>" and that is all.

There are some pitfalls by that. Go always uses the master branch of the package which is kind of bad. But there are alternatives. One which is very common is to clone the remote repository and put it in your own version control in a vendor directory. This is called vendorizing but is also not the perfect solution. There is gopkg which does help in this case and you can require explicit branches or tags. You would require it like import "gopkg.in/<user>/<repository>.<tagname|branchname>". This is a bit better and you can rely on semver. At last, there was released a tool named dep, which looks like a more sophisticated solution. There you can specify in a special file (Gopkg.toml) which dependency you want and either which branch, version, tag or commit hash you want. You can also include version ranges like in package.json or composer.json for example. It also creates a lock file to ensure everyone gets the same dependencies. Dep isn’t in the core yet, but it’s widely used and I think the recommended method to do dependency management in go today. It’s from the Go authors and is called an “official experiment”.

Since version 1.11 Go ships with go mod which is very simple to use. You start with go mod init github.com/<username>/<reponame> which creates you a go.mod file with the module name and the current Go version:

module github.com/<username>/<reponame>

go 1.12

When you import something in your source and do a go build your go.mod file is automatically updated and contains the dependency you need together with some version information to be able to do reproducible builds. Along the way there was also a go.sum file created which contains some cryptographic hashes of the content of the dependency modules.

If you want to learn more you can read the Go wiki on Modules.


The documentation is generated by code comments. Even the documentation for the standard library is documented by comments and you can also include examples which can be run from the documentation page itself. Look at the strings.Compare function for example. So you get documentation for your package automatically when you act like a proper developer and comment your code. ;)

Go report card

There is Go Report Card where you can simply put in your repository and let seven different linters run over your projects which are specialized on different issues which is also very helpful for your code quality.

Who is using Go?

Go is widely used especially in the DevOps scope, for automating things, but can also be used for everything else. One big Go user is of course Google. But there are more, the most widespread tool written in Go is probably docker. If you want to see a big list of companies who are using Go you can find it on the Go Wiki.

But now …

Talk is cheap. Show me the code.

As Linus Torvalds would say.

Code examples

You can find every code example on Github

Hello World

A simple Hello World program looks like this:

// helloworld.go
// every go file belongs to a package
package main

// we import "fmt" from the standard library
import "fmt"

// our main function
func main() {
	// Prints out "Hello World!"
	fmt.Println("Hello World!")

We need to define the name of our package. Every go file needs to start with a package name. After that, we define our imports. Everything that is in another package needs to be imported like that. The function keyword known from most languages is shortened to func. In our main function, we call the Println function from the fmt package (fmt stands for format) with a string which gets printed out to the console when we run go run helloworld.go. We could also go build and execute the binary the go compiler is creating, which is called like the folder we are in or if we provide a binary name like the name we are providing and executing that binary. go build -o binaryName helloworld.go && ./helloworld. But I find the go run shortcut better to integrate into the development workflow as it is shorter and doesn’t need as many steps.

I will leave out any not needed boilerplate code in the below examples to keep them readable and understandable with focus on the important parts. This means they have errors like unused variables and so one, if you want to look for executable examples look at the Github Repo.

Variables and Constants

// variable declaration
var a string 
// variable initialization
a = "variable initialization"

// variable declaration and initialization
var b string = "variable declaration and initialization" 

// sugar for declaration and initialization
c := "sugar for declaration and initialization"

// would produce an error, as this declares the variable again
// c := "reassign c"

// works as this don't declare c again
c = "reassign c"

// declare multiple variables at once
var d, e string 

// declare and initialize multiple variables at once
var (
  f bool = false
  g string = "hey"

// define a constant
const MY_CONSTANT int = 1
// error, constants can't change their value

That’s pretty straight forward. The only pitfall is when using the := operator, which declares and initializes the variable at once inferred of the type you provide as a value. If you want to reassign a variable you can not do it with := and you need to use =.

What is needed to know is that variables are scoped. So when declaring a variable inside an if-block, it is only available inside this block. Same for functions or files. Yes, you can have global variables, but as a common joke among programmers, the best prefix for global variables is //.


if true {
	// would be executed

if false {
	// would not be executed
} else {
	// would be executed

if false {
	// would not be executed
} else if true {
	// would be executed
} else {
	// would not be executed

I don’t think this needs explanation, except that you don’t need parentheses in simple if comparisons. (:


c := '&'

// this would fail with the default branch
// delete the leading // to try this out
// c = 'a'

switch c {
case '&':
case '<':
case '>':
case '"':
	panic("unrecognized escape character")

The default branch is optional, but I would recommend adding a default branch to every switch statement.


for i := 0; i <= 5; i++ {

for i := 0; i <= 5; i++ {
    // doesn't execute the loop the fourth and fifth time
	if i == 3 {

for i := 0; i < 5; i++ {
	if i < 2 {
    	// wouldn't print the value of i 
        // when i < 2, so the first two loops


// equivalent of a while loop
sum := 1
for sum < 10 {
	sum += sum
	// would be 16 at the end

var array = [5]int{10, 20, 30, 40, 50}

// equivalent of a foreach loop
for key, value := range array {
	fmt.Println(key, value)

// inifite loop
// for {
// 	fmt.Println("infinite ...")
// }

Loops are working like I would expect them to, except that there is only one keyword to use and not 3 or more (while, for, foreach, etc.) and depending what you put in there it behaves differently.


// a simple add function with to arguments
// and an int as return value
func add(a int, b int) int {
	return a + b

func main() {
	// call the function
	result := add(1, 2)

There is nothing special to it, this is like every other language is doing it.

Multiple return values

But now for something cooler, you can have multiple return values:

// multiple return values, 
// one of my favourite feature of Go
func getCoordinates() (int, int) {
	return 1, 2

func main() {
	// call the function and assign the values to x and y
	x, y := getCoordinates()
 	// call  the function and throw away the second return value
  	x, _ = getCoordinates()
   	// call  the function and throw away the first return value
	_, y = getCoordinates()
   	// call  the function and throw away both return values
	_, _ = getCoordinates()
  	// the same as the one directly above
  	// would produce an error:
  	// multiple-value getCoordinates() in single-value context
 	// x = getCoordinates

The multiple return values feature is really cool I think! No need to create objects or arrays like in other languages and later check how many items are in there or which fields are filled. It really eases the development workflow and you can focus on the important parts. Also if you don’t need any return value and you put in an _ Go doesn’t allocate memory for it. if you don’t care for a return value at all you can omit the variable assignment but if you assign fewer variables then the function returns you will get a compiler error, which is good to prevent yourself from mistakes!

Named Return Values

Functions can have named return values, I’ve never used it but I think it comes in handy when you have many variables with slightly similar naming.

// in case the function would be easier to read then
func namedReturnValues() (x int) {
	x = 3
	// do very complex stuff
	return // would return x


Defer is a keyword you can put in front of a statement and this statement gets executed at the end of the function.

// defer is always executed at the end of the function
// so you can not forget to close some buffers, files or connections
func myDeferFunc() int {
	defer fmt.Println("This should be printed at the end")
	// do very complex stuff
	fmt.Println("complex stuff completed")
  	return 1

func main() {

The output of this is:

complex stuff completed
This should be printed at the end

As we can see, the defer is executed at the end of the first function, but before the main function is executed further.

Inline aka Anonymous Functions

You can create anonymous functions, if you have a background in functional programming or javascript, for example, you have probably done it several times. It’s good when you use something only one time and so it doesn’t need to be extracted into an own external function. Here is how it looks like:

// inline function aka anonymous function
add := func(x int, y int) int {
	return x + y
fmt.Println(add(1, 2))

Pretty much like a normal function but you assign it to a variable which you can call later.

If you want to create an immediately-invoked function expression you can do it by adding () at the end of the function and it gets executed without calling it later:

func() {
	fmt.Println("HELLO WORLD")

Variadic Functions

Variadic functions are functions which can accept n arguments of the same type. They get packed into an array and can be used then.

// variadic functions can take
// as many arguments of the same type as you like
func myVariadicFunc(ints ...int) {

If you call that function with myVariadicFunc(1, 2, 3) you will get \[1 2 3\]. If you call it with myVariadicFunc(1, 2, 3, 4, 5) you will get \[1 2 3 4 5\].

If we want to create a sum function which can take as many integers as we want we can define it like this:

func sum(ints ...int) int {
	result := 0
	// loop over the int array 
  	// and throw away the key (which would be the array index)
	for _, value := range ints {
		result += value
	return result

If we call that function with sum(1, 2, 3, 4, 5) we would get back 15 as expected.

Passing Functions around aka Functions as first class citizens

You can pass functions around. Again, if you come from functional programming or Javascript this might feel natural for you, but many languages are lacking this feature. To be honest

// We need to declare a type Convert 
// which is a function which takes a string as an argument 
// and returns a string
type Convert func(string) string

// first convert function
func ask(smth string) string {
	return fmt.Sprintf("%s????", smth)

// second convert function
func exclamate(smth string) string {
	return fmt.Sprintf("%s!!!!", smth)

// convert takes a string and a function which is of type Convert
func convert(smth string, fn Convert) string {
	return fmt.Sprintf("%s", fn(smth))

func main() {
	fmt.Println(convert("what", ask)) // produces "what???"
	fmt.Println(convert("what", exclamate)) // produces "what!!!"


Structs are a way to mimic object-oriented programming in Go. Let us declare a Person struct:

// declare a struct named Person
type Person struct {
	name string
	age  int

A person has a name of type string and an age of type int. Let us create an instance of that type:

// create an instance of the struct
me := Person{"Max", 28}

Now we can define functions that run on that type of struct:

func (p Person) sayName() {
	fmt.Println("My name is: ", p.name)

And call them with:

me.sayName() // My name is Max

If we want to change the data of the Person we need to define the function to a pointer of Person, that sounds a bit scary at first but it’s easier than you think if you look at the code:

func (p *Person) birthday() {

It’s only that asterisk that needs to be there. If we have a function sayAge:

func (p Person) sayAge() {
	fmt.Printf("I'm %d years old\n", p.age)

We can test our birthday function:

me.sayAge() // I'm 28 years old
me.birthday() // change value of age
me.sayAge() // I'm 29 years old

If we would forget the little asterisk to indicate we want a pointer rather than a copied value and say we have a birthday2 function:

func (p Person) birthday2() {
	p.age += 1

And call it:

me = Person{"Max", 28}
me.sayAge() // I'm 28 years old

It would have no effect, inside the function the struct would be different but not at the outside world. If you want to know more about that you can google something like “call by reference vs call by value”.

Go doesn’t have any inheritance mechanism, but there is something called duck typing.

type Pupil struct {
	grade int

A Pupil is a person but also in a specific class.

to instantiate such a struct is not as easy as it could be but it works like this:

schoolkid := Pupil{Person{"Max", 14}, 7}

The cool thing is that we can use our functions from our Person type for the Pupil type:

schoolkid.sayAge() // 14
schoolkid.sayAge() // 15


Let’s assume we want to create an interface which must implement the Say function:

type Speaker interface {

And let us assume we have our Person struct from the struct paragraph and want to implement the Say function for the Speaker:

// interface implementation of Say method to become a Speaker
func (p Person) Say(msg string) {
	fmt.Println(p.name + ": " + msg)

We can now create a function which accepts a Speaker as an argument:

func speakSomething(s Speaker) {

Now we can define a different struct, say a Dog and a Say function for that struct:

type Dog struct {
	name string

func (d Dog) Say(msg string) {
	fmt.Println(d.name + ": bark bark bark")

Now we can use this function on our structs and like the other functions it even works on the Pupil struct:

speakSomething(me) // Max: something
speakSomething(schoolkid) // Max: something
dog := Dog{"Bello"} // instantiate a Dog
speakSomething(dog) // Bello: bark bark bark



How do we do error handling in Go? It might be way easier than you think. We use the multiple return values for us and check for an error. Let us create a function that can return an error:

// An error is a datatype
// often used as a second return value
func someFuncThatCanFail(shouldFail bool) (int, error) {
	if shouldFail {
		// create a new error with a message
		return 0, errors.New("something went wrong")
	return 0, nil

Now when we call this function we check if the second return value is nil or not:

var _, err := someFuncThatCanFail(false)

// would not be called
if err != nil {
	// prints the error, in a real application you might want to
	// log this into a file or something
	// do error handling

// would be called
_, err = someFuncThatCanFail(true)
if err != nil {
	// do error handling

// a panic prints a message to stdout and exits the application
// with an exit value != 0

If you have an error which you can’t handle you have always the ability to do a so-called panic which exits the application at this point.


Every programming language needs to have some kind of module or code separation system. As you may have noticed in the hello world example, every go file needs to have a package name. This is used for our modules.

Let’s create a folder called pkg and place a file called pk.go in there:

// the name of the subpackage
package pkg

import "fmt"

// Capitalized is an exported function
func DoStuff() {
	fmt.Println("Doing stuff ...")

// same for constants/variables with capitalization
var MyConst string = "Some constant"

// Add function
func Add(x int, y int) int {
	return x + y

We use a different package name, that’s the name of the package we need to import soon. Every capitalized function, constant, variable or type is exported. You don’t need any export keyword or else. If you let your program run through Go Report Card there are linter who will tell you that you need comments above exported variables, which is good because that’s how the documentation is generated also. If you might have noticed, in every other module we used (for example the fmt module) every function call began with a capitalized letter(fmt.Println). So the standard library is also just a module.

Now let us call these filthy functions:

package main

import (

	// $GOPATH/src/
	// import a module
	// if this is a github repo it would work on the same way

func main() {
	// call DoStuff of the subpackage

	// prints the constant declared in the subpackage

	x := pkg.Add(1, 2)

The only thing that needs explanation is the $GOPATH I think, the rest should be pretty clear and straightforward. Go assumes you have a $GOPATH environment variable set. There will live all your code and all your dependencies. If you don’t set this environment variable it’s in your home directory: $HOME/go/. There you can see three folders: bin, pkg, and src. In pkg are build artefacts stored. In binaries are the binaries stored if you use go installfor example: go install github.com/mstruebing/tldr/cmd/tldr/. Then you only need to add $GOPATH/bin to your $PATH environment variable and can call every binary installed via go install. src is, of course, where the code lives. There will be stored every dependency you use as well as your own code. There you will have a path like: $HOME/go/src/github.com/mstruebing/go-examples/ where the root of the repository where I store my examples is. That’s how it is working that you only need the GitHub link to the repository to use it as a dependency.


As I said earlier a test runner and benchmarker is already included in Go, so it isn’t hard to set up. At first, we create a function in a file, let’s call it tests_and_benchmarks.go😀

func add(x int, y int) int {
	return x + y

If we want to test that function we need to create a file tests_and_benchmarks_test.go, the prefix Test is required. You can look into the documentation of the testing package if you want to dig deeper.

func TestAdd(t *testing.T) {
	result := add(1, 2)

	// yep, as straightforward as error handling
	if result != 3 {
		t.Errorf("Expected add(1,2) to be 3, got %d instead", result)

We can run the test with go test:

$ go test
ok      github.com/mstruebing/go-examples/09_tests_and_benchmarks       0.001s

If we let the test fail, say we change the add function to subtract instead of adding the output would be:

$ go test
--- FAIL: TestAdd (0.00s)
    tests_and_benchmarks_test.go:14: Expected add(1,2) to be 3, got -1 instead
exit status 1
FAIL    github.com/mstruebing/go-examples/09_tests_and_benchmarks       0.001s


A benchmark is as easy to write as a test.

func BenchmarkAdd(b *testing.B) {
	for n := 0; n < b.N; n++ {
		add(10, 20)

Pay attention to the b.N usage. You will understand in a bit. To execute a benchmark in the current directory use go test -bench=.

$ go test -bench=.
goos: linux
goarch: amd64
pkg: github.com/mstruebing/go-examples/09_tests_and_benchmarks
BenchmarkAdd-8          2000000000               0.30 ns/op
ok      github.com/mstruebing/go-examples/09_tests_and_benchmarks       0.630s

You can see how often your function was called (2000000000, this is the b.N) and how many operations in a nanosecond where executed. In the end, you can see how long it took to execute the whole benchmark.

Now let us add a second add function with a loop inside to make it slower:

func add2(x int, y int) int {
	sum := 0
	for i := 0; i < 10000; i++ {
		sum += i

	return x + y

An addition benchmark:

func BenchmarkAdd2(b *testing.B) {
	for n := 0; n < b.N; n++ {
		add2(10, 20)

And we execute the benchmark again:

$ go test -bench=.
goos: linux
goarch: amd64
pkg: github.com/mstruebing/go-examples/09_tests_and_benchmarks
BenchmarkAdd-8          2000000000               0.30 ns/op
BenchmarkAdd2-8           500000              2961 ns/op
ok      github.com/mstruebing/go-examples/09_tests_and_benchmarks       2.139s

We can see that the second benchmark gets executed much less than the first one. Go figures out a good number for b.N to get reliable results. From the docs:

During benchmark execution, b.N is adjusted until the benchmark function lasts long enough to be timed reliably.


Let us order coffee. We will order different kind of coffees which need different times to finish.

type Coffee struct {
	kind string // kind of coffee
	name string // name of the customer
	id   int    // id to show it is executed async

This is the Coffee-type. A Coffee has a kind, a name which customer ordered it and an id. The order function is rather simple, we check for the kind of the coffee and use a timeout to simulate different production times.

func order(coffee Coffee, coffeChannel chan Coffee) {
	switch coffee.kind {
	case "cappuccino":
		time.Sleep(1000 * time.Millisecond)
		coffeChannel <- coffee
	case "espresso":
		time.Sleep(2000 * time.Millisecond)
		coffeChannel <- coffee
	case "flat white":
		time.Sleep(3000 * time.Millisecond)
		coffeChannel <- coffee
	case "macchiato":
		time.Sleep(4000 * time.Millisecond)
		coffeChannel <- coffee

We are communicating via channels here. The function order gets a channel of type Coffee called coffeeChannel where it can send messages to. Think of it as something like a stream where you can send and receive messages on. We are only sending messages here. You can remember if you send or read from a channel by the position of the <-: channel <- message writes to the channel and someVar <- channel reads from the channel and assign the value to the variable.

Our main function looks like this:

func main() {
	coffeChannel := make(chan Coffee)

	go order(Coffee{"macchiato", "Roman", 1}, coffeChannel)
	go order(Coffee{"espresso", "Marcel", 2}, coffeChannel)
	go order(Coffee{"flat white", "Carsten", 3}, coffeChannel)
	go order(Coffee{"cappuccino", "Max", 4}, coffeChannel)

	for i := 0; i < 4; i++ {
		coffee := <-coffeChannel
		fmt.Println(fmt.Sprintf("%d: Coffee for %s ready", coffee.id, coffee.name))

We create a coffeeChannel and order different coffees. Note that we are using the **go** keyword to say that we want this to be executed concurrently. When we execute this the result looks like this:

$ go run coffee.go
4: Coffee for Max ready
2: Coffee for Marcel ready
3: Coffee for Carsten ready
1: Coffee for Roman ready

If we would have a program where the different functions wouldn’t take so much of a difference we would also get different result orders every time we run it.

Now, if we have a more complex application where we need to calculate lots of stuff we also would see that our processor will be used with as many as possible power.

Let’s use the fibonacci-loop function from dotnetperls:

func fibonacci(n int) {
	a := 0
	b := 1
	// Iterate until desired position in sequence.
	for i := 0; i < n; i++ {
		// Use temporary variable to swap values.
		temp := a
		a = b
		b = temp + a


If we execute that and monitor the CPU usage we can see one core goes up to 100% usage:


cpu usage on core

If we now use multiple goroutines:

go fibonacci(10000000000)
go fibonacci(10000000000)
go fibonacci(10000000000)
go fibonacci(10000000000)
go fibonacci(10000000000)
go fibonacci(10000000000)
go fibonacci(10000000000)
go fibonacci(10000000000)

fmt.Scanln() // to prevent the program from terminating

We can see a slightly higher CPU usage:

cpu usage multiple cores

There could much more be said about goroutines, but I will leave it at this now.

A Simple Web Server

Let’s implement a simple web server. At first, we create a function which gives us a port number, either via an environment variable or some predefined port

const DEFAULT_PORT = 3000
func getPort() string {
	port, err := strconv.Atoi(os.Getenv("PORT"))
	// strconv.Atoi("") == 0
	if port == 0 || err != nil {

 	return fmt.Sprintf(":%d", port)

This gives us an integer port we want our application to listen on.

Now our main function:

func main() {
	http.HandleFunc("/", rootHandler)
	log.Fatal(http.ListenAndServe(getPort(), nil))

We set a function to be called at access on the / endpoint, so our root endpoint, this function is called rootHandler and will be implemented now. After that, we say that we want to listen and server on our defined port. The function will block further execution and only returns an error if an unexpected error occurs. Now the rootHandler:

func rootHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello from our Webserver!\n")

We get two parameters in, the http.ResponseWriter and the http.Request. We write a string to our ResponseWriter. Now, if we start our program without environment variables and curl the url curl localhost:3000we get our expected result:

$ curl localhost:3000/
Hello from our Webserver!

How would we add another route? We only need to add another http.HandleFunc in our main function:

http.HandleFunc("/endpoint", endpointHandler)

Of course, we need to implement that handler, and we want to do a POST, GET and OPTIONS request and handle differently on each:

func endpointHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "GET":
		fmt.Fprintf(w, "This is a GET request")
	case "POST":
		fmt.Fprintf(w, "This is a OPTIONS request")
	case "OPTIONS":
		fmt.Fprintf(w, "This is a OPTIONS request")
		fmt.Fprintf(w, "This is an unsupported request method")

I think you can imagine that we can call other functions in these case statements which do some more logic like getting data from a database, inserting data, do other requests or anything else.

Logging web server events to a file

Let’s say we want to log everything that is an unsupported request method because we want to analyze how many hacking attempts or misusing of our API is happening. At first, we implement a new module logger.go with only one function Log:

func Log(msg string) bool {
	// file descriptor
	// os.O_APPEND|os.O_CREATE|os.O_WRONLY are flags to indicate what to do with that file
	// 0644 is filemode when its created
	f, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	// close the file descriptor at the end of the function
	defer f.Close()

	if err != nil {
		return false

	// write string to file
  	_, err = f.WriteString(fmt.Sprintf("%s\n", msg))

	if err != nil {
		return false

	return true

Here we are using some of our already acquired knowledge. We are using error handling and defer. The remaining parts are all out of the standard library and can be looked up there. We create a file log.txt if it’s not already there and if it’s there our data will be appended and we are writing a time string and a message into the file.

Now let’s call the the Log function inside our default branch:

// import the logger
import "github.com/mstruebing/go-examples/11_webserver/logger"
// ...
	fmt.Fprintf(w, "This is an unsupported request method")
	logger.Log(fmt.Sprintf("ERROR: Unsupported request made: %s", r.Method))

Now if we start the server either with go run or compiling it an run the binary and we make a DELETE request, for example, we can see the log.txt file being created and logging the falsy request method:

$ curl -X DELETE localhost:3000/endpoint
This is an unsupported request method%                                                                                                                          ➜ 11_webserver
$ cat log.txt
ERROR: Unsupported request made: DELETE
$ curl -X PUT localhost:3000/endpoint
This is an unsupported request method%                                                                                                                          ➜ 11_webserver
$ cat log.txt
ERROR: Unsupported request made: DELETE
ERROR: Unsupported request made: PUT

Cross Compiling

Cross-compiling works with environment variables. If you do go build go automatically uses your operating system and architecture to compile the binary. If you now want to compile your binary for a different system, say you have a Linux 64bit and want to compile it to run on a mac 64bit system you could do GOOS=darwin GOARCH=amd64 go build and go will produce a binary that can be run on a Mac but not on my machine. If you want to name the binary accordingly you could do something like: GOOS=darwin GOARCH=amd64 go build -o darwin-binary. You can see a complete list of support environment variables here: Installing Go from source - The Go Programming Language.

That’s all for now. If you have any questions, notes or anything else you can reach out to me on Twitter: @mxstrbng or open an issue on Github: go-examples.