Skip to content
Search
Generic filters
Exact matches only

An Introduction to Golang. All you need to get started | by Dhanesh Budhrani | Aug, 2020

All you need to get started

Dhanesh Budhrani
It’s time to learn Golang. [Image source]

This post intends to be an introduction to the Go programming language, also known as Golang.

I’m not an expert in Go. In fact, I’ve started learning about Go very recently. Therefore, take everything in this post with a pinch of salt.

Then… why am I writing a post about Go? It’s simple: I want to use this post as a tool to reinforce my learning process. I believe that working on a blog post and publishing it will force me to get every detail straight. Moreover, I’ll be crafting the guide that I would have liked to have found in the first place (and, therefore, it is likely that it will become the guide that you were looking for as well). This also means that this post will probably be evolving as I learn new aspects of Go.

Keep in mind, though, that despite of the fact that I’ll be working on every detail of the post, I may make mistakes. I encourage you to indicate in the comments section any errors that you may find, and I’ll gladly correct them.

If this disclaimer has let you down and you are not interested in reading this post anymore, here are some fantastic resources to get started with Go:

If you, however, decide to stay and keep reading… welcome!

Golang was born in Google, and it was created by Robert Griesemer, Rob Pike and Ken Thompson. It was discussed for the first time on September 21, 2007 and eventually became an open-source project on November 10, 2009.

As reported in Go’s site, this language was born due to the commonly necessary choice between an efficient compilation, efficient execution or ease of programming at the time of choosing a programming language to work with. Therefore, the main goal of Go was to create a language that would achieve the ease of an interpreted and dynamically typed language, while preserving the efficiency and safety of a statically typed, compiled language. Another one of the main goals was to take advantage of the growth of multicore CPUs, making concurrency one of the priorities of the language.

To get a deeper insight into the goals of Go, I’d recommend reading this article, written by Rob Pike. Some of the goals mentioned in this article include finding a solution for:

  • slow builds
  • uncontrolled dependencies
  • each programmer using a different subset of the language
  • poor program understanding (code hard to read, poorly documented, and so on)
  • duplication of effort
  • cost of updates
  • version skew
  • difficulty of writing automatic tools
  • cross-language builds

Considering that Google works with large-scale systems that have huge codebases, Go was primarily developed as a language “in the service of software engineering”, attempting to solve the most common issues in large systems.

If you are going to start programming in Go, you need to know the language’s mascot! It’s called the gopher.

The gopher. [Image source]

Learn about the gopher here!

Setting up our development environment

In order to follow through this post, you have two options:

  1. Set up a local development environment
  2. Use Go’s official online playground

I will not go into the installation details for each platform, but here are some guides:

If you, however, prefer not to commit for now and test out the language, option 2 is a perfectly valid option. In any case, I recommend you to go through this introduction executing the examples, instead of just reading the code: I’m sure you would get a better grasp of the language.

Hello world!

Well, first things first! Let’s get started writing our first program in Go. In the following sections I’ll be explaining in more detail each part of our program. We may print our “Hello world!” message with the following few lines of code:

package mainimport "fmt"func main() {
fmt.Println("Hello world!")
}

We’ll save the above snippet in a file called Main.go and then run the following instruction in our CLI:

go run Main.go

Voilà! If everything worked correctly, you should see our message printed in the terminal. Congrats! You’ve written your first Go program!

Let’s make an initial explanation of the above snippet, so that we start getting our first building blocks. Go programs are organized into packages, and one special package is “main”, which must be used in executable commands. We may now move towards the import statement, which allows us to include packages in our program. It may be worth mentioning that we may import multiple packages with the following syntax:

import (
"<package_1>"
"<package_2>"
)

In our example, we can see the “fmt” package, which is commonly used to read from stdin or write to stdout. We’ll be discovering this package in more detail as this article develops.

Finally, we may find the func main() declaration which, unsurprisingly, declares the main function of our Go program.

Declaring and initializing variables

Go is a statically typed language. This means that, unlike dynamically typed languages (such as JavaScript or Python), you initially declare the type of a variable and stick to it. For instance, if you declare an integer variable, it cannot become a string in a later stage of its lifecycle. Even though it becomes more verbose and strict, it gives the compiler more opportunities to optimize your code, given that the variable type is always the same. Check out this article for a more in-depth comparison of statically and dynamically typed languages.

