This page looks best with JavaScript enabled

Go basics

 ·  🎃 kr0m

Go is the programming language developed by Google and released in 2009. Available for various Unix-like systems, including FreeBSD, Linux, Mac OS X, and Plan 9, as well as different architectures such as i386, amd64, and ARM.

The main advantages compared to other programming languages are:

  • Higher performance than Python, Ruby, or JavaScript.
  • Better support for concurrent work.
  • Go runtime is included in the binaries, making it highly distributable across systems.
  • Decentralized libraries; each programmer hosts their code wherever they prefer. To install the software, access to the respective platform is done directly.

- Installation
- Hello world
- Variables and auto-infer data types
- Variable scope
- Flow control
- Functions
- Arrays and slices
- Maps
- Additional resources


Installation:

pkg install go

Hello world:

vi 00-HelloWorld.go
package main

import "fmt"

func main() {
    fmt.Println("Hello world!")
}
go run 00-HelloWorld.go
Hello world!
go build 00-HelloWorld.go
file 00-HelloWorld
00-HelloWorld: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, for FreeBSD 12.3, FreeBSD-style, Go BuildID=Y4MQLyWJvSerAP4BVk2v/9P2KHgW0r9hY_WmEb9uy/zyzxSbQ1ETRoacqPL2Kq/-SzOfM95EQBcBz8mFnZr, with debug_info, not stripped
./00-HelloWorld
Hello world!

Variables and auto-infer data type:

vi 01-Variables.go
package main

import "fmt"

func main() {
    // Variable declaration
    var x int
    x = 5

    // Sugar syntax: Short declaration method, auto variable type detection
    y := 10
    // Only first value assigment requires ":"
    y = 15

    // Print variable values
    fmt.Println("x:", x)
    fmt.Println("y:", y)
}
go run 01-Variables.go
x: 5
y: 15

Variable scope:

The scope of variables in Go is very limited, meaning if a variable is created inside an if statement, it will only be accessible within that if. If it is created inside a for loop, it will only be available within the loop, and similarly, within any other type of control flow structure.

Let’s look at some examples:

vi 01b-Variables.go

package main

import "fmt"

func testFunction() error {
    return fmt.Errorf("Error")
}

func main() {
    i := 1
    if i > 0 {
        j := 2
        if j > 0 {
            j ++
        }
    }
    // i is in scope
    fmt.Println("i: ", i)
    // j is out of the scope
    // fmt.Println("j: ", j) would render an error

    if err := testFunction(); err != nil {
        fmt.Println("Error returned")
    }
    // err is out of the scope
    // fmt.Println("err: ", err) would render an error
}
go run 01b-Variables.go
i:  1
Error returned

Flow control:

The most basic control structure is the if.

vi 02-FlowControl.go

package main

import "fmt"

func main() {
    // Conditionals
    age := 25
    if age >= 18 {
        fmt.Println("You are of legal age")
    } else {
        fmt.Println("You are NOT of legal age")
    }

    // For loop
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }
}
go run 02-FlowControl.go
You are of legal age
0
1
2
3
4

One way to group if statements and get more readable code is to use a switch.

vi 02b-FlowControl.go

package main

import "fmt"

func checkOption(option string){
    switch option {
    case "A":
        fmt.Println("A")
    case "B":
        fmt.Println("B")
    default:
        fmt.Println("OTHER")
    }
}

func main() {
    checkOption("A")
    checkOption("B")
    checkOption("X")
}
go run 02b-FlowControl.go
A
B
OTHER

If we have a nested loop, we can use break from the second loop to exit the iteration of the first loop. To do this, we need to label the loop we want to break:

vi 02c-FlowControl.go
package main

import "fmt"

func main() {
    FirstLoop:
        for i := 0; i < 5; i++ {
            fmt.Println("i: ", i)
            for j := 0; j < 5; j++ {
                fmt.Println("j: ", j)
                if j == 0 {
                    break FirstLoop
                }
            }
        }
}
go run 02c-FlowControl.go
i:  0
j:  0

Functions:

