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:
Hello world:
package main
import "fmt"
func main() {
fmt.Println("Hello world!")
}
Hello world!
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
Hello world!
Variables and auto-infer data type:
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)
}
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:
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
}
i: 1
Error returned
Flow control:
The most basic control structure is the if
.
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)
}
}
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
.
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")
}
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:
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
}
}
}
}
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.
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)
}
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:
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)
}
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:
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)
}
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:
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)
}
[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:
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)
}
[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:
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)
}
[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:
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)
}
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.
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"])
}
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:
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")
}
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:
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)
}
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.
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)
}
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.
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)
}
}
Currency Code: USD, Name: US Dollar, Symbol: $
Currency Code: GBP, Name: Pound Sterling, Symbol: £
Currency Code: EUR, Name: Euro, Symbol: €