Let’s declare our first variables. To do that, we’ll have to use the following syntax:

var <name> <type> [= <value>]

I’m using the values between < and > as placeholders, and the square brackets [ ] to indicate an optional part of the statement. Therefore, both of the following statements would be valid:

var i int
var j string = "hello"

Note that, while we have explicitly initialized variable j with the string “hello”, the variable i is actually initialized as well: declared-only variables are initialized with a “zero value”, which translates into 0 for numbers, “” for strings and false for boolean variables.

You may, moreover, declare (and optionally initialize) multiple variables of the same type using a single line of code with the following syntax:

var "name_1", "name_2" "type" [= "val_1", "val_2"]

Let’s illustrate this feature with an example:

var i, j int = 12, 27

Golang, however, not only had the goal of producing efficient code, but also to make the process of programming faster. This is why we may also initialize a variable using the walrus operator (:=). Using this operator, the syntax becomes more succinct, and we do not need to explicitly define the variable type (Go decides it for us, aka type inference). It’s important to note, though, that the walrus operator may only be used inside a function. The following example may illustrate how simple it becomes to declare and initialize a variable:

k := 42.0

Go, moreover, allows you to decide the visibility of a variable based on its name. If your variable starts with a lowercase letter, it will remain internal to the package. However, if the first letter is uppercase, it will become visible (or exported) to other packages.

I’d like to conclude this section by adding the following caveats:

  • all variables need to be used, otherwise Go will return a compile-time error.
  • redeclaring a variable is not allowed (with the exception of shadowing).
  • the above walrus operator example will create a variable of type float64. If we want to create a float32, we need to use the more verbose declaration.

You may also be interested in checking out the list of allowed data types.

Converting between types

We may convert (or cast) a value into a different type by applying the following syntax:

<type>(<var>)

Naturally, in-place casting is not allowed, as we are working in a statically typed language:

var i int = 12
var j float32 = float32(i)

Let’s now attempt to convert an integer into a string:

a := 65
b := string(a)
fmt.Println(b)

