Everything you need to know about Go to get started! Let’s have a look at what go has to offer, some of the quirks, the syntax and some tips. This is assuming you have installed go, but you didn’t look too much into the documentation. Talking about documentation, the tour is a great place to start for interactive examples.
Let’s have an overview of what you could have missed, but first, let’s remind ourselves how to run the code.
I. Run it!
Create a new file named main.go
.
package main
import "fmt"
func main() {
fmt.Println("hello")
}
It needs to have a package main
declaration at the top,
as the main
function in that package is the entry point of the program.
The import "fmt"
statement is used to import the fmt
package, which is used to print messages to the console.
To run a Go file, you can use the go run
command.
go run main.go
This will compile and run the main.go
file.
To build an executable, you can use the go build
command.
go build main.go
This will create an executable file named main
.
II. Assigning in Go
1. Variables
Variables in Go are used to store values. They are used to store values that can change during the execution of the program. You need to specify the type of the variable when you declare it.
var a int
a = 1
fmt.Println(a)
You can also declare and initialize a variable at the same time.
b := 2
fmt.Println(b)
2. Constants
Constants in Go are values that don’t change. They are used to define values that are known at compile time.
const Pi = 3.14159
3. Ignoring values
You can assign multiple variables at once in Go, and ignore those you don’t need.
_, b := 1, 2
It will ignore the first value and assign 2
to b
.
This pattern is used when you need to explicitly ignore return values from a method.
4. Pointers
Pointers are a way to pass a reference to a value. They are used to avoid copying large values.
func Modify(a *int, b int) (int, int) {
*a += 1
b += 1
return *a, b
}
Now you can call the Modify
function with pointers.
This will copy the value of b
and pass a reference to a
and return the value of each increased by 1:
a, b := 2, 2
fmt.Println(Modify(&a, b)) // 3, 3
fmt.Println(a, b) // 3, 2
And as you can see, both return the same thing,
however the value of a
has been changed while the value of b
remained the same!
III. Data structures
1. Structs
Structs are a way to define a type that groups together fields. They are similar to classes in other languages, as such they can have methods and implement interfaces. (we’ll have a look at that in more details, keep scrolling down 🤓).
type Person struct {
Name string
Age int
}
Now you can create a Person
type.
p := Person{Name: "Alice", Age: 30}
fmt.Println(p.Name)
2. Slices
Slices are a way to represent a sequence of values. They are similar to arrays, but they are more flexible.
s := []int{1, 2, 3}
fmt.Println(s[0])
You can append values to a slice.
s = append(s, 4)
fmt.Println(s)
The difference between a Slice and an Array is that an Array has a fixed size (e.g. an array of size 3 var arr [3]int
),
while a Slice can grow or shrink.
A Slice is a reference to an underlying array, so when you append a value to a Slice,
it might create a new array and copy the values.
3. Maps
Maps are a way to represent a collection of key-value pairs. They are similar to dictionaries in other languages.
m := map[string]int{
"one": 1,
"two": 2,
}
fmt.Println(m["one"])
You can add values to a map.
m["three"] = 3
fmt.Println(m)
IV. Errors
Error handling in Go is a bit different from other languages.
It uses the error
type to represent an error.
err := doSomething()
if err != nil {
log.Fatal(err)
}
The downside is that the error checking can become a bit bulky. You can reduce the size using this syntax:
if err := doSomething(); err != nil {
log.Fatal(err)
}
The log.Fatal
function will print the error message and stop the program with a non-zero exit code.
V. Functions
1. Named return values
Named return values are a way to define the return values of a function. They are used to make the code more readable.
func Add(a, b int) (sum int) {
sum = a + b
return
}
The return value is called sum
, and it is automatically returned by the function.
That’s why you don’t need to specify it in the return
statement.
It can also be used to return multiple values and empty values.
func example() (hello string, error error) {
if err := doSomething(); err != nil {
return
}
return "hello", nil
}
No need to specify ""
when there’s an error, it will do it be default for the hello returned value.
2. Using a function from another package
To use a function from another package, you need to import it.
You can import a package using the import
keyword.
import (
"fmt"
"src/pkg/example"
)
func main() {
fmt.Printf("1 + 2 = %d", example.Add(1, 2))
}
Since the method Add
is capitalized, it’s exported and can be used outside the package.
To use it in main, I can use it since it’s in my import
like the built-in fmt package.
3. Defer
Defer is a keyword in Go that allows you to delay the execution of a function until the surrounding function returns.
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
In this example, the world
message will be printed after the hello
message.
VI. Interfaces
Go doesn’t have classes, but it has interfaces. Interfaces are a way to define a set of methods that a type must implement.
type Handler interface {
Handle()
}
Now you can create a type that implements the Handler
interface.
type UserHandler struct{}
func (h UserHandler) Handle() {
fmt.Println("hello")
}
As you can see, the UserHandler
type implements the Handle
method.
It is implicitly implementing the Handler
interface.
The (h UserHandler)
is the receiver of the method. A receiver is a way to attach a method to a type.
It is similar to this
in JavaScript or self
in Python.
Let’s create our handler and call the Handle
method:
handler := UserHandler{}
handler.Handle()
Only functions with Capitalized names are exported, so they can be used outside the package.
VII. Goroutines
1. Introduction
Goroutines are lightweight threads managed by the Go runtime. They are used to run functions concurrently.
go fmt.Println("hello 1")
You can use it to run functions concurrently using the go
keyword.
If you want or need to wait for the goroutine to finish,
you can use a sync.WaitGroup
to defer the wait group’s done so it waits for the goroutine execution.
func main() {
var wg sync.WaitGroup
wg.Add(1) // Increment goroutine to wait for
go func() {
defer wg.Done() // Decrement the counter when it's done
fmt.Println("hello 2")
}()
wg.Wait() // Wait for all goroutines to finish
}
Or you can use a channel.
2. Channels
Channels are a way to communicate between goroutines. They are used to send and receive values between goroutines.
ch := make(chan int)
go func() { ch <- 42 }()
fmt.Println(<-ch)
This will wait for the goroutine to send a value to the channel and then print it.
3. Select
Select is a way to wait for multiple channels to send values. It allows you to wait for multiple channels to send values based on the first received one:
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch1 <- 43 }()
go func() { ch2 <- 44 }()
select {
case v1 := <-ch1:
fmt.Println(v1)
case v2 := <-ch2:
fmt.Println(v2)
}
It will print either 42 or 43 based on which channel sends a value first. The Select only execute one case, so it won’t print both numbers.
4. Context
You can use the context here to cancel the goroutine, but it’s not limited to goroutines. Let’s create a context with cancel for our example derived from the application context.
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-ctx.Done()
fmt.Println("done")
}()
cancel()
The <-
blocks the goroutine’s execution until the context is done and the cancel
function is called.
When we call the cancel
function, it will let the goroutine execute and print done
.
The context becomes useful when used in an API call. It can hold information regarding the request, like the user, the request ID. For example if the user logs out, you can cancel the context and stop the request.
VIII. Testing
1. Introduction
Test files in go are named *_test.go
, they are usually placed right next to the file they are testing,
in the same package.
main.go
main_test.go
To run the tests in the current package, you can use the go test
command.
go test
To run all the tests in all the packages, you can use the go test ./...
command.
2. Write a test
Testing in Go is done using the testing package.
It allows you to write tests for your code and run them using the go test
command.
func Add(a, b int) int {
return a + b
}
And now the test:
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Errorf("Add(1, 2) = %d; want 3", got)
}
}
If the test pass, or in this case, doesn’t error out, it will simply output PASS
in the console with the test name.
3. Using an assertion library
There’s no default assertion library in Go, it just prints the error message and stops the test. You can use the external library testify to have more advanced assertions.
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestAdd(t *testing.T) {
assert.Equal(t, 3, Add(1, 2))
}
To make it a test function, it must be named Test*
, have a *testing.T
parameter and be in a file named *_test.go
.
As you can see the function’s first letter is capitalized however it can’t be exported to other packages.
So helper test functions for multiple packages need to have their proper package and file.
4. Benchmarking
Benchmarking in Go is done using the testing
package.
It allows you to write benchmarks for your code and run them using the go test -bench
command.
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
This will run the Add
function b.N
times and measure the time it takes to run it.
BenchmarkAdd
BenchmarkAdd-8 1000000000 0.5165 ns/op
The prefix of the test matters as it’s what’s make it a benchmark test.
The above output shows that the Add
function runs in 0.5165 ns/op
which means it runs in 0.5165 nanoseconds
per operation!
The number of operations is defined by the b.N
value.