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.

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.
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.