If you run the above 3 lines of code, you’ll see that it prints out “A”, instead of “65” as we could have expected. This happens because the direct casting of an integer into a string returns the Unicode character of the given number (“A” is the character #65 in Unicode).

So… how can we cast an integer into a string? We’ll need the strconv package for this purpose. Specifically, in this case we’ll need the Itoa (Integer to ASCII) function:

package mainimport (
"fmt"
"strconv"
)
func main() {
a := 65
b := strconv.Itoa(a)
fmt.Printf("%v, %Tn", b, b)
}

If you run the above snippet, you’ll see that b is now “65”.

Constants

You may also define constant values using the “const” keyword:

const z = 123

Now, variable z will have a fixed value of 123 and you will not be able to modify it. I’d also like to add that the walrus operator is not applicable to constants.

Comments

Writing comments in your code can help you explain to your co-workers (and your future self) a fragment of code. You may write a single-line comment with the following syntax:

// <comment>

… or multi-line comments just like this:

/*
<comment>
*/

Pointers

Go allows you to work with pointers, which hold the memory address of a value. We may define a pointer with the following syntax:

var <pointer_name> *<type>

As an example:

var i *int

We may also use the & operator, which returns the pointer of the referenced variable:

var <pointer_name> *<type> = &<variable>

Finally, we may access the content of a pointer with the * operator:

*<pointer_name>

To wrap up, let’s see how we may work with pointers:

package mainimport "fmt"func main() {
var i int = 27
var p *int = &i
fmt.Println(*p)
*p += 1
fmt.Println(i)
}

In this example, we’ve created an integer variable “i” set to 27 and then a pointer “p” referencing to it. Then, we’ve printed out the value of the variable through the pointer. Finally, we’ve incremented the value of the variable through the pointer and printed out the final value.

Functions

We may create a function in Go using the following syntax:

func <name>([<var_1> <type_1>, <var_2> <type_2>]) [<return_type>] {}

In the above fragment I’ve included two arguments as an example, but we may provide zero or more arguments to a function. Let’s see an example:

func multiply(i int, j int) int {
return i * j
}

Note that the returned value is of the same type as the declared returned value in the function’s signature. We may now call this function as follows:

multiply(3, 11)

Go offers a more compact syntax when two (or more) consecutive arguments share the same type. We may simply declare the type of these variables by declaring it in the last one:

func multiply(i, j int) int {
return i * j
}

In the above example, both arguments are of type int.

We may also return multiple values in a function:

func personal_info() (string, int) {
name := "John"
age := 27
return name, age
}

… or return named values (aka naked return):

func ops(x, y float64) (mult, div float64) {
mult = x * y
div = x / y
return
}

In this case, we don’t even need to specify the variables in the return statement, since they are already specified in the function’s signature. Moreover, note that we do not need to declare these variables in the function’s body, as they are declared in the function’s signature!

Conditional statements

If(-else) statements may be implemented in Go with the following syntax:

if [<statement>;] <condition> {
<if-block>
} [else if <condition> {
<else-if-block>
}] [else {
<else-block>
}]

Let’s get started with the simplest case: a simple conditional statement.

package mainimport "fmt"var x int = 8func main() {
if x >= 5 {
fmt.Println("passed")
}
}

Easy, right? Note that parentheses are not needed!

Let’s spice it up a bit more:

package mainimport "fmt"var foo int = 4
var bar int = 3
func main() {
if baz := foo + bar; baz < 3 {
fmt.Println("very low")
} else if baz < 5 {
fmt.Println("low")
} else if baz < 7 {
fmt.Println("medium")
} else {
fmt.Println("high")
}
}

Note the statement that defines the variable “baz” in the first if statement (baz := foo + bar). The scope of this variable is limited to the if(-else) block.

For loops

A loop in Go may be created with the following syntax:

for [<initializer>;] [<condition>] [; <update>] {
<loop-block>
}

Note that parentheses are not needed in Go’s “for” loop. Also, you may notice that both the initializer and the update step are optional, allowing you to define a “for” loop with only a condition. This kind of loop is normally known as a “while” loop in many languages. However, Go allows you to do this with a “for” loop.

Let’s see an example of a normal “for” loop:

package mainimport "fmt"func main() {
res := 1
base := 2
exp := 9
for i := 1; i <= exp; i++ {
res = res * base
}
fmt.Println(res)
}

In this example, we’ve implemented the exponentiation (just as an example, you may use math.Pow if you need to use it).

Let’s now look at a “while” example (no initializer and no update step):

package mainimport "fmt"func main() {
i := 1
sum := 0
for i < 10 {
sum += i
i += 1
}
fmt.Println(sum)
}

This loop keeps track of an incrementing “i” variable, which is compared to 10 in the condition, and adds it to a “sum” variable. As a result, we obtain the sum of integers from 1 to 9.

As a final note, you may ignore the condition as well, and create an infinite loop.

Defer

Defers are a very interesting instruction of Go: they allow you to execute a statement after the surrounding function has returned. They are, however, evaluated immediately.

Let’s look at an example:

package mainimport "fmt"func main() {
defer fmt.Println("3")
defer fmt.Println("2")
fmt.Println("1")
}

Note that I’ve included 2 defer statements. I wanted you to note that defers are stacked in a LIFO (Last In First Out) basis. In fact, if you execute the above snippet, you’ll see that the numbers are printed out in ascending order.

Panic

Panic is a built-in function that allows us to indicate that something has gone wrong. It will stop the execution of the program and start unrolling the call stack, propagating the error. Let’s see how we can panic in Go:

package mainimport "fmt"func main() {
r := divide(1, 0)
fmt.Println(r)
}
func divide(i int, j int) int {
if j == 0 {
panic("dividing by 0 is not allowed")
}
return i / j
}

As you can see, Go exits with an error status, displaying our message.

Recover

Sometimes we’ll want to handle these errors, and we can do that by using the recover built-in function. It should be noted that this function will only have an effect if it is inside a deferred function. Let’s modify our panic example, and recover from the situation:

package mainimport "fmt"func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Let's not panic. Error received:", r)
}
}()
r := divide(1, 0)
fmt.Println(r)
}
func divide(i int, j int) int {
if j == 0 {
panic("dividing by 0 is not allowed")
}
return i / j
}

