Golang basics
Old notes about golang basic syntax and features
this is an old study notes about golang, when I was first learning its syntax and nice features.
Imports
import "fmt";
import "math";
// OR
import (
"fmt"
"math"
)
Exports
Exports begins with capital letter
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Pi)
}
Note Println
and Pi
, they are exported.
Functions
func add(x int, y int) int { return x + y }
If two or more arguments have the same type, you can do: add(x, y int)
A function can return multiple results:
func swap(x, y string) (string, string) {
return y, x
}
- Note that you need to declare the returned types
You can name the returned values like so:
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
- Note that
x
andy
are defined to be returned asint
types. - Also note the alone
return
, it is called naked return and it returns the named return values (x
andy
) - Only use naked returns in short functions, because of readability
When passing arguments into a function, if it’s passed by value (like a string), it will basically copy the value and you can modify the value inside the function:
func main() {
greeting := 'Hello'
name := 'Ricardo'
sayGreeting(greeting, name)
fmt.Println(name)
}
func sayGreeting(greeting, name string) {
fmt.Println(greeting, name)
name = 'Ted'
fmt.Println(name)
}
// RESULT
Hello Ricardo
Ted
Ricardo
The name
is only modified inside the sayGreeting
function
But if we pass a pointer to the value?
func main() {
greeting := 'Hello'
name := 'Ricardo'
sayGreeting(&greeting, &name) // pass the address
}
func sayGreeting(greeting, name *string) { // make sure it's a pointer
fmt.Println(*greeting, *name) // dereference the pointers to display the values
*name = 'Ted' // dereference pointer and assign a new value
fmt.Println(*name)
}
// RESULT
Hello Ricardo
Ted
Ted
Now we’ve changed the value not only in the scope of the function, but outside too. We do that because:
- sometimes you want to manipulate the arguments value inside the function
- passing a pointer is much more performatic than passing entire values (like a large data structure)
You also can do something like a spread operator, but it only works as the last parameter of a function:
func main() {
sum(1, 2, 3, 4, 5)
}
func sum(values ...int) {
fmt.Println(values)
result := 0
for _, v := range values {
result += v
}
fmt.Println(result)
}
Go can do something unusual. The return value of a function can be a local variable returned as a pointer:
func sum(values ...int) *int { // return type is a pointer integer
fmt.Println(values)
result := 0
for _, v := range values {
result += v
}
return &result // return the address of result
}
We also can do a named return, which means we don’t have to declare the variable name inside the function, neither return it. The syntax is like this:
func sum(values ...int) (result int) { // define the result variable here
fmt.Println(values)
result := 0
for _, v := range values {
result += v
}
return // return the result is implicit
}
- This above is just syntatic sugar, but it can be kind of confuse to read.
In Go we can have two return values from a function. That’s very usefull when dealing with function that can give us an error. So we return an error value as a second argument, and deal with the error in the higher function:
func main() {
d, err := divide(5.0, 0.0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(d)
}
func divide(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, fmt.Errorf("Cannot divide by zero")
}
return a / b, nil
}
This above is a very commom pattern in Go.
Functions themselves can be threated as values, can be passed around and everything. Usually to work with functions as values you use the anonymous function:
func main() {
var f func() = func() {
fmt.Println("Anonymous")
}
f()
}
A more complex example would be:
func main() {
var divide func(float64, float64) (float64, error)
divide = func(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, fmt.Errorf("Cannot divide by zero")
} else {
return a / b, nil
}
}
d, err := divide(5.0, 3.0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(d)
}
Above, we are declaring the variable which will hold the function, with it’s args and return value types.
Then we assign the anonymous function to the divide
variable.
Then retrieving the returned values from divide into variables d, err
Now let’s talk about functions as methods. A method is basically a function executed within a context:
func main() {
g := greeter{
greeting: "Hello",
name: "Go",
}
g.greet()
}
type greeter struct {
greeting string
name strig
}
func (g greeter) greet() {
fmt.Println(g.greeting, g.name)
}
The method is basically a function inside the type. In this case the type is a struct called greeter
.
So you can define a struct, above is called g
and then call the method g.greet()
Everytime you call a method you pass the copy of the type, in this case a copy of the struct greeter
.
If you want, you can pass a pointer, so everything you do will change the underlying struct too:
func (g *greeter) greet() { ... }
Variables
var
declares a list of variables. It can be declared inside or outside the function.
var c, python, java bool
func main() {
var i int
fmt.Println(i, c, python, java)
}
- Note that types come after the declaration name
Variables can be declared with initial values:
var i, j int = 1, 2
If there is an initial value, the type can be omitted, since the variable will have the type of the initial value:
var c, python, java = true, false, "no!"
Inside a function, variables with initial values can be declared in a short declaration:
func main() {
k := 3
c, python, java := true, false, "no!"
}
- Note:
:=
is only possible inside functions
Variables can be factored declared like import statements:
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
Variables declared without a value are given zeroed values
- 0 for numeric types
false
for boolean""
for strings
We can use constants for declaring varibles, using const
. They cannot be declared with :=
syntax
const Truth = true
Constants
We can define a const block and use the special value iota
to create enumerated expressions.
iota
starts at 0 value and increment by one each time:
const (
a = iota // 0
b // 1
c // 2
d // 3
)
const (
isAdmin = 1 << iota // 1 or 00000001
isHeadquarters // 2 or 00000010
canSeeFinancial // 4 or 00000100
canSeeAfrica // 8 or 00001000
canSeeAmerica // 16 or 00010000
canSeeAsia // 32 or 00100000
canSeeEurope // 64 or 01000000
)
func main() {
var roles byte = isAdmin | canSeeFinancial | canSeeAmerica
fmt.Printf("%b
", roles)
fmt.Printf("canSeeFinancial? %v
", canSeeFinancial & roles == canSeeFinancial)
}
// RESULT
10101 (00010101 with 8 bytes)
canSeeFinancial? true
In the snippet above, we can see if a user has a permission to do something, for example.
With this line: var roles byte = isAdmin | canSeeFinancial | canSeeAmerica
we use the OR operator to store these values in a byte variable called roles
. The result is: 10101
because:
- isAdmin is 00000001 (first house has a byte)
- canSeeFinancial is 00000100 (third house has a byte)
- canSeeAmerica is 00010000 (fifth house has a byte)
- All together becomes: 10101 (fifth, third and first house has bytes). The value is 21.
Now, in this line: fmt.Printf("canSeeFinancial? %v\n", canSeeFinancial & roles == canSeeFinancial)
we use a AND mask canSeeFinancial & roles
to get the byte that has canSeeFinancial (third house, 00000100), which is value 4, and see if it’s equal (==) canSeeFinancial
value (which is 4). This evaluates to true, therefore, canSeeFinancial equals true
Types
Go 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
You can convert types using T(value)
:
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
// OR
i := 42
f := float64(i)
u := uint(f)
When declaring numeric variables, the type of the variable depends on the precision of the value:
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
Bit Operators
func main() {
a := 10
b := 3
fmt.Println(a & b)
fmt.Println(a | b)
fmt.Println(a ^ b)
fmt.Println(a &^ b)
}
// RESULTS
2
11
9
8
Why these weird results? We have to see the binary representation to understand
a := 10 // 1010 b := 3 // 0011
So for the & (and) operator, we compare the bits in each house to see if there is a bit in a
AND in b
. The result is:
0010 (2 in decimal) because the third house is the only place there is a bit in both a
and b
The | (or) operator check if there is a bit in a
or in b
. Therefore, the result is: 1011 (because there are bits in the first house, third house and fourth house). 1011 binary is 11 in decimal
Bitshifting
func main() {
a := 8
fmt.Println(a << 3)
fmt.Println(a >> 3)
}
a := 8 is basically 2^3. If we bitshift to the left 3 places (a << 3) it will basically do 2^3 * 2^3 = 2^6 (64)
The a >> 3 will bitsift to right 3 places: 2^3 / 2^3 = 2^0 (1)
For loop
Go has only one looping construct, the for
loop.
Is a normal for loop, with three components:
- the init statement: executed before the first iteration
- the condition expression: evaluated before every iteration
- the post statement: executed at the end of every iteration
func main() {
sum := 0
for i := 0; i < 10; i++ {
fmt.Println(sum)
sum += i
}
fmt.Println(sum)
}
The init and post statement are optionals:
func main() {
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum) // 1024
}
- Note there is no
()
and{}
are required
A while
loop can be done as for
loop in Go:
func main() {
sum := 9757
for sum < 100 {
sum += sum
}
fmt.Println(sum)
}
You can initialize two or more variables in the loop:
for i, j := 0, 0; i < 5; i, j = i+1 j+1 {
fmt.Println(i, j)
}
We can create a infinite for loop, with the conditional inside the for loop and a break
keyword to exit the loop:
i := 0
for {
fmt.Println(i)
i++
if i == 5 {
break
}
}
We can also use the continue
keyword, so the for loop exits the current loop, and goes to the next one:
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i)
}
This code above will print only odd numbers (ímpar).
If you have nested for loops and you want to break the loop (the inner and outer loop), you can use a label
:
Loop:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
fmt.Println(i * j)
if i * j >= 3 {
break Loop
}
}
}
// RESULT
1
2
3
So now when the conditional pass, we will stop the loop using the label Loop
, which is breaking the outer loop (because it’s positioned before the outer loop)
To use a for loop with a collection, say a slice, we use the for range syntax (because we may not know the size of the collection at runtime)
func main() {
s := []int{1, 2, 3}
for k, v := range s {
fmt.Println(k, v)
}
}
// RESULT
0 1
1 2
2 3
So range
will give us the key and value of each item in the slice, which we are storing inside the k
and v
variables.
If statement
if x < 0 {
fmt.Printf("X is like: %v
", x)
return sqrt(-x) + "i"
}
Like for loops, if don’t use ()
and {}
are required.
Also like the for loop, if statements can have a short statement before the loop:
if v := math.Pow(x, n); v < lim {
return v
}
v
is only scoped inside the if/else statement
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else {
fmt.Printf("%g >= %g
", v, lim)
}
// can't use v here, though
return lim
}
Another example of a if statement with a initializer:
if pop, ok := statePopulations["Florida"]; ok {
fmt.Println(pop)
}
The ok
is the test case for the if statement
. Everything behind is just a initializer, a expression.
When comparing floating point numbers, there is a gotcha! The best approach to compare them is not by using ==
, because the decimals can be a bummer.
The best solution is to divide the numbers and subtract 1, to get the remainer. And now you compare this remainer, to see if it’s smaller than a certain number (like 0.001). If is true, we can consider the numbers being equal.
func main() {
myNum := 0.123
if math.Abs(myNum / math.Pow(math.Sqrt(myNum), 2) - 1) < 0.001 {
fmt.Println("These are the same")
} else {
fmt.Println("These are different")
}
}
Switch statement
Go’s switch case cannot be constants and the values cannot be integers
func main() {
fmt.Print("Go runs on ")
switch os := "linux"; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.
", os)
}
}
- Note we don’t need a
break
statement, as Go will stop running automatically once it finds the right case
We can use switch statements without a condition (which always evaluate to true
) in order to write cleaner if/else statements:
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
We can use more than a case:
switch 5 {
case 1, 5, 10:
fmt.Println("Entrou)
case 2, 3, 11:
fmt.Println("Nahh")
default:
fmt....
}
You can put the comparisson inside the case
:
i := 10
switch {
case i <= 10:
fmt.Println("Umm")
fallthrough
case i <= 20:
fmt.Println("Doiss")
default:
fmt.Println("defaultt")
}
- You can make your switch to fall through by using the keyword
fallthrough
, like the above. Be aware that fallthrough will go to the next case even if it’s false.
We can use a switch case to check the type:
var i interface{} = 1
switch i.(type) {
case int:
fmt.Println("Integer")
case float64:
fmt.Println("Float64")
case string:
fmt.Println("String")
default:
fmt.Println("Another")
}
You can put a break
keyword inside the case, if you want to stop the case before something happens. For example, you can use a if statement and if evaluates to true, you use the break
.
Defer
We can use defer
to wait for surrounding functions to return, before run the deffered function
func main() {
defer fmt.Println("world")
fmt.Println("hello")
fmt.Println("WAAAAIT")
}
// OUTPUT
hello
WAAAAIT
world
Deferred function calls are pushed onto a stack. When a function returns, its deferred calls are executed in last-in-first-out order.
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
// OUTPUT
counting
done
9
8
7
6
5
4
3
2
1
0
Pointers
A pointer has the memory address of a value.
Putting *
in front of a TYPE indicates a pointer to it’s value:
var p *int
This above will be a pointer to an integer.
The &
generates a pointer to it’s operand:
i := 42
p = &i
So the long form would be:
var a int = 42
var b *int = &a
// RESULT
42
0x414020
The &
operator in front of a value indicates the memory address of the value:
fmt.Println(&a)
The *
operator in front of a value indicates the pointer’s underlying value:
fmt.Println(*p) // read i through the pointer p
*p = 21 // set 21 through the pointer p
Read a pointer’s value is known as dereferencing or indirecting
func main() {
i, j := 42, 2701
fmt.Println(&i)
fmt.Println(&j)
p := &i // point to i
fmt.Println(*p) // read i through the pointer
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j
}
// RESULTS:
0x414020
0x414024
42
21
73
In other languages you have something called pointer arithmethics, where you can do calculations on pointers:
func main() {
a := [3]int{1, 2, 3}
b := &a[0]
c := &a[1]
fmt.Printf("%v %p %p\n", a, b, c)
}
// RESULT
[1, 2, 3] 0x1040a124 0x1040a128
Note that c
is only 4 bytes higher than b
. It’s because in this system an integer is 4 bytes long.
So if you come from another language like C, you might want to do this:
c := &a[1] - 4
Now, by theory, you would be doing something like 0x1040a128 - 4
, which will point to b
.
But Go does not support this. If you reeeeally want to do this, use the unsafe
package by Go.
Pointer types:
type myStruct struct {
foo int
}
func main() {
var ms *myStruct
ms = &myStruct{foo: 42}
fmt.Println(ms)
}
// RESULT
&{42}
The response above means that ms
is holding the address of an object with a field 42.
We can also initialize a variable to point to a object with the new
function:
var ms *myStruct
ms = new(myStruct)
fmt.Println(ms)
- With the
new
func, you can’t initialize with object initialization, so ms would be&{0}
If you declare a pointer like var ms *myStruct
but don’t initialize, it will get a <nil>
value.
To set, or get a value from a pointer, you have to dereference it and do your thing:
var ms *myStruct
ms = new(myStruct)
(*ms).foo = 42 // SET A VALUE
fmt.Println((*ms).foo) // GET A VALUE
- Note we used
(*ms)
to make sure we are dereferencing onlyms
BUT we don’t need to do that in Golang. The compiler is smart enough to know we want to handle the underlying object, so Go will derefence the pointer:
ms.foo = 42
fmt.Println(ms.foo)
This will work.
Now some important facts:
- If you share slices through your app, it will all share the same underlying array. Because slices point to the first element of the underlying array.
- Another type with this behavior is a
map
, which also points to the underlying data.
be carefull when passing slices and maps in your app struct, arrays and other data structs don’t have this behavior
Structs
It’s a collection of fields, acessed by a dot
Struct gathers informaion together, related to a concept, and it’s very flexible because we can mix the types together. All other data structures have to have the same types (arrays, slices)..
type Doctor struct {
number int
name string
companions []string
}
func main() {
aDoctor := Doctor{
number: 3,
name: "Ricardo Han",
companions: []string{
"Stella",
"Jeff",
},
}
}
To get a specific value: fmt.Println(aDoctor.companions[1])
We can use positional fields when declaring structs:
aDoctor := Doctor{
3,
"Ricardo Han",
[]string{
"Stella",
"Jeff",
},
}
- This is not really recommended because any change into the Struct type can break the app
When declaring a Struct type, it follows the name rules for exported variables in Go: if is lowercase, is available only inside the file, if is Uppercase, it will be exported and available outside as well. So a exported Struct type would be like:
type Doctor struct {
Number int
Name string
Companions []string
}
Intead of declaring a Struct like the above, we can declare a anonymous Struct:
aDoctor := struct{name string}{name: "Ricardo Han"}
You can’t use it anywhere else, but you can use it where you declare it.
Structs are values, so when assign the original Struct to another variable and change it, the original Struct will remain unchanged:
aDoctor := struct{name string}{name: "Ricardo Han"}
anotherDoctor := aDoctor
anotherDoctor.name = "John Baker"
fmt.Println(aDoctor)
fmt.Println(anotherDoctor)
// {Ricardo Han}
// {John Baker}
So when you pass Structs around in your app, it will create a copy everytime. If you want to manipulate the same original data, use a pointer:
anotherDoctor := &aDoctor
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
fmt.Println(v.X)
}
We can access a struct through a struct pointer. You could access the struct values by doing (if p was a pointer):
(*p).X
, but in Go you can access values of a struct directly from a pointer as p.X
You can allocate new values for the struct also using the struct literals
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2}
v2 = Vertex{X: 1} // Y will be 0
v3 = Vertex{Y: 1} // X will be 0
)
Arrays
var a [10]int
is an array of ten integers
The array’s length in part of it’s type, so arrays in Go cannot be resized. This seems limiting, but don’t worry: Go provides a convenient way of working with arrays.
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
When you initialize an array, you don’t have to declare the size of the array, just pass [...]
and it will automatically fit the contents of the array:
grades := [...]int{1, 2, 3}
To check the length of array: len(grades)
It can be an array of arrays: identity := [2][2]int{ [2]int{1,2}, [2]int{3,4} }
So we are declaring an array with 2 array, that will have 2 integers inside: var identity [2][2]int
In Go, the arrays are actuall values, they are not passed by reference as in Javascript.
So if you declare and array a
, and then assign that array to b
, you will have two different array.
If you don’t want this behaviour, you can use pointers to point to the same data:
a := [...]int{1, 2, 3}
b := &a
Now b
is pointing to the same data as a
Slices
Slices offers a flexible view into element of an array.
A slice is formed by two values: a low and high bound. eg: a[low:high] int
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[0:4]
fmt.Println(s)
}
// RESULT
[2 3 5 7]
In the example above, s
is a slice that gets the 0 index until (but not including) the 4° index.
- Note that slices are like references to the array
- Slices don’t store any data, just describe a section of the array
- So changing a slice will change it’s underlying array
- And also other slices that share the same data will change too!
- So they are reference types, they refer to the same underlying data
a := []int{1, 2}
b := a
a
and b
reference to the same array
We can create slice literals by declaring the same as an array, but without the length:
Array literal: [3]bool{true, true, false}
Slice literal: []bool{true, true, false}
- Note: This creates an array and then builds a slice that references it.
When creating a slice, you can omit the low and high value and use their defaults instead. The defaults are 0 for low and the slice length for high:
Array is like: var a [10]int
Slices are like:
a[0:10]
a[:10]
a[0:]
a[:]
// they are all the same!
The many ways of creating a slice:
func main() {
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
b := a[:] // create a slice with all the element of a
c := a[3:] // create slice from 4th element (index 3) until the end (4 to 10)
d := a[:6] // create slice until 6th element (index 5) (1 to 6)
e := a[3:6] // from index 3 (4th element) until 6th element (4 to 6)
}
It’s confusing because the numbers inside have different meaning. You can think of to
as including the index and from
excluding the index. So a[3:6]
includes element of index 3 until (but excluding) element of index 6.
A slice has length and capacity.
The length of a slice is the number of elements it contains. len(s)
The capacity of a slice is the size of the backing array, is the number of elements in the underlying array, counting from the first element in the slice. cap(s)
We can extends the slice length by re-slicing it, providing a bigger high value.
The zero value of a slice is nil
Slices can be created with make
function. To create dinamically-sized-array, we can use it.
The 1° parameter is the type and the second is the size of the slice:
a := make([]int, 3)
The make function allocates a zeroed array and returns a slice that refers to that array:
a := make([]int, 5) // len(a)=5
To specify a capacity, we pass a 3° arg:
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4
It’s usefull to have different capacity because, unlike array, slices can have a flexible size, adding and removing elements during their lifecycle.
If we declare a empty slice, and then begin appending data inside, Go will copy the underlying array to a new array with the content size. So doing this every time can be expensive for the memory. It’s better to use a make
function, define a bigger capacity and then begin assigning data to the slice.
Slices can contain any type, including other slices:
func main() {
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
}
To append new elements to a slice, we can use the built-in append
function
func append(s []T, vs ...T) []T
-
s
is a slice with type T -
the rest are T values to append to
s
-
it returns a slice with type T, containing all element
-
If the backing array of s is too small to fit all the given values a bigger array will be allocated. The returned slice will point to the newly allocated array.
func main() {
var s []int
printSlice(s)
// append works on nil slices.
s = append(s, 0)
printSlice(s)
// The slice grows as needed.
s = append(s, 1)
printSlice(s)
// We can add more than one element at a time.
s = append(s, 2, 3, 4)
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
There is something like spread operator in Go, where you can use to concatenate slices for example:
a := []int{}
a = append(a, []int{1, 2, 3}...)
// RESULT
[1, 2, 3]
Above, we are appending a slice of 3 integers into the a
slice, using the spread operator
Another operations for slices:
- Remove first element:
a := []int{1, 2, 3, 4}
b := [1:]
fmt.Println(b)
// [2, 3, 4]
- Remove last element:
a := []int{1, 2, 3, 4}
b := [:len(a) - 1]
fmt.Println(b)
// [1, 2, 3]
- Remove element in the middle:
a := []int{1, 2, 3, 4, 5}
b := append(a[:2], a[3:]...)
fmt.Println(b)
// [1, 2, 4, 5]
- Make sure you know you are referencing the underlying array by making this slice operation
Map
A map maps keys to values.
statePopulations := map[string]int {
"California": 39250017,
"Texas": 2799328,
"Florida": 846626,
}
- The keys for a Map has to be able to be tested for equality (strings, boolean, numerics, pointers, arrays, structs..) Some data types cannot (slices, map and another functions)
The make function returns a map of the given type, initialized and ready for use.
To get a value from the map:
statePopulations["Ohio"]
To add a key/value:
statePopulations["Georgia"] = 1083828
The order of a map is not guarantee, it not follows the same order of the entries.
To delete something from a map:
delete(statePopulations, "Georgia")
To check if there is a key inside the Map, we can use the comma okay syntax:
_, ok := statePopulations["Ohio"]
- If the key exists, it will return true, if don’t exist, return false.
- The
_
is a read-only value - There is nothing magic about
ok
variable, it is just a convention
To see the length:
len(statePopulations)
When you pass a Map somewhere, it is passed by reference! So you are manipulating the same Map structure.
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
}
// RESULT
{40.68433 -74.39967}
Map literals are like struct literals, but the keys are required.
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
func main() {
fmt.Println(m)
}
// RESULT
map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
We could’ve ommited the types from the elements of the literal:
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
To insert an element in a map m
:
m[key] = elem
Retrieve an element:
elem = m[key]
Delete an element:
delete(m, key)
Test if key is present, with a two-value:
elem, ok = m[key]
OR
elem, ok := m[key]
If key
is in the map, ok
is true
and elem
is the value of the element. Else, ok
is false
and elem
is zero-value
Function Values
Functions can be values too, passed around like arguments, return values, in variables…
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}
Function Closures
Go functions may be closures, which is a function value that references variables from outside its body.
For example, this function adder
return a closure function (because it references the sum
variable that lives outside it’s scope):
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
Methods
Go does not have classes, but it has methods.
Methods are just function with a special receiver argument, between the func
keyword and the method name:
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
Here there is a method Abs
with a receiver v
of type Vertex
Since methods are just functions, here is Abs
written as a regular function:
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(Abs(v))
}
You can define non-struct types in methods too:
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
- You can only declare a method with a receiver whose type is defined in the same package as the method.
Methods can have a pointer receiver, with the syntax of *T
for a type T.
A pointer receiver modifies the value to which the pointer points (since is a pointer, is the same place in memory)
With a value receiver (not pointer), the method operates in a copy of the value, not the value itself.
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}
// RESULT 50
In the example above, with (v *Vertex)
being a pointer, the values of vertex change.
Methods with pointer receivers can take either a pointer or a value as argument, because Go will interpret thats as a pointer:
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(2)
}
So the point is: in Methods, if you expect a pointer receiver and send a value, the value will be understood as a pointer. The reverse is also true: methods with value receivers take either a value or a pointer as the receiver when they are called.
There are two reasons to choose a pointer receiver for the method:
- The method can modify the value that its receiver points to
- Avoid copying the value on each method call. This can be more efficient if the receiver is a large struct, for example
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := &Vertex{3, 4}
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
// RESULT
Before scaling: &{X:3 Y:4}, Abs: 5
After scaling: &{X:15 Y:20}, Abs: 25
Embedded
Go does not have inheritance, but it has composition
So supposed we have two Structs, Animal and Bird.
type Animal Struct {
Name string
Origin string
}
type Bird Struct {
SpeedKPH float32
CanFly bool
}
In other OOP languages, ‘Bird’ would BE and ‘Animal’. But in Go, is more like Bird HAS Animal properties
We use composition for that, it would be like this:
type Bird Struct {
Animal
SpeedKPH float32
CanFly bool
}
func main() {
b := Bird{}
b.Name = "Emu"
b.Origin = "Australia"
b.SpeedKPH = 48
b.CanFly = false
}
Through composition, now Bird struct has Animal properties
If you initialize the Bird
with the values, you have to be aware of the Animal
struct, and declare it like this:
b := Bird{
Animal: Animal{Name: "Emu", Origin: "Australia"},
SpeedKPH: 48,
CanFly: false,
}
Generally you would use Interfaces
for interchangeble data between types. But Embedding
is usefull when you have a baseComponent and want to put the baseComponent types into other structures.
Tag
Used to describe information about a Struct field. Suppose you have to validate a input field, Tags are great for this:
import (
"fmt"
"reflect"
)
type Animal struct {
Name string `required max:"100"`
Origin string
}
func main() {
t := reflect.TypeOf(Animal{})
field, _ := t.FieldByName("Name")
fmt.Println(field.Tag)
}
// RESULT
required max:"100"
The format of a Tag is to have backticks (“) and space-delimited key-value pairs (can be only a key also)
The Tag itself is useless, it’s just a string. You have to get this information, with the reflect
package.
- First thing is to get the type of the Object you are working with:
t := reflect.TypeOf(Animal{})
- Then you grab a field from that type using
field, _ := t.FieldByName("Name")
- Then you get the Tag by:
fmt.Println(field.Tag)
Defer, Panic and Recover
Defer is how we can invoke a function but delay it’s execution. Panic is how a Go application can stop running, either the Go runtime trigger that as well as we can do it. Recovery is when the program panic, but we save the program so it does not bail out completely.
Defer
func main() {
fmt.Println("Hello")
defer fmt.Println("Middle")
fmt.Println("Bye")
}
// RESULT
Hello
Bye
Middle
Go defer the function call and call it when the outer function finish, but before exit/return.
func main() {
defer fmt.Println("Hello")
defer fmt.Println("Middle")
defer fmt.Println("Bye")
}
// RESULT
Bye
Middle
Hello
- Take care with loops. If you are opening stream in loops, don’t use defer like this, because it will keep all the streams open until it ends execution. You can call another function with defer in it to close the stream at each loop. Or close the stream manually without defer.
Defer is last in first out.
Now a common example when using defer, to close streams:
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
res, err := http.Get("http://www.google.com/robots.txt")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
robots, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", robots)
}
We are making a request, opening a stream of bytes (robots
) and printing it.
But meanwhile, we are also defering and closing the res.Body
, so it will close after the function executes.
- Remember that
defer
always invoke the code after the function is done.
a := "start"
defer fmt.Println(a)
a = "end"
// RESULT
start
You might think that the code above should print “end”, because the fmt.Println was defered. BUT, the defer get the arguments at the time it is declared. That’s why it prints “start”.
Panic
We don’t have exeptions in Go. But there are situations where a Go app cannot continue, so we use panic
.
a, b := 1, 0
ans := a / b
fmt.Println(ans)
// RESULT
panic: runtime error: integer divide by zero
In this example, the Go runtime panic. But we also can call panic ourselves:
fmt.Println("Start")
panic("something bad happened")
Now let’s build a simple web server and panic in case there is an error:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello Go!"))
})
err := http.ListenAndServe(":8080", nil)
if err != nil {
panic(err.Error())
}
Now if, for example, we try to use the same PORT (8080), the app would give an error, but now we are using panic and it will shut down.
It’s important to note that defer it’s called BEFORE panic. So if we use defer to close a stream or something,it will close before panicing.
The order of execution of a function is:
- Execute the function
- Execute any defers
- Execute any panic
- Return the value
But if we deal with a anonymous defer function, things roll a bit different:
func main() {
fmt.Println("Start")
panicker()
fmt.Println("end")
}
func panicker() {
fmt.Println("Its about to panick")
defer func() {
if err := recover(); err != nil {
log.Println("Error:", err)
}
}()
panic("something bad happened")
fmt.Println("Done panicking")
}
In the code above, there is a anonymous function which is self invoking itself. Inside, the recover()
return nil if the app is not panicing. If not null (if the app is panincing), will print the error.
- recover is only useful inside defer functions
So the panicker func will panic(), and then the defer func will be called, it will recover from the panic, print the error, and then go back to the main() func and print “end”.
You can re throw a panic inside the defer function, so the recover function don’t make your app follow the flow. This is usefull when you don’t know the error and wants to see what’s hapening:
defer func() {
if err := recover(); err != nil {
log.Println("Error:", err)
panic(err)
}
}()
Interfaces
Interface is a type, just like Structs or other types. But different from Structs, which define data inside them, interfaces describe behavior. So we will be storing meta-definitions
We created a method Write
, which accepts a slice of bytes as args, and return an int and error
Now to implement this interfacem let’s define a struct:
func main() {
var w Writer = ConsoleWriter{}
w.Write([]byte("Hello Go!"))
}
type Writer interface {
Write([]byte) (int, error)
}
type ConsoleWriter struct {}
func (cw ConsoleWriter) Write(data []byte) (int, error) {
n, err := fmt.Println(string(data)) // convert the bytes into a string and print to console
return n, err
}
In Go we don’t explicity implement interfaces, it’s implicit by defining a method Write (same name as the Write method inside the interface) in the ConsoleWriter struct.
The advantage of interfaces is that, here:
func main() {
var w Writer = ConsoleWriter{}
w.Write([]byte("Hello Go!"))
}
The w
variable is an intance o Writer
interface, but it doesn’t know whats it’s being written to. It could be and TCPWriter, fileWriter or any other Writer (not just ConsoleWriter).
This line w.Write([]byte("Hello Go!"))
just knows it can call a Write
method, but does not know to where is calling (this is the responsability of the implementation, in this case is calling to ConsoleWriter
method.)
To follow a name convention, if your interface has only one method, use the name with er
like Writer
:
type Writer interface {
Write([]byte) (int, error)
}
Name your interface by what it does.
You can implement interfaces to any type you can add methods. An example of an interface to an Int
type:
func main() {
myInt := IntCounter(0)
var inc Incrementer = &myInt
for i := 0; i < 10; i++ {
fmt.Println(inc.Increment())
}
}
type Incrementer interface {
Increment() int
}
type IntCounter int // we define a type alias for the int type, so we can add methods to it
// This below is the implementation for the Incrementer interface
func (ic *IntCounter) Increment() int {
*ic++
return int(*ic)
}
We are defining a method to our custom type IntCounter
.
How to compose interfaces together:
type Writer interface {
Write([]byte) (int, error)
}
type Closer interface {
Close() error
}
type WriterCloser interface {
Writer
Closer
}
You have to implement all the methods of the nested interfaces to be able to have the compose interface (WriterCloser in this case)
We also can type convert interfaces, if we want to get to the underlying type in case we want to work with them directly:
var wc WriterCloser = newBufferedWriterCloser()
bwc := wc.(*BufferedWriterCloser)
Inside the parens, we put the type we want to convert to. We can use the comma-ok, o if the type assertion fails, the app does not panic:
r, ok := w.(io.Reader)
if ok {
fmt.Println(r)
} else {
fmt.println("Convertion failed")
}
Now about empty interfaces, it’s just a empty interface define on the fly, with no methods assign to it.
var myObj interface{} = newBufferedWriterCloser()
The nice thing is that evertyhing can be casted to the empty interface. This can be usefull when working with a lot of thing that are not type compatible.
But in this case, myObj
has no methods so we cannot do anything.
You can try to typecast the variable:
if wc, ok := myObj.(WriterCloser); ok {}
We’ve tried to typecast myObj with the comma_ok syntax. If return an ok, enter in the if statement.
So empty interfaces are useful as a middle step. You first declare it, and then you choose what to do with it.
Empty interfaces are often used with switch cases
:
var i interface{} = 0
switch i.(type) {
case int:
fmt.Println("i is an integer")
case string:
fmt.Println("i is aa string")
default:
fmt.Println("I don't know what i is")
}
Now about pointers and interfaces. When implementing an interface, if one of your methods has a pointer receiver, like this:
type WriterCloser interface {
Writer
Closer
}
type myWriterCloser struct {}
func (mwc *myWriterCloser) Write(data []byte) (int error) {
return 0, nil
}
You have to implement the interface as a pointer: var wc WriterCloser = &myWriterCloser{}
Best practices for using interfaces
- Prefer many small interfaces than big monolitics interfaces
- Design functions and methods to receive interfaces whenever possible
- If you need the underlying data, that’s not possible. But if you are working with behavior, try to define interfaces
Resume of interfaces, because it’s really hard man!
First define the interface. It is a type, with the name of the interface and the keyword interface
.
Inside there are the methods of the interface, the behaviour of that interface:
type Writter interface {
Write([]byte) (int, error)
}
Then you implement the interface by implicit defining an interface method into a type:
type ConsoleWriter struct{}
func (cw ConsoleWriter) Write(data []byte) (int, error) {
n, err := fmt.Println(string(data))
return n, err
}
Now you can use this type, with the methods of the interface:
func main() {
var w Writer = ConsoleWriter{}
w.Write([]byte("Hello Go!"))
}
- Note the type of the
w
variable is the interface typeWriter
.
Go Routines
Enable us to work with concurrency easily in Go apps.
func main() {
go sayHello()
time.Sleep(100 * time.Millisecond)
}
func sayHello() {
fmt.Println("Hello World")
}
That go
keyword in front of the function call will start a green thread and run the say hello in that thread.
In this example, the main
func runs in it’s own goroutine already. So by starting another goroutine, we have to delay the exit process of the main
, so the other goroutine can start and call our sayHello
.
Most programming languages use OS (Operation System) threads. This means they use an individual call stack dedicated to the execution to the code. They tend to be very expansive, that’s why we have thread pooling and other stuffs. In Go this works a bit different.
Green thread means that, intead of using those heavy threads, we are using an abstraction called goroutine. Inside the Go runtime, we have a scheaduler that will math the goroutines with the OS threads available. We don’t have to deal with the lower level threads, just with the abstraction (goroutine) The benefit is that goroutines can start with very small stack spaces, so they are cheap to create and destroy.
We can have thousands or more goroutines running in an app, with no problem at all.
Another example, passing arguments into a goroutine. Although Go has the concept of Closures, it’s not good to pass an outer variable directly into the goroutine function. It’s better to use the following pattern:
func main() {
var msg = "Hello"
go func(msg String) {
fmt.Println(msg)
}(msg)
msg = "Goodbye"
time.Sleep(100 * time.Millisencod)
}
// RESULT
Hello
Now, we are passing the msg
as an argument, so passing by value. That means the goroutine function will no depend on the outer variable anymore. So it will print “Hello”, and will not be change and print “Goodbye”
These examples ae not good because they use the time.Sleep
, which is bad practice.
An alternative is to use the sync.WaitGroup{}
, they will sync your goroutines.
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
var msg = "Hello"
wg.Add(1)
go func(msg string) {
fmt.Println(msg)
wg.Done()
}(msg)
msg = "Goodbye"
wg.Wait()
}
The WaitGroup in wg
starts at 0.
The wg.Add(1)
indicates we have one more goroutine to run.
The wg.Wait()
waits for the goroutines to be executed all, waits for the WaitGroup be 0 again so it can exit.
The wg.Done()
decrements the WaitGroup by -1, so it indicates the goroutine is done.
How about when launching multiple goroutines together, that depends on one another? Take this code for example:
var wg = sync.WaitGroup{}
var counter = 0
func main() {
for i := 0; i < 10; i++ {
wg.Add(2)
go sayHello()
go increment()
}
wg.Wait()
}
func sayHello() {
fmt.Printf("Hello #%v\n", counter)
wg.Done()
}
func increment() {
counter++
wg.Done()
}
We should expect the result to be predictable, by printing “Hello #1” and so on. But in this case, the goroutines are in race condition, rying to finish the job as soon as possible. We have to somehow sync them.
We could’ve used WaitGroup
again to this issue, but let’s taks about another approach with sync.RWMutex
.
Mutex
is basically a lock to your app. It is either locker or unlocked.
If the app is locked and someone tries to manipulate that value, it has to wait until it gets unlocked. We can then, protect our code so that only one entity can manipulate the data at a single time. It protect from race conditions.
RXMutex
means ReadWrite Mutex. It means everybody can read the data but only one can modify (write) at a single time. And if something is reading the data, we can’t write to it.
So we have a infinite number of readers and only one writer. The writer has to wait until every reader reads the data, and only then it will be able to write. When writing to the data, it locks the data and no one can read or write to it, until it unlocks.
var wg = sync.WaitGroup{}
var counter = 0
var m = sync.RWMutex{}
func main() {
for i := 0; i < 10; i++ {
wg.Add(2)
m.RLock()
go sayHello()
m.Lock()
go increment()
}
wg.Wait()
}
func sayHello() {
fmt.Printf("Hello #%v\n", counter)
m.RUnlock()
wg.Done()
}
func increment() {
counter++
m.Unlock()
wg.Done()
}
So we are using the m.RLock()
to lock the read in the sayHello
func. So after it prints the phrase, we unlock the read with m.RUnlock()
.
Then, in the icnrement
function we are modifiying the data so we have to call m.Lock
before, then we increment the value (counter++
) and the call m.Unlock()
.
- Note we are calling the
m.RLock()
andm.Lock()
before we call the functions. It’s because they have to be outside the goroutines to be able to lock those functions.
Best practices with GoRoutines
- Don’t create goroutines in libraries (let the consumer of the library to decide when using goroutine)
- Know how it’s going to end, to avoid memory leaks.
- Check for race conditions at compile time
- For this, use the flag —race, like:
go run --race src/main.go
- For this, use the flag —race, like:
Channels
The majoroty of languages out there were designed with single processing core in mind. So it was hard to do concurrency and paralellism. Go is born in multiprocessing world.
Channels can be used to pass data between goroutines, and avoid race conditions and memory share problems.
Almost everytime you work with channels, it will be in the context of goroutines.
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int)
wg.Add(2)
go fun() {
i := <- ch
fmt.Println(i)
wg.Done()
}()
go func() {
ch <- 42
wg.Done()
}()
wg.Wait()
}
We created the WaitGroup in order to sync the goroutines, to make sure our main goroutine waits until other routines finish. And then we will use channels to syncronize the data flow between them
Channels are created with the make
function.
First we define the chan
keyword and then the data type that will flow through the channels.
You can only pass data from the type you defined.
We defined 2 item to the WaitGroup, so 2 goroutines will be created. The first goroutine will receive data from the channel. The second will send data.
So this syntax ch <- 42
it’s passing the data 42
into the channel. Think like the arrow <-
is pointing to where the data should go, so 42 should go into the channel.
The same way, if we want to get data from the channel: i := <- ch
We are passing a copy of the data to the channel, so if you pass the data and the modify the data, the modification will not happen inside the channel:
go func() {
i := 42
ch <- i
i := 17
wg.Done()
}()
// The goroutine receiver will print out 42!
Another thing to keep in mind is:
ch := make(chan int)
go func() {
i := <- ch
fmt.Println(i)
wg.Done()
}()
for j:= 0; j < 5; j++ {
wg.Add(2)
go func() {
ch <- 42
wg.Done()
}()
}
wg.Wait()
In the example above, we have one channel, only one receiver channel and 5 sender channels (inside the for loop)
This will give us a deadlock
error. Because the goroutine will send data (42) into the channel for the first time, and the receiver will listen one time and then close (wg.Done()).
Then, the secondo time the receiver try to send data into the channel, nobody will listen to that, since there was only one recevier and it is already done.
So the takeway is: when sending data into the channel, something has to listen to that data.
Channels can be receiver AND senders, like the example below:
go func() {
i := <- ch
fmt.Println(i)
ch <- 27
wg.Done()
}()
go func() {
ch <- 42
fmt.Println(<-ch)
wg.Done()
}()
Send the 42 into the channel, the upper function reads it, and then pass 2 into the channel. Then the bottom function reads it.
We can specify the goroutines to be only senders or receivers, by passing arguments into the function:
ch := make(chan int)
go func(ch <-chan int) {
i := <- ch
fmt.Println(i)
wg.Done()
}(ch)
go func(ch chan <- int) {
ch <- 42
wg.Done()
}(ch)
ch <-chan int
it means data is flowing out of the channel. (receiving)
ch chan <- int
means we are sending data into the channel (chan) (sending)
Before we ran into the issue of only one receiver and 5 senders, which caused the deadlock
problem.
We can solve that by using buffers, passing a second parameter when creating a channel: ch := make(chan int, 50)
Buffers are used when your sender or receiver needs more time to process the data. For example, when a sender has too much information to send, the receiver has to deal with that somehow. That’s when we need buffers to put the data while the receiver process the incoming data.
A buffer will block the channel, until the receiving (or sender) is able to handle the data.
The best way to process the big incoming data with buffers is with a looping construct, using range
, where we range the channel:
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int, 50)
wg.Add(2)
go func(ch <-chan int) {
for i := range ch {
fmt.Println(i)
}
wg.Done()
}(ch)
go func(ch chan<- int) {
ch <- 42
ch <- 27
wg.Done()
}(ch)
wg.Wait()
}
When you range
a channel, the first value it’s actually the value. If you range a slice for example, the first value would be the index.
Now if we run this code, we print the 2 values but we still deadlock the application. But the deadlock now is in the receiver, because it doesn’t know how to exit, it still waits for more data to process.
So to handle that we can call the close(ch)
in the sender, right after the second message ch <- 27
, to the app knows we are done with the channel and can close it.
go func(ch chan<- int) {
ch <- 42
ch <- 27
close(ch)
wg.Done()
}(ch)
Once you close the channel, you can’t reopen that, and you can’t send any more messages there.
On the receive side, you can use a regular for loop without the range, and close the channel manually using the comma-okay syntax:
go func(ch <- chan int) {
for {
if i, ok := <- ch; ok {
fmt.Println(i)
} else {
break
}
}
wg.Done()
}
Above, we break the loop if ok
is false.
Select Statements
We should always know how the goroutine will shutdown.
We can use defer
to close the goroutine and channel, but we will use another alternative called select statements.
tip: var doneCh = make(chan struct{})
struct{}
has zero allocation of memory. It is used just to sign that we send something through the channel, without actually passing any data. It calls signal channel, and it’s very common. Instead of passing a bool
into the channel to know if it’s working, just use this syntax.
To use the select statement to close the channel:
package main
import (
"fmt"
"time"
)
const (
logInfo = "INFO"
logWarning = "WARNING"
logError = "ERROR"
)
type logEntry struct {
time time.Time
severity string
message string
}
var logCh = make(chan logEntry, 50)
var doneCh = make(chan struct{})
func main() {
go logger()
logCh <- logEntry{time.Now(), logInfo, "App is starting"}
logCh <- logEntry{time.Now(), logInfo, "App is shutting down"}
time.Sleep(100 * time.Millisecond)
doneCh <- struct{}{}
}
func logger() {
for {
select {
case entry := <- logCh:
fmt.Printf("%v - [%v]%v\n", entry.time.Format("2006-01-02T15:04:05"), entry.severity, entry.message)
case <- doneCh:
break
}
}
}
The select
statement is going to block until a message arrive in one of the channels it is listening for (in this case, logCh
and doneCh
)
At the end of the main
function, we passed a message to our doneCh
, just a signal without data.
struct{}{}
is defining the struct with no fields (struct{}
) and initializing that struct ({}
)
This is a common case when you have to monitor the channels and need the channel to terminate somehow.
You can have a default
case in the select statement, but the it will be unblocked since everything that is not in the case statements will be in the default.
So if you wanna block the select statement and only run if a case is matched (in case there is something in the channels), don’t use the default.