In this example, we will see a function that adds two integers as long as they are less than or equal to 100. Additionally, we will use this function twice, once checking the return value for an error and a second time without checking it, assigning the returned value to the blank identifier which is used to assign return values that we want to ignore.

vi 03-Functions.go
package main

import "fmt"

// Function that adds two numbers
func add(a, b int) (int, error) {
    if (a > 100 || b > 100) {
        return 0, fmt.Errorf("Error, operators too big")
    }
    return a + b, nil
}

func main() {
    result, err := add(3, 4)
    if err != nil{
        fmt.Println("Error: ", err)
        return
    }
    fmt.Println("Result:", result)

    // Ignoring returned error
    result, _ = add(300, 400)
    fmt.Println("Result:", result)
}
go run 03-Functions.go
Result: 7
Result: 0

Arrays and slices:

The biggest difference between an array and a slice is that the array is of a fixed size while the slice is dynamic.
In fact, a slice consists of a pointer to an underlying array with a determined length and capacity (maximum length).

Because of this, when we work with slices, we are always working with pointers. This is evident in the following example:

vi 04-Arrays_Slices.go

package main

import "fmt"

func main() {
    // Sugar syntax array
    array := [5]string{"a", "b", "c", "d", "e"}
    fmt.Println("Array:", array)

    // Get array elements from 0 to N-1
    slice1 := array[0:3]
    fmt.Println("Slice1:", slice1)

    // Get array elements from 2 to N-1
    slice2 := array[2:5]
    fmt.Println("Slice2:", slice2)

    fmt.Println("---------")
    // Modify underlying array value:
    array[2] = "X"
    fmt.Println("Array:", array)
    fmt.Println("Slice1:", slice1)
    fmt.Println("Slice2:", slice2)
}
go run 04-Arrays_Slices.go
Array: [a b c d e]
Slice1: [a b c]
Slice2: [c d e]
---------
Array: [a b X d e]
Slice1: [a b X]
Slice2: [X d e]

We have said that slices are dynamic, so what does the “capacity” of a slice mean?
As slices grow in length, they migrate the data from an array of size X to another array of size 2X. The length is the number of elements stored in the slice and the capacity is the maximum size of the underlying array.

With an example, it will become clearer:

vi 04b-Arrays_Slices.go

package main

import "fmt"

func main() {
    // Create slice
    slice := []string{"a"}
    fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)

    slice = append(slice, "b")
    fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)

    slice = append(slice, "c")
    fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)

    slice = append(slice, "d")
    fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)

    slice = append(slice, "e")
    fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)
}
go run 04b-Arrays_Slices.go
Slice length: 1	 Slice capacity: 1	 Slice content: [a]
Slice length: 2	 Slice capacity: 2	 Slice content: [a b]
Slice length: 3	 Slice capacity: 4	 Slice content: [a b c]
Slice length: 4	 Slice capacity: 4	 Slice content: [a b c d]
Slice length: 5	 Slice capacity: 8	 Slice content: [a b c d e]

As we can see, the slice started from an array with a capacity of 1. When adding a second element, it migrated the existing content to another array with a capacity of 2X, in this case, 2*1:2.
In the next append, the array with a capacity of 2 is again too small, so it repeats the migration process to an array with a capacity of 2X: 2*2:4.
The next append does not require migration because we are adding a fourth element to an array with a capacity of 4.
In the next append, the array with a capacity of 4 is again too small, so it repeats the migration process to an array with a capacity of 2X: 4*2:8.

This is the process followed every time we add elements to an array, but this internal functioning of Go’s slice management can affect us as programmers since each migration changes the array.

Let’s see this example:

vi 04c-Arrays_Slices.go

package main

import "fmt"

func change(s []string) {
    s = append(s, "c")
    fmt.Println(s)
}

func main() {
    testSlice := []string{"a", "b"}
    change(testSlice)
    fmt.Println(testSlice)
}
go run 04c-Arrays_Slices.go
[a b c]
[a b]

Let’s not forget that with slices we are always working with pointers, so how is it possible that the slice has different values depending on the part of the code where it is consulted?

What happened here is that an array with len:2 and cap:2 was generated in the main function, the change function was called where an element was added to the slice, but since its capacity is 2 and the third element does not fit, it had to migrate the array. Since this migration occurred, the array pointed to by the slice in the main function is not the same as the one pointed to by the change function.