In this example the error has been handled and we are not exiting with an error status anymore. Moreover, we are printing the error message.

Arrays

We may define an array in Go with the following syntax:

var <name> [<length>]<type>

NB! In this case, with the square brackets [] I don’t mean an optional part… I mean square brackets.

Let’s look at an example:

package mainimport "fmt"func main() {
var position [2]float64
position[0] = 40.4168
position[1] = 3.7038
fmt.Println(position)
position[0] = 51.5074
fmt.Println(position)
}

Note that we may access (and update) an array index using the square brackets. It’s also important to note that Go’s arrays cannot be resized, and hence will always keep the initially set length.

An alternative syntax using the walrus operator allows to declare and initialize the array in one single statement:

<name> := [<length>]<type>{<value_1>, <value_2>, ..., <value_n>}

For instance:

position := [2]float64{40.4168, 3.7038}

Slices

Even though arrays have a fixed length, slices allow us to work with dynamically-sized data. More specifically, they allow us to work with a dynamic range of an array. A slice may be defined with the following syntax:

[]<type>

Then, we may obtain the values of the array in a range of indices by using the following syntax:

<array_name>[<min_idx] : <max_idx>]

In this case, we would obtain the values of the array <array_name>, starting in index <min_idx> (included) and ending in index <max_idx> (excluded).

Note again that, in these two cases above, the square brackets actually refer to… square brackets.

Let’s look at an example to better understand this concept:

package mainimport "fmt"func main() {
vowels := [5]string{"A", "E", "I", "O", "U"}
var some_vowels []string = vowels[1:3]
fmt.Println(some_vowels)
}

In this script we have an array containing the vowels and, using a slice, we print the second and third vowels.

You may also omit <min_idx> and/or <max_idx> in your slices. If you don’t specify them, <min_idx> will default to 0 (start of the array) and <max_idx> will default to the array length (end of the array). Let’s look at this with an example:

package mainimport "fmt"func main() {
vowels := [5]string{"A", "E", "I", "O", "U"}
var some_vowels []string = vowels[:3]
fmt.Println(some_vowels)
some_vowels = vowels[2:]
fmt.Println(some_vowels)
some_vowels = vowels[:]
fmt.Println(some_vowels)
}

In the first example, we’ve omitted <min_idx> and displayed every vowel until index 3 (excluded). In the second one, we’ve omitted <max_idx> and displayed every vowel starting from index 2 (included). Finally, in the third example we’ve omitted both <min_idx> and <max_idx> and displayed the full array of vowels.

Slices can be seen as a reference to the array given that, if we modify some value in a slice, such change will reflect in the original array (and in any slices referring to the modified array position). Let’s look at this with an example:

package mainimport "fmt"func main() {
numbers := [5]int{1, 2, 3, 4, 5}
var some_numbers []int = numbers[:3]
fmt.Println(some_numbers)
var other_numbers []int = numbers[2:]
fmt.Println(other_numbers)
some_numbers[2] = 7 fmt.Println(some_numbers)
fmt.Println(other_numbers)
fmt.Println(numbers)
}

As you can see, number 3 has been replaced by a 7, no matter if you print the original array or a slice of it.

Maps

A map is a key-value data structure, aka dictionary. We may create a map either using the make function or initializing it with some initial values.

Let’s see how we may create it with the make function, which receives the type of the keys and the values, and creates an empty map:

package mainimport "fmt"var colors map[string]stringfunc main() {
colors = make(map[string]string)
colors["red"] = "#FF0000"
colors["green"] = "#00FF00"
colors["blue"] = "#0000FF"
fmt.Println(colors["red"])
}

You may also create a map without the make function, if you can provide some initial values:

package mainimport "fmt"var colors = map[string]string {
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
}
func main() {
fmt.Println(colors["red"])
}

Let’s now manage the map and see how we may add an element, delete it or check whether it is present:

package mainimport "fmt"var colors = map[string]string {
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
}
func main() {
// add a new entry to the map
colors["black"] = "#000001"
// edit an existing entry of the map
colors["black"] = "#000000"
// delete an entry from the map
delete(colors, "red")
fmt.Println(colors) rgb_black, present_black := colors["black"]
fmt.Printf("%s, %tn", rgb_black, present_black)
// remember that we've deleted "red"!
rgb_red, present_red := colors["red"]
fmt.Printf("%s, %t", rgb_red, present_red)
}

