Posts List
moustapha.dev : A tour of Go blog post cover image

A tour of Go

What is Go

Go is a high-level general purpose programming language that is statically typed and compiled. It is known for the simplicity of its syntax and the efficiency of development that it enables by the inclusion of a large standard library supplying many needs for common projects […] Go was designed at Google in 2007 to improve programming productivity in an era of multicore, networked machines and large codebases.

Wikipedia

As a web developer, I'm fluent with Typescript, PHP, Ruby and some other languages. Recently, I have been looking for performance and more control.

Go is a garbage collected language and it's pretty high level. After writing some small programms, I felt in love with Go's simplicity, the pover it's standard library and how good the tooling is. Go is powering many large projects like Docker, Kubernetes, Terraform, Prometheus, and many more. It's a great language for building web servers, command line tools, and distributed systems. It has a strong focus on concurrency and parallelism, which makes it a good choice for building scalable applications.

In this post we'll be exploring the basics of Go including data types, functions, error handling, control flow etc. If you need more details, you can check the docs and the tour. Go by example is also a good resource for quick concept discovery.

Markdown RSC

Go's mascott, Gopher

Let's Go!

Initialize a project, package, module

You can download go for your OS first then create a directory and initialize a project. Once you have Go installed, you can create a directory for your project and initialize a Go module. A Go module is a collection of Go packages that are versioned together. A package is a collection of Go source files in the same directory. The go mod init command creates a new module in the current directory.

mkdir go-tour
 
cd go-tour
 
go mod init yourid/go-tour

This will create a go.mod file in the current directory, which contains the module name and the Go version used in the project. Later, you can add dependencies to this module using the go get command, which will automatically update the go.mod file with the new dependencies.

Now add a file named main.go in the project directory. This file is part of the main package and contains the main function, which is the entry point of the program.

 
package main
 
import "fmt"
 
func main() {
    fmt.Println("Let's Go!")
}

Public and private members

In Go, you can define public and private members using the first letter of the identifier. If the first letter is uppercase, the member is exported (public). Otherwise, the member is unexported (private). When we talk about members, we mean variables, constants, functions, types and methods and that visibility is scoped to the package. So if you want to use a member from another package, it must be exported.

Comments

You comment using // one line or /**/ for multiline. When you put a comment immediately before top-level package, const, func, type, and var declarations with no intervening newlines, it's a doc comment and can be extracted later. Doc comments are picked by the IDE's LSP when using the exported members.

fmt function definition doc comment

Data type, variable, constant and pointer

Create a variable using the var keyword, followed by the variable name and it's type. So Go is statically typed, which means the type of a variable is known at compile time. The basic types are

bool
 
string
 
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr
 
byte // alias for uint8
 
rune // alias for int32
     // represents a Unicode code point
 
float32 float64
 
complex64 complex128

The int8, int16 etc are int variants representing the allocation size. I'm not going to go deeper here. But generally speaking,

When you need an integer value you should use int unless you have a specific reason to use a sized or unsigned integer type

var age int
var name string
 
name = "Foobar"

When you declare a variable without explicitly assigning it a value, it has a zero value. The zero value is 0 for a numeric type, false for a boolean and "" (empty string) for a string.

If you have an initial value, the type is optional as it's inferred from the value.

var goMascott = "Gopher"

If you copy the previous line in your IDE, you may see an error saying you declared something but not used it. So let's print out the variable's value in the console using Println() from the fmt package.

import "fmt"
 
fmt.Println(goMascott)

If you are declaring and initializing the variable inside a function block, you can omit the var keyword and use the declaration-assignation operator :=

foo := "bar"

