The Go programming language
Go is a systems programming language with great developer ergonomics. The core goal when designing Go was to have the efficiency of a statically-typed language with the ease of programming of a dynamically typed language. It’s main design traits are:
- Great developer ergonomics
- Easy to learn and write
- Efficient and latency-free garbage collection
- Performant like a systems programming language
- Type-safe and memory-safe
- Cocurrency as a first-class citizen
- Fast to compile
- Consistent across projects and teams due to the standardadized Go formatter
It was design to specifically address problems with writing system programming in C and C++ at the time:
- Slow compile times
- Hard to write concurrent code
- Hard to manage dependencies
- Bad developer ergonomics
- Badly designed type system gets in the way, makes programming harder, and it is too stringent (think classical inheritance in OOP). Rob pike calls this the type system tyranny and argues that with the type systems of the day you can either be fast, or productive but not both. This is an important problem that Go is trying to address.
All of it to tackle the fact that the software engineering landscape was very different that what it looked like when C and C++ were originally designed. Go offers a fresh look at solving the practice of modern software programming so that it is both fast during development and in production, and a fun and enjoyable pursuit on its own right.
This talk by Rob Pike (designer of Go with Robert Griesemer and Ken Thompson) is a bit old by it summarizes perfectly why Go exists, what it is trying to solve, and it illustrates it really well through actual code examples:
If you’re more curious about the history behind the Go programming language take a look at these podcasts:
- Creating the Go programming language at the Go time podcast
- The Go programming language on the Changelog podcast
Table of Contents
Learning Go
I think a really nice way to learn Go is to:
- Complete A Tour of Go. Make sure to complete all the exercises, that’s where you really solidify your understanding.
- Take a look at Effective Go, it has a look of great information about writing idiomatic Go (even though some parts are a bit outdated)
- Build something that scratch you own itch. Here are some examples
- Take a look at The Go programming language to understand why Go is the way it is and what the designers of the language where trying to solve when they designed Go.
- Solidify your knowledge with Go by example and learn more about web development in Go with Go Web by example.
Go Fundamentals
Image from go.dev getting started with Go
The following sections take a walkthrough through the fundamentals of the Go programming language. To make the most of it try experimenting yourself with the Better Go Playground (or your favorite editor).
Basic Anatomy of a Go File
// At the top of any go file we have a package name.
// Every Go program needs a package main.
// The main package contains a main function that acts as entry point to the Go
// program.
package main
// import statements to access additional libraries.
import "fmt"
// functions
// Every Go program needs a main function which represents the entry point of
// your program.
func main() {
// Calling a function from a package that we import
fmt.Println("Hello world")
}
Running a Go program
You can run a go program using the go run
command:
$ go run hello-world.go
Learn more about the available commands in a later section in this doc.
Comments
Go supports //
line comments and /* */
multi-line comments:
/*
This is a multiline comment in Go.
It is often used at the top of a package.
*/
// This is a single line comment in Go.
// It's the most common way to add comments in Go.
The godoc program can extract documentation for your Go code comments. It is recommended to have a comment at the top of every package (and only in one file for multi-file packages) and on top of your exported top-level declarations to provide rich documentation to consumers.
/*
My package that does this and that.
// this part because it is indented gets rendered in fixed-width font
// that's useful for sample code snippets and such
eat("banana")
*/
package mypackage
For more information and advice about comments take a look at Effective Go.
Types
The Go programming language has the following types:
- Integer (bunch of different integer types to optimize memory allocation
int
,int32
,int64
,uint
,uint32
,uint64
) - Float (
float32
,float64
) - String
- Boolean
- Functions
- Arrays
- Slices
- Maps
- Structs (custom type with properties)
- Interfaces (custom type that defines reusable behaviors)
- Channels
- Pointers
For more information about the types avialable take a look at the Go programming language specification
Variable declarations
// Variable declaration
var age int = 42
var name string = "Jaime"
var weight float64 = 100.4
// strings can be concatenated with +
var fullName string = "Jaime" + " Gonzalez" + " Garcia"
// Go has type inference so we can remove the types above
var age = 42
var name = "Jaime"
var weight = 100.4
// Create variable but don't initialize it just yet.
// It requires providing the type since we can't infer it.
// Having provided the type Go assigns a default value for that type.
// string => empty string
// int => 0
// float64 => 0.0
// etc
// This is called to zero-value a variable.
var name string
// Multiple variable declarations.
var him, her = "Jaime", "Malin"
// The "colon-equal" is a short-hand syntax to create a variable.
name := "Jaime"
// Type conversion
floatingNumber := float32(age)
Constants
Go constants declare constant values that cannot change once they’ve been defined.
package main
import fmt
// We can declare constants anywhere we can define variables
// For instance at the top-package level
const name = "Jaime"
func main() {
// Or within a function
// Numeric constant don't have a type unless it's given one
const speedOfLight = 300000000
// We can't change the value of a constant
name = "John"
}
Pointers
var name string
var pointer *string
fmt.Println("The variable name value: ", name)
// => The variable name value:
fmt.Println("The value of the pointer to the name variable: ", pointer)
// => The value of the pointer to the name variable: <nil>
name = "Antonio"
pointer = &name
fmt.Println("The variable name value: ", name)
// => The variable name value: Antonio
fmt.Println("The value of the pointer to the name variable: ", pointer)
// => The value of the pointer to the name variable: 0xc000096250
// It is a memory location where the variable 'name' is stored
fmt.Println("Dereferencing the pointer gets us the variable name value: ", *pointer)
// => Dereferencing the pointer gets us the variable name value: Antonio
Controls Structures
Go has support for most of the common control structures that you’d expect of a modern programming language and it removes some of them (while
, do-while
) which can be achieved with a simplified version of the other control structures. Worthy of note is that Go being concerned with saving you from typing removes the necessity of having parentheses around expressions in control structures (the braces are always required).
If
///////////////////
// if statements //
///////////////////
// A single if statement
if age > 21 {
fmt.Println("Congrats! You can finally drink legally! Wihoo!")
}
// If, else if, and else statements
if age > 21 {
fmt.Println("Congrats! You can finally drink legally! Wihoo!")
} else if age > 3 {
fmt.Println("Only juice for you")
} else if age >= 1 {
fmt.Println("Only milk for you")
} else {
fmt.Println("Only breast for you")
}
// Go has the special ability to have a statement before the conditional
// expression. The variable declared in this statement is available to all
// branches of the if but not outside. This is really useful when we want to
// perform some logic based on the result of a function:
if age := getAge(); age > 21 {
fmt.Println("Congrats! You can finally drink legally! Wihoo!")
} else if age > 3 {
fmt.Println("Only juice for you")
} else if age >= 1 {
fmt.Println("Only milk for you")
} else {
fmt.Println("Only breast for you")
}
// A very common pattern in Go is to obtain a value from a function and do
// something with that value when an error occurs. In other programming
// languages this may involve a two step process:
//
// #1. Execute task that may result in an error
err := executeTask()
// #2. Handle error
if err != nil {
fmt.Println(err.Error());
}
// In Go we can merge these two separate statements:
if err := executeTask(); err != nil {
// the err variable is scoped within the if block
// it doesn't exist outside
fmt.Println(err.Error());
}
Switch
///////////////////////
// switch statements //
///////////////////////
// Given a variable we can use a switch statement and execute branches of code
// based on whether the variable is equal to a specific value:
class := getPlayerClass()
switch class {
// You can use commas to have multiple expressions match the same case
case "warrior", "soldier":
fmt.Println("A martial warrior.")
case "cleric","priest":
fmt.Println("A pious cleric.")
case "paladin":
fmt.Println("A holy paladin.")
case "barbarian":
fmt.Println("A mighty barbarian.")
case "warlock":
fmt.Println("A powerful warlock.")
case "ranger":
fmt.Println("A dextrous ranger.")
default:
fmt.Println("A peasant.")
}
// Alternatively we don't need to specify a variable in the switch statement
// itself and instead we can defer to evaluate conditional predicates. This
// results in something very similar to a if/else statement but less wordy:
age := getUserAge()
switch {
case age > 21:
fmt.Println("Congrats! You can finally drink legally! Wihoo!")
case age > 3:
fmt.Println("Only juice for you")
case age >= 1:
fmt.Println("Only milk for you")
default:
fmt.Println("Only breast for you")
}
// when a case statement is true Go will execute the code in that case
// statement and then stop evaluating the rest of the cases in the switch
// statement. If we want to have Go evaluate other cases we can explicitly
// tell Go to do that using the 'fallthrough' keyword:
switch {
case age > 21:
fmt.Println("Congrats! You can finally drink legally! Wihoo!")
case age > 3:
fmt.Println("Only juice for you")
case age > 1:
fmt.Println("Only milk for you")
fallthrough
case page == 1
fmt.Println("That was close.")
default:
fmt.Println("Only breast for you")
}
// You can also use switch with types:
type Food struct {
name string
}
type Consumable interface {
eat()
}
func main() {
banana := Food{name: "banana"}
whatsMyType(banana)
// => some food: Food
}
func whatsMyType(i interface{}) {
// For some reason I can't quite understand this only works with interfaces
// e.g. I cannot call i directly on a struct
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
case Food:
fmt.Printf("some food: %T", v)
case Consumable:
fmt.Println("a consumable: %T", v)
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
For
The for
in Go is the only control structure for looping over a collection of elements. It is, however, very versatile. Go’s for loop is similar to, but not the same as, C’s. It unifies for and while and there is no do-while. There are three forms, only one of which has semicolons. The first one is the classical for loop:
///////////////////
// for loops //
///////////////////
// 1. Like a C for
for i := 0; i < 10; i++ {
fmt.Println(i)
}
The second form is equivalent to a while
loop:
// 2. Like a C while
i := 25
for i < 50 {
fmt.Println(i)
i = 51
}
The third form is an unbounded loop:
// 3. Like a C for(;;) or while (true)
for {
fmt.Println("Trapped for ever... or am I?")
// We use break to break off an otherwise infinite loop
break
}
Short declarations make it easy to declare the index variable in place:
sum := 0
for i := 0; i < 10; i++ {
sum += i
fmt.Println(sum)
}
We can use continue
to jump to the next iteration of the loop:
sum := 0
for i := 0; i < 10; i++ {
// We shan't like to add 5
if i == 5 {
continue
}
sum += i
fmt.Println(sum)
}
When looping over an array, string or map use the range
form like so:
// This looks like iterators in other languages
sentence := "I shall destroy you"
for index, letter := range sentence {
fmt.Printf("letter in position %v is %s\n", index, string(letter))
}
// We'll see more about maps in later sections...
for key, value := range oldMap {
newMap[key] = value
}
// If you only need the key, drop the value:
// We'll see more about maps in later sections...
for key := range m {
if key.expired() {
delete(m, key)
}
}
If you only need the value, use an underscore _
to discard the key (the
underscore is known as the blank identifier). This is necessary because
otherwise the Go compiler will throw an error because it doesn’t allow you to
declare variables that are not used:
sum := 0
for _, value := range array {
sum += value
}
Functions
// A simple function in Go
// func nameOfTheFunction(arg1 type) returnType {}
func add(a int, b int) int {
return a+b
}
// When a function declares multiple parameters with the same type
// we only need to provide the type once
func add(a, b int) int {
return a+b
}
// Go allows you to return multiple things in a very seamless way
func addAndMultiply(a int, b int) (int, int) {
return a+b, a*b
}
// You can also provide a name for the return values and assign values to those
// names instead of returning the values directly
func addAndMultiply(a int, b int) (sum int, mult int) {
sum = a+b
mult = a*b
return
}
// Go allows you to consume a function that returns multiple values
// with very slim syntax
sum, mult := addAndMultiply(2, 3)
// sum => 5
// mult => 6
// Go supports variadic functions (a function of indefinite arity - i.e. number of arguments)
// by using something similar to the rest/spread operators in JavaScript
func addMany(numbers ...int) (sum int) {
for _, number := range numbers {
sum += number
}
return
}
Go passes arguments by value. That means that when we use a value within the body of a function it has been copied from the arguments that were originally passed. Therefore, changing a value inside the body of a function doesn’t affect the original argument that was passed into the function:
func main () {
strength := 10
buffStrength(strength)
fmt.Println(strength) // => 10
}
func buffStrength(strength int) {
strength++
fmt.Println(strength) // => 11
}
This also applies to custom types:
func main() {
conan := models.Character{ID: 1, Name: "Conan", Class: "Barbarian", Hp: 500}
fmt.Println(conan.Hp) // => 500
heal(conan)
fmt.Println(conan.Hp) // => 500
}
func heal(character models.Character) {
character.Hp += 100
fmt.Println(character.Hp) // => 600
}
If we want to modify the actual variable that we pass in, we can explicitly pass arguments by reference using pointers:
func main() {
conan := models.Character{ID: 1, Name: "Conan", Class: "Barbarian", Hp: 500}
strength := 10
buffStrength(&strength)
fmt.Println(strength) // => 11
fmt.Println(conan.Hp) // => 500
heal(&conan)
fmt.Println(conan.Hp) // => 600
}
func buffStrength(strength *int) {
*strength++
fmt.Println(*strength) // => 11
}
func heal(character *models.Character) {
// noticed how with structs we don't need to dereference the pointer
// we can access its properties directly
character.Hp += 100
fmt.Println(character.Hp) // => 600
}
Defer
TODO
Data Structures in Go
Arrays
An array in Go is a numbered collection of elements that is arrayed (hehe) in sequence and that has a fixed length:
/////////////////
// Arrays //
/////////////////
// Arrays in Go have a fixed size
var numbers [5]int
// This creates an array of 5 integers with the default value
fmt.Println(numbers)
// => [0 0 0 0 0]
// One can read and set value using index notation
numbers[0] = 1
fmt.Println(numbers)
// => [1 0 0 0 0]
fmt.Printf("First element in the array is %v", numbers[0])
// => First element in the array is 1
// Alternatively one can initialize an array directly with some values:
numbers := [5]int{1, 2, 3, 4, 5}
// The size of the array can be inferred by the actual number of elements:
numbers := [...]int{1, 2, 3, 4, 5}
// The built-in len function allows us to get the length of an array
fmt.Println(len(numbers))
// => 5
// You can have multidimensional arrays like in other languages
var aSquareMatrix [3][3]int
// [ [0, 0, 0],
// [0, 0, 0],
// [0, 0, 0] ]
Slices (dynamically sized arrays) are far more common than arrays in Go.
Slices
A slice is a dynamically allocated array that can have any arbitrary size (or to be more accurate a dynamically-sized, flexible view into the elements of an array). They are the go-to data structured in Go when one wants to interact with collections of things.
We can create a slice by selecting a portion of an existing array using a range index operator:
// array[low:high]
numbers[1:2]
// This creates a half-open range where `low` is included, and `high` is not
For example:
// Given this array
numbers := [...]int {1, 2, 3, 4, 5}
// Get a portion of an array with range index operator
// this grabs from index 0 to index 2 non-inclusive
// (so the first two items in the array indexed 0 and 1)
someNumbers := numbers[0:2]
fmt.Println(someNumbers)
// => [1, 2]
// When slicing one may omit the low and/or high bounds. It works as one would
// expect:
//
// - [:10] means from [0:10]
// - [4:] means from [4:len(array)]
// - [:] means from [0:len(array)]
Slices don’t store data, they are just views of an underlying array. Changing the elements of a slice also modifies the elements of the underlying array and vice versa. Likewise, any other slices that share the same underlying array will also be affected by these changes.
// someNumbers still points to the original array.
// So if we change a value
someNumbers[0] = 99
// it also affects the original array
// numbers[0] => 99
Go provides the built-in fuctions that help us make sense of the length of a slice and its underlying array:
len
gives you the length of a slice (the number of elements in the slice)cap
gives you the size of the underlying array
// We can use len and cap to find out how the memory for a slice is allocated
len(someNumbers) // => 2 (items in the slice)
cap(someNumbers) // => 5 (size of underlying array)
// If we use len and cap on an actual array we can see how they are the same
len(numbers) // => 5
cap(numbers) // => 5
We can also create a slice directly using slice literals (much like array literals but where we don’t specify the length). This also creates an array that underlies the slice we get a reference to:
numbersSlice := []int{1, 2, 3, 4, 5}
fmt.Println(numbersSlice)
// => [1 2 3 4 5]
personsSlice := []struct{
name string;
age int;
}{
{"Jaime" 38},
{"Teo" 4},
}
fmt.Println(numbersSlice)
// => [{Jaime 38} {Teo 4}]
Alternatively we can create a slice using the built-in make
function:
// We can also create an slice using the built-in *make* function
// (make lets you allocate memory for a slice, map or channel)
// This is how you define dynamically sized arrays in Go
numbers := make(int[], /* current size */ 5, /* max size */ 100)
fmt.Println(numbers)
// => [0 0 0 0 0]
The zero value of a slice is nil
:
// The zero value of a slice is nil
var emptySlice []int
// When you print it, it appears as an empty slice
fmt.Println(emptySlice)
// => []
// It's lenght and capacity are 0
fmt.Println("length: %v, capacity: %v", len(emptySlice), cap(emptySlice))
// => length: 0, capacity: 0
// Its value is equivalent to nil
if emptySlice == nil {
fmt.Println("empty slice is nil")
}
// => empty slice is nil
Just like with arrays, we can have slices of slices (i.e. multidimensional slices):
squareMatrix := [][]string{
[]string{0, 0, 0},
[]string{0, 0, 0},
[]string{0, 0, 0}
}
We can add items to a slice by using the append
built-in function:
// Original array
numbers := [...]int {1, 2, 3, 4, 5}
// Slice that offers a view into that array
someNumbers := numbers[0:2]
// Add (append) an item to the slice. It returns the updated slice
someMoreNumbers := append(someNumbers, 42, 24)
// =>now someMoreNumbers contains [99, 2, 42, 24]
This is all fine but we need to understand that we’re using the same space in memory (the same underlying array) all along, so the current state of the world is:
// numbers => [99, 2, 42, 24, 5]
// someNumbers => [99, 2]
// someMoreNumbers => [99, 2, 42, 24]
If you’re coming from another programming language this is kind of unexpected right? One might incorrectly assume that we’re cloning the original array, but we’re not. We’re creating a new slice that provides a larger view over the original underlying array.
So what happens if we try to add a new element to the slice beyond the size of the original array?
// What happens if we try to add a new element to the slice?
evenMoreNumbers = append(someMoreNumbers, 5000, 6000, 7000)
// Now we have the following arrays/slices:
// numbers => [99, 2, 42, 24, 5] (original array)
// someNumbers => [99, 2]
// someMoreNumbers => [99, 2, 42, 24]
// evenMoreNumbers => [99, 2, 42, 24, 5000, 6000, 7000] (the underlying array is a new array)
So we only allocate a new array if the slice is extended beyond the size of the original underlying array. If we never interact with the original array then it’s fine. But this feels like a potential way to shoot oneself in the foot.
In summary, when calling append
to add an item to a slice we get a new slice with the
added item. If the underlying array has enough capacity to hold the new size of
the slice we’ll use it to store the new items, otherwise a new array will be
allocated to store the new slice.
// We can use len and cap to find out how the memory for a slice is allocated
len(someNumbers) // => 2
cap(someNumbers) // => 5 (size of the underlying array)
// This tells us that the someNumbers slice has currently two items
// but its capacity is 5, that means that it has allocated space for 5 items
// this includes the items in the original array.
// When we append items beyond the slice capacity, new memory is allocated:
len(evenMoreNumbers) // => 6
cap(evenMoreNumbers) // => 10
// New memory that isn't shared with the original array
// As a result of appending a new array has been allocated that can fit the new slice
// numbers => [99, 2, 42, 24, 5]
// evenMoreNumbers => [99, 2, 42, 24, 5000, 6000, 7000)
// that's why any element after the first 4 ones aren't overwritten
For more information about append take a look at the Go documentation.
You can iterate over the elements of a slice using a range
form of the for
loop:
for i, v := range someNumbers {
fmt.Printf("value at index %v is %v\n", i, v)
}
// If you don't need to use the index or the values you can omit them using the _
// for i, _ := range someNumbers
// for _, v := range someNumbers
// for i := range someNumbers
For more information about slices read Go slices: usages and internals.
Maps
///////////////////
// Maps //
///////////////////
// Like in many other languages maps are collections of key/value pairs
// that can be accessed via the key with constant complexity
// The most straightforward way to create a map is using a map initializer:
users := map[int]string {
321312: "[email protected]",
321314: "[email protected]",
}
// A users map with integer keys and string values
// map[int]string => map with integer keys and string values
fmt.Println(users)
// => map[321312:"[email protected]", 321314:"[email protected]"]
// We can also create a map using make:
users := make(map[int]string)
// And add key/value pairs to the map using the index operator
// like so:
users[321312] = "[email protected]"
users[321314] = "[email protected]"
fmt.Println(users)
// => map[321312:"[email protected]", 321314:"[email protected]"]
// Check whether an element exists in the map
userEmail, ok := users[12313123]
// when user is not in the map: ok => false
// when user is in the map: ok => true
// in this case ok would be false since our map only has two users
// with different ids that the one provided
// We can use this with the idiomatic go if statement:
if userEmail, ok := users[666] ok {
fmt.Println("Found user!")
// do something
} else {
fmt.Println("Didn't find user!")
// do something else
}
// Remove key/value pair
delete(users, 321314)
// goodbye Thor!
Custom Types
Structs
type Character struct {
name string
class string
hp int
}
// Alternatively we can use the consolidated syntax
type Character struct {
hp int
name, class string
}
// Capitalized fields are visible outside of the package where the struct is
// defined. Lowercase fields are only visible within the same package where the
// struct is defined.
// A Character in a game
type Character struct {
Name string
Class string
Hp int
}
// To create a new instance of a struct type we can use a struct initializer:
conan := Character {Name: "Conan", Class: "Barbarian", Hp: 500}
fmt.Println(conan)
// => {1 Conan Barbarian 500}
// Once defined we can access the properties of a struct using dot notation
fmt.Printf("Hither came %v", conan.Name)
// => Hither came Conan
// We can also use it in other struct types:
// A scene in a game
type Room struct {
Name string
Characters []Character
Description string
}
// And refer to these custom types in functions:
// Describes a room
func DescribeRoom(room Room) {
fmt.Println(room.Name)
fmt.Println(room.Description)
if numberOfCharacters := len(room.Characters); numberOfCharacters == 1 {
character := room.Characters[0]
fmt.Printf("%s is in the room\n", character.Name)
} else if numberOfCharacters > 1 {
fmt.Printf("There are %d in the room.", numberOfCharacters)
}
}
// Probably better to return a string and use a separate component to decide
// whether to send the string to stdout or other type of interface (like a web
// service response in a web game)
room := models.Room{Name: "Tavern", Description: "A dirty and noisy tavern", Characters: []models.Character{conan}}
models.DescribeRoom(room)
// => Tavern
// A dirty and noisy tavern
// Conan is in the room
// Alternatively we can use methods and be able to use dot notation on structs:
func (room Room) Describe() {
fmt.Println(room.Name)
fmt.Println(room.Description)
if numberOfCharacters := len(room.Characters); numberOfCharacters == 1 {
character := room.Characters[0]
fmt.Printf("%s is in the room\n", character.Name)
} else if numberOfCharacters > 1 {
fmt.Printf("There are %d in the room.", numberOfCharacters)
}
}
// Now we can use dot notation to call this method as if it were
// a part of the Room custom type:
room.Describe()
// => Tavern
// A dirty and noisy tavern
// Conan is in the room
Interfaces
Where struct
defines a collection of data that represent and encapsulate a custom type, interface
represents a collection of behaviors also tied to a custom type. A struct
and the methods that we defined that can operate on that struct together can implement an interface
if we have all methods that fulfill that given interface
. Go uses structural typing (like JavaScript/TypeScript) which means that a given type doesn’t need to explicitly state that it implements a given interface. By having the matching methods to conform to the shape of an interface a struct automatically can be used as something that supports that interface. If it walks like a duck, cuacks like a duck and swims like a duck, then it is a duck.
Give these structs that represent characters in a game:
type Warrior struct {
HitPoints int;
}
func (w *Warrior) takeDamage(damage int) {}
func (w *Warrior) attack(target *Target) {}
type Monster struct {
HitPoints int;
}
func (w *Monster) takeDamage(damage int) {}
func (w *Monster) attack(target *Target) {}
type Wizard struct {
HitPoints int;
}
func (w *Wizard) takeDamage(damage int) {}
func (w *Wizard) attack(target *Target) {}
func (w *Wizard) castSpell(spell Spell, target *Target) {}
And these interfaces:
type Target interface {
takeDamage(damage int)
}
type Attacker interface {
attack(target *Target)
}
type SpellCaster interface {
castSpell(spell Spell, target *Target)
}
Then Warrior
, Monster
and Wizard
can be used as Target
since they all have the takeDamage
method, likewise they are all Attacker
because they implement the attack
method, but only Wizard
s are SpellCaster
because they are the only struct that can be used with the castSpell
method.
When in other languages one would have a class or interface that would include both properties and methods, Go breaks these down into two separate programmatic structures: struct
for data (a separate collection of methods that can be applied to the struct) and interface
for behaviors:
// Behaviors of a geometrical figure
type geometricalFigure interface {
area() float64
perimeter() float64
}
// A rectangle
type rectangle struct {
width, height float64
}
// A circle
type circle struct {
radius float64
}
Go has an empty interface type that can be used to hold any type of value.
things := map[int]interface {
1: 22,
2: "banana",
3: conan,
}
Error Handling in Go
Error handling in Go is somewhat different from what we see in other languages, instead of relying on exceptions, it takes advantage of the fact that functions in Go can return multiple things to establish the error handling pattern of having functions optinally return a detailed error in addition to an actual value. For example, os.Open
doesn’t just return a nil pointer on failure, it also returns an error value that describes what went wrong.
All errors in Go conform with the error interface:
// All errors in Go conform with the error interface
// nil can be assigned to the type error
type error interface {
Error() string
}
Library writers can enhance this interface with additional information that makes sense and provides context in whichever error they need to create in relation to their domain model.
// MagicError records an error and the spell that caused it
type PathError struct {
Spell string // "fireball", "ice cone", "hammer of the gods", etc.
Err error // Actual error
manaAmount int // mana used to cast spell
}
A good example of error handling implementation comes from the os
package and the os.Open
function that allows us to open files. When we try to open a file with os.Open
, in addition to returning a *os.File
value, os.Open
also return an error value. If the file is opened successfully the error value will be nil
, but if not it’ll return a os.PathError
with additional context about the error that ocurred while opening the file:
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}
// open /src/mai.go: no such file or directory
// When defining errors it is really helpful to provide adequate context for
// easier troubleshooting. Providing the operation on the file, and the file itself
// give the reader a lot more helpful information that just "no such file or directory"
// TODO: Continue expanding about error handling, and specially around the uses of // panic and recover
// We can import error related functions from the errors package
// https://pkg.go.dev/errors
import "errors"
// And use the function New to create new errors
func castDangerousSpell(mana int) (err error, manaSpent int) {
if mana > 5 {
err = errors.New("Boooom!")
} else {
manaSpent = mana
fmt.Println("You obliterate all thy enemies!")
}
return
}
function main() {
err, manaSpent := castDangerousSpell(1)
fmt.Println(err) // => nil
fmt.Println(manaSpent) // => 5
err, manaSpent = castDangerousSpell(20)
fmt.Println(err) // => Booom!
fmt.Println(manaSpent) // => 0
}
// We should handle this error
function main() {
if err, manaSpent := castDangerousSpell(200); err != nil {
fmt.Println(fmt.Errorf("oh no, something bad happened: %v", err))
fmt.Println("Heal heal heal")
// We can also log the error using the log package
// https://pkg.go.dev/log
log.Fatalln(err)
} else {
fmt.Printf("We spent some mana %d\n", manaSpent)
}
}
When something unexpected happens at runtime that prevents the continued execution of your program Go has the concept of panic.
// One can explicitly use panic to automatically end the execution of the program
// when something bad happens
panic()
Concurrency
The main mechanism to work with concurrency in Go are goroutines. A goroutine is a lightweight thread managed by the Go runtime. To execute a piece of code in a goroutine you wrap that code into a function and call it prefixed with the go
keyword:
func add(a int, b int) {
fmt.Printf("%d + %d = %d\n",a,b, a + b)
}
go add(1, 2)
The evaluation of add(1,2)
happens in the current goroutine whereas the execution of the actual function happens in a separate goroutine. In order, to be able to communicate (send and receive values) between goroutines one relies on channels.
A channel is a typed conduit over which you can send and receive values between goroutines. You instantiate it using make
like you would a map or slice:
channel := make(chan int)
And now you can use it to send and receive values:
// The values flow in the direction of the arrow:
// Send value to channel
ch <- value
// Receive value from channel and assign it to variable `value`
value := <-ch
Using channels we can receive the result of the addition in the previous example:
func add(a int, b int) {
fmt.Printf("%d + %d = %d\n",a,b, a + b)
}
func main() {
c := make(chan int)
go add(1, 2, c)
value := <-c
fmt.Println(value)
// => 3
}
By default, sending and receiving data block until the other side is ready which allows goroutines to synchronize with each other without the need to use locks explicitly.
This example from a Tour of Go distributes summing a collection of elements between two separate go routines:
// Example from A Tour of Go
// https://go.dev/tour/concurrency/2
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
Channels can be buffered so that the channel only blocks when the buffer is full. For example:
// Example from a Tour of Go exanded a bit
// https://go.dev/tour/concurrency/3
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
// This would cause a deadlock
// because it would block the goroutine
// until someone reads the data
// but there's no other goroutine in this program
// ch <- 3
// (deadlock -> https://en.wikipedia.org/wiki/Deadlock)
fmt.Println(<-ch)
fmt.Println(<-ch)
// This is OK
ch <- 3
fmt.Println(<-ch)
// This would block the goroutine until someone
// sends more data. Since there's no other goroutine
// in this program it would cause a deadlock.
fmt.Println(<-ch)
}
A sender goroutine can close a channel to indicate consumers that no more values will be sent. Consumers (or receivers) can find out whether a channel is closed by assigning a second parameter to the receive expression:
// ok is false when the channel is closed
// and therefore there aren't more values to be received
value, ok := <-ch
You can use the range
form of the for loop on a channel to receive values until the channel is closed:
// Example from a Tour of Go:
// https://go.dev/tour/concurrency/4
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
Useful packages
- fmt. Format I/O
- reflect. Use run-time reflection to understand and/or manipulate types and shapes of different programming constructs at runtime.
- errors. Includes functions to manipulate errors.
- log. Provides logging functionality for your program.
- net/http. Provides functionality to implement an HTTP client and server.
For more packages check the go packages docs.
fmt
The fmt lets you format I/O.
Print
lets you print some text into stdout:
// Print some text
fmt.Print("hello world\n")
// Print some text in a line (automatically adds end of line)
fmt.Println("hello world")
// Formatting with string interpolation
// - %s stands for string
// - %t renders true/false for booleans
// - %d a base 10 integer (decimal)
// - %v renders the value in the default format. That's probably what you
// want 90% of the time.
fmt.Printf("hello world, my name is %s", "Jaime")
FPrint
prints some text into an io.Writer
interface, this allows you to print to stdout or other outputs like files or an HTTP response. SPrint
formats a string and returns the actual string (The S
probably stands for string).
Reflect
The reflect package lets you use run-time reflection to understand and/or manipulate types and shapes of different programming constructs at runtime.
fmt.Println(reflect.TypeOf("WaaaT")) // => string
age := 42
fmt.Println(reflect.TypeOf(x)) // => int
Errors
The errors package implements functions to manipulate errors.
// Create a new error
err := errors.New("Something really bad just happened")
// Unwrap errors
wrappedError := errors.Unwrap(error)
fmt.Println(error.Error()) // => the wrapping error
fmt.Println(wrapped.Error()) // => the wrapped error
Log
The log package provides logging functionality to your program. It defines a type, Logger, with methods for formatting output. It also has a predefined ‘standard’ Logger accessible through helper functions Print[f|ln]
, Fatal[f|ln]
, and Panic[f|ln]
, which are easier to use than creating a Logger manually. That logger writes to standard error and prints the date and time of each logged message.
log.Fatalln(error) // => logs error
net/http
The net/http package provides an HTTP server and client implementations. This, together with the html/template package data driven templates gives you all the functionality you need to create your own web server:
package main
import (
"fmt"
"html/template"
"log"
"net/http"
"strconv"
)
func main() {
fmt.Println("My own web server")
// Here you can specify how different routes match to route handlers
// Each handler is a function that will be executed whenever a user navigates
// to a given route. (i.e. when a user makes an HTTP request to that route)
http.HandleFunc("/", home)
http.HandleFunc("/ingredients/", getIngredients)
// alternatively, one could create a more RESTful approach and use same
// handler but have a different logic based on the HTTP method of the request
// e.g. GET -> get ingredients
// e.g. POST -> add new ingredient
// e.g. PUT -> update ingredient
// etc
http.HandleFunc("/add-ingredient/", addIngredient)
fmt.Println("Server is running on port 8080...")
fmt.Println("Listening for HTTP requests...")
// Only log an error if an error occurs while setting up the webserver to listen
log.Fatalln(http.ListenAndServe(":8080", nil))
}
// This function is a route handler. It has two params:
// http.ResponseWriter => a writer that we'll use to write a HTTP response to
// http.Request => object that represents the HTTP request sent by the client
func home(w http.ResponseWriter, r *http.Request) {
fmt.Println("Serving home route: /")
}
type Ingredient struct {
Quantity int64
Name string
}
var ingredients []Ingredient
type IngredientsPageData struct {
Ingredients []Ingredient
}
// GET ingredients
func getIngredients(w http.ResponseWriter, r *http.Request) {
fmt.Println("Serving ingredients route: /ingredients/")
// This uses Go templates to generate HTML based on some data
if t, err := template.ParseFiles("ingredients.html"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println("There was an error parsing the template:", err)
} else {
pageData := IngredientsPageData{
Ingredients: ingredients,
}
t.Execute(w, pageData)
}
}
// POST ingredients
func addIngredient(w http.ResponseWriter, r *http.Request) {
fmt.Println("Adding a new ingredient")
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Println("Error when parsing form from request:", err.Error())
}
quantity, err := strconv.ParseInt(r.FormValue("quantity"), 10, 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Println("Error when parsing form from request:", err.Error())
}
newIngredient := Ingredient{
Name: r.FormValue("name"),
Quantity: quantity,
}
ingredients = append(ingredients, newIngredient)
http.Redirect(w, r, "/ingredients/", http.StatusSeeOther)
}
For more information about Go templates take a look at the html/package documentation and Go Web by example
You can also use the net/http package to implement a HTTP client, that is, to consume other APIs from your Go program. For example:
package main
import (
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
)
func main() {
fmt.Println("Star wars server")
// Here you can specify how different routes match to route handlers
// Each handler is a function that will be executed whenever a user navigates to a given route
// (i.e. when a user makes an HTTP request to that route)
http.HandleFunc("/people/", getPeople)
fmt.Println("Server is running on port 8080...")
fmt.Println("Listening for HTTP requests...")
// Only log an error if an error occurs while setting up the webserver to listen
log.Fatalln(http.ListenAndServe(":8080", nil))
}
// Our view model
// The part of our business model that we'll render in a web page
type PeoplePageData struct {
People []Person
}
type PeopleResponse struct {
People []Person `json:"results"`
}
// Our domain model
type Planet struct {
Name string `json:"name"`
}
type Person struct {
Name string `json:"name"`
HomeworldURL string `json:"homeworld"`
Homeworld Planet
}
const BaseURL = "https://swapi.dev/api/"
func getPeople(w http.ResponseWriter, r *http.Request) {
fmt.Println("Serving people route: /people/")
var people PeopleResponse
if response, err := http.Get(BaseURL + "people"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println("Error when requesting people from swapi:", err)
} else {
fmt.Println("Response:", response)
if data, err := ioutil.ReadAll(response.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println("Error parsing swapi response:", err)
} else {
fmt.Println("Parsed data:", string(data))
if err := json.Unmarshal(data, &people); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println("Error parsing swapi response as JSON:", err)
}
for _, person := range people.People {
if homeworld, err := getHomeworld(person); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println("Error retrieving persons homeworld:", err)
} else {
person.Homeworld = *homeworld
}
}
}
}
if t, err := template.ParseFiles("people.html"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println("There was an error parsing the template:", err)
} else {
pageData := PeoplePageData{
People: people.People,
}
t.Execute(w, pageData)
}
}
func getHomeworld(person Person) (*Planet, error) {
var planet Planet
if response, err := http.Get(person.HomeworldURL); err != nil {
log.Println("Error when requesting planet from swapi:", err)
return nil, err
} else {
fmt.Println("Response planet:", response)
if data, err := ioutil.ReadAll(response.Body); err != nil {
log.Println("Error parsing swapi planet response:", err)
return nil, err
} else {
fmt.Println("Parsed planet data:", string(data))
if err := json.Unmarshal(data, &planet); err != nil {
log.Println("Error parsing swapi planet response as JSON:", err)
return nil, err
} else {
return &planet, nil
}
}
}
}
Useful Go Commands
# Get help
$ go help
$ go help {command}
# Build binary so you can run your Go program as an executable
$ go build
# Run a Go program (this builds the program behind the scenes)
$ go run main.go
# Get Go documentation
$ go doc fmt # get info about fmt package
$ go doc fmt.Printf # get info bout fmt.Printf function
# Format your code (this is something you'll want to integrate with
# your editor so that it happens on save)
# https://pkg.go.dev/cmd/gofmt
$ go fmt my-program.go
# Examine Go source code and report suspicious constructs
# https://pkg.go.dev/cmd/vet
$ go vet my-program.go
# List Go packages
# https://pkg.go.dev/cmd/go/internal/list
$ go list
# Install third party library from the Go package registry
$ go get golang.org/x/lint/golint
# Install referenced third party libraries
$ go install
# Run Go tests
# Go tests are files that match the following naming convention *_test.go
$ go test
For a complete list of commands see the Go command documentation and the Go command reference
Code organization in Go
TODO
Workspaces
Modules
Packages
package utils
// In Go you capitalize the names of methods to indicate that these are
// functions that are exported from this package. According to golint rules
// any exported function needs to be commented.
// Adds together multiple numbers
func Add(nums ...int) (sum int) {
for _,num := range nums {
sum += num
}
return
}
// Multiplies multiple numbers together
func Multiply(numbs ...int) int {
result := 1
for _,num := range nums {
result *= num
}
return result
}
package main
import (
// to import a package use its full name:
// name-of-module/package
"example.com/my-module/utils"
// You can also use an alias if the package name is very long:
// u "example.com/my-module/utils"
"fmt"
)
func main() {
sum := utils.Add(1, 2)
fmt.Println(sum) // => 3
}
Testing in Go
Go provides a testing package in its standard library to help you with automated testing and a built-in command go test
to run your tests.
// Filename: math_test.go
package utils
import "testing"
func TestAdd(t *testing.T) {
expected := 2
actual := Add(1, 1)
if actual != expected {
t.Errorf("Sum is not correct! Expected %v but was %v", expected, actual)
}
}
Using Go in VSCode
To setup VSCode to work with Go install the Go for Visual Studio Code extension which is part of the Go project. That’ll give you support for IntelliSense, code navigation and code editing (snippets, renaming, refactoring, etc).
Take a look at the Go for VSCode docs for more information on how to configure the extension and all the features supported.
Using Go in Vim
- Install go in your system (e.g.
brew install go
) - Install vim-go and run
:GoInstallBinaries
- If you use coc.nvim you’ll want to install the coc-go extension. In the current version there seems to be some conflict between
coc-go
andgopls
(the go LSP server). You can circumvent it by installing goplsbrew install gopls
and setting thego.goplsPath
via:CocConfig
{
"go.goplsPath": "gopls"
}
Where to go from here?
TODO
Additional Resources
- Go.dev website
- Go documentation
- Go playgrounds
- Official Go playground
- Better Go playground with syntax highlighting, completion and more Go examples
- Go blog
- Get started with Go
- Get help with Go
- Go commands reference
- The Go programming language specification
- Good starters
- A tour of Go is a great interactive way to learn Go. Remember to switch on the syntax highlighting that for some reason is turned off by default.
- How to write Go code
- Effective Go. This is a great document that guides you towars writing clear, idiomatic Go code. This is an awesome starting point for programmers that are already comfortable with other programming languages and are now learning Go.
- Learn Go in Y minutes. A great way to get a high level overview of the syntax and functionality of a language.
- Books
- Head First Go
- Learning Go: An Idiomatic Approach to Real-World Go Programming
- The Go programming language
- Go by example (freely available online book)
- Go Web by example (freely available online book)
- Courses
- Podcasts
- Articles
- The go blog has lots of interesting and helpful articles about Go
- How to use interfaces in Go
- Go code review comments
Interesting open source projects in Go
- Revive. Fast, configurable, extensible, flexible, and beautiful linter for Go.
JavaScript vs Go
If you’re coming from JavaScript and interested in learning Go these are some nice pointers to compare different features of both languages:
- Dynamic typing vs static strong typing
- ES6 classes vs structs, pointers, methods, interfaces
- Exceptions vs errors as values that need to be explictly handled
- Single-threaded (callbacks, promises, async/await) vs multi-threated (concurrency, goroutines, sync)
- Non opinionated vs very opinionated (gofmt, one well defined set of conventions on how to write idiomatic Go code and specific formating shared across the whole ecosystem)
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.