As you may have noticed, you can check whether a key is present in a map by unpacking the map[key] syntax to two values: the first one holds the value of the key (if present, otherwise it holds the type’s zero-value), while the second value is a boolean that indicates whether it is present or not.

Structs

Go allows us to create structs, which are collections of fields. We may define a struct with the following syntax:

type <name> struct {
<name_1> <type_1>
<name_2> <type_2>
...
<name_n> <type_n>
}

As an example:

type Position struct {
Latitude float64
Longitude float64
}

Let’s now see how we may work with this new struct:

package mainimport "fmt"type Position struct {
Latitude float64
Longitude float64
}
func main() {
pos := Position{40.4168, 3.7038}
fmt.Println(pos)
pos.Latitude = 51.5074
fmt.Println(pos)
pos2 := Position{Longitude: 41.9028, Latitude: 12.4964}
fmt.Println(pos2)
}

Note how we’ve created a new instance of our struct:

<name> := <struct>{<value_1>, <value_2>, ..., <value_n>}

Moreover, we may access (and update) the value of a field using the dot operator:

<struct>.<field> [= <value>]

Finally, note as well that we may define the value of our fields by using their name in the instantiation statement, even with a different order.

Methods

Even though Go does not have the concept of classes, it does allow us to define methods. What is a method? It’s a function that is applied to a type. To better understand how to create a method in Go, we need to introduce the concept of “receiver”. Such “receiver” is the type to which this function will be applied. Let’s take a look at the syntax:

func (<receiver_instance> <receiver_type>) <method_name>([<arg_1> <type_1>, <arg_2> <type_2>, ..., <arg_n> <type_n>]) [<return_type>] {
<method_body>
}

Let’s look at an example:

package mainimport (
"fmt"
"math"
)
type Point struct {
X float64
Y float64
}
func (p Point) EuclideanDistanceFromOrigin() float64 {
return math.Sqrt(math.Pow(p.X, 2) + math.Pow(p.Y, 2))
}
func main () {
point := Point{4.2, 5.7}
fmt.Println(point.EuclideanDistanceFromOrigin())
point = Point{7.4, 8.1}
fmt.Println(point.EuclideanDistanceFromOrigin())
}

Go embraces higher-order functions, i.e. a function that takes one (or more) function as an argument and/or returns a function. Let’s see an example of both cases:

A function as an argument

We may pass a function as an argument, such as in the following example:

package mainimport "fmt"func sum(a, b int) int {
return a + b
}
func multiply(a, b int) int {
return a * b
}
func printResult(fn func(int, int) int, a, b int) {
val := fn(a, b)
fmt.Println(val)
}
func main() {
printResult(sum, 4, 5)
printResult(multiply, 4, 5)
}

Note that we need to pass both the type of function arguments and the type of the function output.

Returning a function

Let’s now look at an example of a higher-order function that returns another function:

package mainimport "fmt"func sum(a, b int) int {
return a + b
}
func multiply(a, b int) int {
return a * b
}
func getOperation(name string) func (int, int) int {
if name == "sum" {
return sum
} else if name == "multiply" {
return multiply
}
panic("Operation not supported")
}
func main() {
op := getOperation("sum")
fmt.Println(op(4,5))
op = getOperation("multiply")
fmt.Println(op(4,5))
}

Note that, in this case, we are declaring the function type as the return type of the higher-order function.

Concurrency is one of the main reasons why many people decide to switch to Go, given its efficiency and simplicity. One of the main concepts to know is the “goroutine”, which is a lightweight thread managed by the Go runtime. A goroutine may be created as simply as follows:

go f(<arg_1>, <arg_2>, ..., <arg_n>)

It should be noted that, even though the function f will be executed in the new goroutine, such function (and its arguments) are evaluated in the current goroutine (in fact, the main function runs in a goroutine as well!).

In order to allow two or more goroutines to communicate, we’ll have to make use of Go’s channels, which allow us to send and receive information from it. A channel may be created with the following syntax:

<variable> := make(chan <type>[, <buffer_size>])