If your variable's value isn't going to change in the future (well that's not a variable), you can make it a constant with const. Constants cannot be declared using the := syntax.

const tokyoGravitationalAcceleration = 9.798

You can also declare multiple variables at once, using a comma , to separate them. If you want to declare multiple variables of the same type, you can use parentheses ().

var (
    x int
    y int
    z int
)
 
x, y, z := 1, 2, 3

Enum

Go don't explicitly have an enum operator, but you can use constants to achieve the same effect. You can use the iota keyword to create a sequence of constants.

type Status int
 
const (
	Done Status = iota
	InProgress
	NotStarted
)
 
var statusColor = map[Status]string{
	Done:       "green",
	InProgress: "yellow",
	NotStarted: "red",
}
 
func printStatus(s Status) {
	fmt.Printf("Status: %s, Color: %s\n", s, statusColor[s])
}

Array and slice

An array is a fixed-size sequence of elements of the same type. You can create an array using the [size]type syntax.

var numbers [5]int

You access the elements of an array using the index, which starts at 0.

numbers[0] = 1
numbers[1] = 2
 
fmt.Println(numbers[0]) // prints 1

Note that the size of the array is part of its type, so [5]int and [10]int are different types. An array's size must be a constant expression, and it cannot be changed after the array is created. That's why in real world applications, you will rarely use arrays. Instead, you will use slices, which are more flexible and powerful. A slice is a dynamically-sized, flexible view into the elements of an array. You can create a slice using the []type syntax.

var numbers = []int{
    1, 2, 3, 4, 5,
}

You can also create a slice from an array using the [:] syntax. The general syntax is array[start:end], where start is the index of the first element you want to include in the slice, and end is the index of the first element you want to exclude from the slice. If you omit start, it defaults to 0, and if you omit end, it defaults to the length of the array.

var arr = [5]int{1, 2, 3, 4, 5}
var slice = arr[:] // slice is a slice of the entire array

Conceptually, a slice is a reference to an array, and it contains a pointer to the underlying array, the length of the slice, and its capacity. You can think of a slice as a lightweight abstraction over an array. The capacity of a slice is the number of elements in the underlying array that can be accessed by the slice. You can use the len() and cap() functions to get the length and capacity of a slice, respectively.

fmt.Println(len(slice)) // prints 5
fmt.Println(cap(slice)) // prints 5

You can append elements to a slice using the append() function. If the slice has enough capacity, the new element is added to the end of the slice. If not, a new underlying array is allocated with double the capacity, and the elements are copied to the new array.

slice = append(slice, 6) // slice now contains [1, 2, 3, 4, 5, 6]
 
fmt.Println(cap(slice)) // prints 10 (the new capacity)

You can map through a slice using the for loop (which is by the way the only loop in go), and use the range keyword to iterate over the elements of a slice.

for i, v := range slice {
    fmt.Printf("Index: %d, Value: %d\n", i, v)
}

Map

A map is an unordered collection of key-value pairs, where each key is unique. You can create a map using the map[keyType]valueType syntax.

var ages = map[string]int{
    "Alice": 30,
    "Bob":   25,
    "Charlie": 35,
}

You can access the value associated with a key using the [] syntax. If the key does not exist in the map, it returns the zero value of the value type.

fmt.Println(ages["Alice"]) // prints 30
fmt.Println(ages["Dave"]) // prints 0 (zero value of int)

You can also check if a key exists in the map using the value, ok := map[key] syntax. If the key exists, ok will be true, and value will be the value associated with the key. If the key does not exist, ok will be false, and value will be the zero value of the value type.

value, ok := ages["Alice"]
if ok {
    fmt.Println("Alice's age is", value)
} else {
    fmt.Println("Alice not found")
}

You can add or update a key-value pair in a map using the map[key] = value syntax.

ages["Dave"] = 40 // adds a new key-value pair
ages["Alice"] = 31 // updates Alice's age

You can delete a key-value pair from a map using the delete(map, key) function.

delete(ages, "Bob") // removes Bob from the map

Function

A function is a block of code that performs a specific task. You can define a function using the func keyword, followed by the function name, parameters, return type and the function body.

func add(a int, b int) int {
    return a + b
}
 
func main() {
    result := add(2, 3)
    fmt.Println("Result:", result)
}

In Go, functions can have multiple return values. You can define the return types in parentheses after the parameter list. If a function has multiple return values, you can use the return statement to return them.

func divide(a int, b int) (int, int) {
    if b == 0 {
        panic("division by zero")
    }
    return a / b, a % b // returns quotient and remainder
}
func main() {
    quotient, remainder := divide(10, 3)
    fmt.Println("Quotient:", quotient, "Remainder:", remainder)
}

Error handling

This pattern is very common in Go, especially for error handling. You can return an error as the last return value, and check if it's nil to determine if the function succeeded or failed. Talking about error handling, Go doesn't have exceptions like other languages.

Let's redefine the divide function to return an error if the divisor is zero.

func divide(a int, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil // returns quotient and nil error
}
 
func main() {
    quotient, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Quotient:", quotient)
}

Struct, interface and method

Struct

A struct is a composite data type that groups together variables (fields) under a single name. You can define a struct using the type keyword, followed by the struct name and the fields.

 
type Person struct {
    Name string
    Age  int
}

You can create an instance of a struct using the struct literal syntax, which is similar to creating a map.

p := Person{
    Name: "Alice",
    Age:  30,
}

You can access the fields of a struct using the . operator.

fmt.Println("Name:", p.Name)
fmt.Println("Age:", p.Age)

You can also define methods on a struct, which are functions that operate on the struct's fields. To define a method, you need to specify the receiver type before the function name. The receiver type is the type of the struct that the method operates on.

func (p Person) Greet() {
    fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
}
func main() {
    p := Person{
        Name: "Alice",
        Age:  30,
    }
    p.Greet() // calls the Greet method on the Person struct
}

Interface

An interface is a type that defines a set of methods that a struct must implement. You can define an interface using the type keyword, followed by the interface name and the method signatures.

type Greeter interface {
    Greet()
}

You can implement an interface, implicitely by defining a method with the same signature as the interface method on a struct. If a struct implements all the methods of an interface, it is said to satisfy that interface.

type Person struct {
    Name string
    Age  int
}
func (p Person) Greet() {
    fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
}
 
type Dog struct {
    Name string
}
 
func (d Dog) Greet() {
    fmt.Printf("Woof! My name is %s.\n", d.Name)
}
 
func main() {
    var g Greeter // declares a variable of type Greeter interface
    p := Person{Name: "Alice", Age: 30}
    d := Dog{Name: "Buddy"}
 
    g = p // assigns a Person to the Greeter interface
    g.Greet() // calls the Greet method on the Person struct
 
    g = d // assigns a Dog to the Greeter interface
    g.Greet() // calls the Greet method on the Dog struct
}

Control flow statements

If, else if, else

In Go, you can use the if, else if, and else statements to control the flow of your program based on conditions. The syntax is similar to other languages, but there are some differences.

if age := 30; age < 18 {
    fmt.Println("You are a minor")
} else if age < 65 {
    fmt.Println("You are an adult")
} else {
    fmt.Println("You are a senior citizen")
}

In this example, we're declaring a variable in the if statement, which is scoped to the if block. This is useful for avoiding variable shadowing and keeping the code clean. We can use this pattern for example accessing conditionnally a value from a map.

if value, ok := ages["Alice"]; ok {
    fmt.Println("Alice's age is", value)
} else {
    fmt.Println("Alice not found")
}

Also, Go don't have a ternary operator as part of the philosyphy of keeping the language simple.

Switch

A switch statement is a control flow statement that allows you to execute different code blocks based on the value of an expression. The syntax is similar to other languages, but there are some differences.

switch day := "Monday"; day {
case "Monday":
    fmt.Println("It's the start of the week")
case "Friday":
    fmt.Println("It's the end of the week")
case "Saturday", "Sunday":
    fmt.Println("It's the weekend")
default:
    fmt.Println("It's a regular day")
}

Switch statements can also be used without an expression, in which case it behaves like a series of if statements. You can also use the fallthrough keyword to continue executing the next case block, even if the current case matches.

switch {
case age < 18:
    fmt.Println("You are a minor")
case age < 65:
    fmt.Println("You are an adult")
    fallthrough // continue to the next case
case age >= 65:
    fmt.Println("You are a senior citizen")
}

Loop

Go has only one loop construct, the for loop. It can be used in three different forms: a traditional for loop, a while-like loop, and a range-based loop.

// Traditional for loop
for i := 0; i < 10; i++ {
    fmt.Println(i)
}
 
// While-like loop
i := 0
for i < 10 {
    fmt.Println(i)
    i++
}
 
// Range-based loop over a slice
numbers := []int{1, 2, 3, 4, 5}
for index, value := range numbers {
    fmt.Printf("Index: %d, Value: %d\n", index, value)
}

You can also use the break and continue keywords to control the flow of the loop. break exits the loop, while continue skips the current iteration and continues to the next one.

for i := 0; i < 10; i++ {
    if i == 5 {
        continue // skip the rest of the loop when i is 5
    }
    fmt.Println(i)
}

Concurrency

Go has built-in support for concurrency, which allows you to write programs that can perform multiple tasks simultaneously. The main concurrency primitives in Go are goroutines and channels.

Goroutines

A goroutine is a lightweight thread of execution that runs concurrently with other goroutines. You can create a goroutine using the go keyword followed by a function call.

func sayHello() {
    fmt.Println("Hello from a goroutine!")
}
 
func main() {
    go sayHello() // starts a new goroutine
    time.Sleep(1 * time.Second) // wait for the goroutine to finish
}

Goroutines are managed by the Go runtime, which schedules them to run on available CPU cores. They are very lightweight compared to traditional threads, allowing you to create thousands of them without significant overhead. [...work in progress...]

Conclusion

Go is a powerful language that combines simplicity with performance. It has a rich standard library and a vibrant community. The language's design encourages writing clean and maintainable code. If you're looking for a language that can handle high-performance applications while being easy to learn, Go is worth considering. Personally I'm going to go deeper in it and write some complex project with it in the future. This blog post only explored the very basics of the language. A great way to learn it is to try building something with it. I would suggest the this repository which contains a list of Go projects to help you get started. Thank you for reading this post, and I hope you found it helpful in your journey to learn Go. If you have any questions or suggestions, feel free to reach out to me.

If you need to dive deeper into Go, I recommend checking out the official documentation, the Wiki and the blog. I really liked learn go with tests too, it's TDD but the content is really nice, well explained, step by step.