The simplest way to resolve this would be to return a value in the function and overwrite the value of testSlice in the main function:

vi 04d-Arrays_Slices.go

package main

import "fmt"

func change(s []string) []string {
    s = append(s, "c")
    fmt.Println(s)
    return s
}

func main() {
    testSlice := []string{"a", "b"}
    testSlice = change(testSlice)
    fmt.Println(testSlice)
}
go run 04d-Arrays_Slices.go
[a b c]
[a b c]

Another problem related to the pointer-based origin of slices is that if we perform append on a slice different from the original, we can encounter somewhat illogical results at first glance.

Let’s see this in an example:

vi 04f-Arrays_Slices.go

package main

import "fmt"

func main() {
    s1 := []int{1}
    s2 := append(s1, 2)
    s3 := append(s2, 3)
    s4 := append(s3, 4)
    fmt.Println(s1, s2, s3, s4)

    s4[0] = 55
    fmt.Println(s1, s2, s3, s4)
}
go run 04f-Arrays_Slices.go
[1] [1 2] [1 2 3] [1 2 3 4]
[1] [1 2] [55 2 3] [55 2 3 4]

But what happened? Simply, the underlying array shared by s3 and s4 is the same since cap(s3) was 4. Therefore, in the next append, it was not necessary to migrate the array. When modifying an element of one of the slices that shares an underlying array with another slice, it has affected both slices.

The best way to avoid incorrect results is always to overwrite the same slice when using append.


Maps:

Maps in Go are what other programming languages refer to as associative arrays, which are arrays with an index. This is very useful for accessing an element in an array without having to traverse its positions to find the desired element.

Let’s look at a simple example:

vi 05-Maps.go

package main

import "fmt"

func main() {
    // Sugar syntax: Map declaration
    grades := map[string]int{
        "John":    8,
        "Sarah":   6,
        "Bob":     9,
    }

    fmt.Println("John grade:", grades["John"])
    fmt.Println("Sarah grade:", grades["Sarah"])
    fmt.Println("Bob grade:", grades["Bob"])

    // Add map field
    grades["Mary"] = 7
    fmt.Println("Mary grade:", grades["Mary"])

    // Print the complete map
    fmt.Println("Grades:", grades)
}
go run 05-Maps.go
John grade: 8
Sarah grade: 6
Bob grade: 9
Mary grade: 7
Grades: map[Bob:9 John:8 Mary:7 Sarah:6]

When working with maps, keep in mind that accessing a nonexistent element will return the nil value of the type, meaning that in our example, which is a map[string]int, it would return 0.

vi 05b-Maps.go

package main

import "fmt"

func main() {
    // Sugar syntax: Map declaration
    grades := map[string]int{
        "John":    8,
        "Sarah":   6,
        "Bob":     9,
    }

    fmt.Println("John grade:", grades["John"])
    fmt.Println("Sarah grade:", grades["Sarah"])
    fmt.Println("Bob grade:", grades["Bob"])

    // Add map field
    grades["Mary"] = 7
    fmt.Println("Mary grade:", grades["Mary"])

    // Print the complete map
    fmt.Println("Grades:", grades)

    // Print the nonexistent map
    fmt.Println("Nonexistent grade:", grades["XX"])

}
go run 05b-Maps.go
John grade: 8
Sarah grade: 6
Bob grade: 9
Mary grade: 7
Grades: map[Bob:9 John:8 Mary:7 Sarah:6]
Nonexistent grade: 0

To distinguish whether the map is a grade with a value of 0 or a nonexistent index, you should obtain the values with a second parameter as follows:

if value, ok := grades["XX"]; ok {
    fmt.Println("Exists, value: ", value)
} else {
    fmt.Println("Key doesn’t exist")
}

Continuing with the previous example, the best approach is to create a function that performs this check, with the code as follows:

vi 05c-Maps.go

package main

import "fmt"

func getGrade(grades map[string]int, name string) {
    if value, ok := grades[name]; ok {
        fmt.Printf("%v grade: %v\n", name, value)
    } else {
        fmt.Printf("Incorrect student: %v\n", name)
    }
}