Note that you may, optionally, define a buffer size. Send operations will be blocked if the buffer is full.

Values may be received/sent from/to a channel using the arrow operator <-. To send a value to a channel we would use the following syntax:

<channel> <- <value>

Instead, receiving a value from a channel and assigning it to a variable would look as follows:

<variable> := <- <channel>

Note that the information always flows in the direction of the arrow.

The use of channels also helps us in managing synchronization, without using any explicit locks or condition variables, given that send and receive operations block until the other side is ready.

Let’s wrap up everything we’ve seen so far with an example:

package mainimport (
"fmt"
"math/rand"
)
func randint(max int, c chan int) {
r := rand.Intn(max)
fmt.Println(r)
c <- r
}
func main() {
c := make(chan int)
go randint(1000, c)
go randint(200, c)
r1 := <- c
r2 := <- c
fmt.Println(r1 + r2)
}

In this example we’ve created a function that gets a random number and sends it to our channel. Then, we’ve called such function twice, each time running in its own goroutine. Finally, we’ve received the last two values of the channel, kept them in r1 and r2, and printed out the sum.

Also note that, for instance, the second printed number may be greater than 200. This would happen because each function call runs in a separate thread (or goroutine), and therefore they may not finish in the same order in which we have called them.

Let’s now see what happens if we attempt to exceed the buffer size:

package mainimport (
"fmt"
)
func main() {
c := make(chan int, 2)
c <- 1
c <- 5
c <- 4
r1 := <- c
r2 := <- c
r3 := <- c
fmt.Println(r1 + r2 + r3)
}

In this example we’ve defined a buffer size of 2, but then we’ve attempted to send 3 values to the channel. As expected, this makes our script to crash. Given that the buffer is full, the instruction c <- 4 is blocked until some value is received from the channel. Therefore, we may easily fix our script as follows:

package mainimport (
"fmt"
)
func main() {
c := make(chan int, 2)
c <- 1
c <- 5
r1 := <- c
c <- 4
r2 := <- c
r3 := <- c
fmt.Println(r1 + r2 + r3)
}

As you can see, we’ve just had to receive a value from the channel when the buffer was full, and then we’ve been able to send a new one to it without any issues.

A channel may be closed (always by the sender) to indicate that no more values will be sent to it. This may be done with the following syntax:

close(<channel>)

The receiver may then check whether the channel is closed or not. This may be achieved by defining a second variable in the receiving operation, which will be set to false if there are no more values to receive and the channel is closed. Let’s look at this with an example:

package mainimport (
"fmt"
"math/rand"
)
func randints(max int, c chan int) {
c <- rand.Intn(max)
c <- rand.Intn(max)
c <- rand.Intn(max)
close(c)
}
func main() {
c := make(chan int)
go randints(1000, c)
r1, ok1 := <- c
fmt.Println(r1, ok1)
r2, ok2 := <- c
fmt.Println(r2, ok2)
r3, ok3 := <- c
fmt.Println(r3, ok3)
r4, ok4 := <- c
fmt.Println(r4, ok4)
}

In this example we’ve sent 3 values to the channel and then read them, always checking whether the channel is closed. In fact, the fourth time we’ve received from the channel, we get that ok4 is equal to false, meaning that the channel is closed. All in all, this means that we could continue reading values from the channel as long as our “ok” variable is true. This is where range comes in, allowing us to iterate over the values of a channel until it is closed. Let’s see how we may simplify the above script with an example:

package mainimport (
"fmt"
"math/rand"
)
func randints(max int, c chan int) {
c <- rand.Intn(max)
c <- rand.Intn(max)
c <- rand.Intn(max)
close(c)
}
func main() {
c := make(chan int)
go randints(1000, c)
for i := range c {
fmt.Println(i)
}
}

In a much cleaner way, we’ve iterated through the channel until it was closed.

First of all, congratulations if you’ve made it this far! You may now be wondering, how can I continue learning about Go? Well, I’m one of those who think that getting your hands dirty is one of the best ways to learn. Nothing replaces working towards something, finding issues in your path and solving them yourself.

If you, however, want to continue reading about Go, you may check out these resources:

As long as I discover more interesting learning resources, I will keep adding them to this list.

Now it’s your turn to Go have fun with Go!