func main() {
    // Sugar syntax: Map declaration
    grades := map[string]int{
        "John":    8,
        "Sarah":   6,
        "Bob":     9,
    }

    getGrade(grades, "John")
    getGrade(grades, "Sarah")
    getGrade(grades, "Bob")
    getGrade(grades, "XX")
}
go run 05c-Maps.go
John grade: 8
Sarah grade: 6
Bob grade: 9
Incorrect student: XX

Maps, like slices , are just pointers, so if we make a “copy” we will actually be sharing the underlying data.

Let’s see this example:

vi 05d-Maps.go

package main

import "fmt"

func main() {
    // Sugar syntax: Map declaration
    grades := map[string]int{
        "John":    8,
        "Sarah":   6,
        "Bob":     9,
    }

    fmt.Printf("grades: %v\n", grades)
    grades2 := grades
    grades2["Laura"] = 3
    fmt.Println("----------------")
    fmt.Printf("grades: %v\n", grades)
    fmt.Printf("grades2: %v\n", grades2)
    delete(grades, "John")
    fmt.Println("----------------")
    fmt.Printf("grades: %v\n", grades)
    fmt.Printf("grades2: %v\n", grades2)
}
go run 05d-Maps.go
grades: map[Bob:9 John:8 Sarah:6]
----------------
grades: map[Bob:9 John:8 Laura:3 Sarah:6]
grades2: map[Bob:9 John:8 Laura:3 Sarah:6]
----------------
grades: map[Bob:9 Laura:3 Sarah:6]
grades2: map[Bob:9 Laura:3 Sarah:6]

If we want them to be independent, it is best to copy the elements one by one into another map, keeping in mind that if we create an empty map, we need to use Go’s make() function.

vi 05e-Maps.go
package main

import "fmt"

func main() {
    // Sugar syntax: Map declaration
    grades := map[string]int{
        "John":    8,
        "Sarah":   6,
        "Bob":     9,
    }

    grades2 := make(map[string]int)
    for name, mark := range grades {
        grades2[name] = mark
    }

    fmt.Printf("grades: %v\n", grades)
    fmt.Printf("grades2: %v\n", grades2)
    fmt.Println("----------------")
    grades2["Laura"] = 3
    fmt.Printf("grades: %v\n", grades)
    fmt.Printf("grades2: %v\n", grades2)
    fmt.Println("----------------")
    delete(grades, "John")
    fmt.Printf("grades: %v\n", grades)
    fmt.Printf("grades2: %v\n", grades2)
}
go run 05e-Maps.go
grades: map[Bob:9 John:8 Sarah:6]
grades2: map[Bob:9 John:8 Sarah:6]
----------------
grades: map[Bob:9 John:8 Sarah:6]
grades2: map[Bob:9 John:8 Laura:3 Sarah:6]
----------------
grades: map[Bob:9 Sarah:6]
grades2: map[Bob:9 John:8 Laura:3 Sarah:6]

Maps do not have to be limited to primitive types; we can also use complex structures, as in the following example.

vi 05f-Maps.go

package main

import (
    "fmt"
)

type currency struct {
    name   string
    symbol string
}

func main() {
    curUSD := currency{
        name:   "US Dollar",
        symbol: "$",
    }
    curGBP := currency{
        name:   "Pound Sterling",
        symbol: "£",
    }
    curEUR := currency{
        name:   "Euro",
        symbol: "€",
    }

    currencyCode := map[string]currency{
        "USD": curUSD,
        "GBP": curGBP,
        "EUR": curEUR,
    }

    for cyCode, cyInfo := range currencyCode {
        fmt.Printf("Currency Code: %s, Name: %s, Symbol: %s\n", cyCode, cyInfo.name, cyInfo.symbol)
    }
}
go run 05f-Maps.go
Currency Code: USD, Name: US Dollar, Symbol: $
Currency Code: GBP, Name: Pound Sterling, Symbol: £
Currency Code: EUR, Name: Euro, Symbol: €

Additional resources:

If you liked the article, you can treat me to a RedBull here