Go Crash Course
Based on Learn Go Programming - Golang Tutorial for Beginners. Almost 7 hours long. All credits to freeCodeCamp, what I have below are just my own notes after watching the video, plus some other minor notes from official source (Golang doc).
Intro
Dowload Go binaries from Golang site
Set env vars: GOROOT and GOPATH
- Avoid re-setting GOROOT if possible (just install Go on default paths)
- Usual values:
Go binaries:
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
Go library sources and my own sources
export GOPATH=/home/raulbajales/golib
export PATH=$PATH:$GOPATH/bin
export GOPATH=/home/raulbajales/mygocode
Folder structure you need to create your own workspace:
$ cd /home/raulbajales/gocode
$ mkdir src
$ mkdir bin
$ mkdir pkg
IDE:
- Visual Studio Code
- Plugins:
Go
by Go Team at Google with all dependencies
Hello World Go Edition
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
Ways to create an executable:
Assuming we have a file named Main.go
with package main
containing a func main()
then there are 2 ways to execute it:
- Just run it:
$ cd /home/raulbajales/mygocode/src
$ go run Main.go
- Build binary in
/home/raulbajales/mygocode/src
folder:
$ cd /home/raulbajales/mygocode/src
$ go build Main.go
$ ./Main
Variables
Declare then assign:
var i int
i = 10
Declare and assign at the same time:
var j int = 15
Declare and assign at the same time avoiding type (cannot be used at a package level):
i := 5
Declare at package level using a var block
:
var (
i int = 10
j float32 = 5
k String = "test"
)
Shadowing
Re-declaring a variable existent in current scope is not allowed, this is wrong (this won't compile as we are trying to re-declare variable i
):
func main() {
var i in = 10
i := 15
}
You can't declare a variable twice in the same scope. But you can declare in inner sopes wit same names as in outer scopes. In the next example i
is declared twice, but in different scopes. The variable with innermost scope takes precedence.
var i int = 10
func main() {
fmt.Println(i)
var i int = 20
fmt.Println(i)
}
Output
10
20
Naming conventions
- Lowercase variables are scoped to
package
orblock
scopes
// Here variable i has package scope
var i int = 10
func main() {
// Here variable j has block scope
var j int = 15
}
- Uppercase variables are scoped to
global
scope (applies whenever your package gets imported)
Type casting
Needs to be done explicitly (Go won't do implicit data type conversion), by using the target type as a function (wrapping the variable to cast with the new type we want).
func main() {
var i int = 10
var j float32 = float32(i)
}
string
is a particular case since this is handled internally as a stream of bytes. So to convert a number into a string
we need to use strconv
package
package main
import (
"fmt"
"strconv"
)
func main() {
var i int = 10
var j string = strconv.Itoa(i)
fmt.Println(j)
}
Output
10
But to convert a byte
(an alias for int8
) you can use string as a function, to get the ASCII char as a string
package main
import (
"fmt"
)
var b byte = 65
func main() {
fmt.Println(string(b))
}
Output
A
Primitives
- Booleans
var b1 bool = true
var b2 bool = false
- Numbers
// Signed Integers:
var i int = 10
// Specifying number of bytes:
var i8 int8 = 10
var i16 int16 = 10
var i32 int32 = 10
var i64 int64 = 10
// Unsigned Integers:
var u uint = 10
// Specifying number of bytes:
var u8 uint8 = 10 // "byte" is alias for "uint8"
var u16 uint16 = 10
var u32 uint32 = 10
var u64 uint64 = 10
// Floating point:
var f32 float32 = 10
var f64 float64 = 10
// Complex numbers:
var c64 complex64 = 1 + 2i
var c128 complex128 = 2 + 5i
// ... with complex functions:
var comp complex64 = complex(5, 12)
var re = real(c64) // Takes real part
var im = imag(c128) // Takes imaginary part
Go also has bitwise/bitshifting operators that only operates on integers:
& bitwise AND
| bitwise OR
^ bitwise XOR
&^ bit clear (AND NOT)
>> Bit shift to the left
<< Bit shift to the right
- Text
A string is a collection of bytes, where a byte is type uint8
(where byte
is also an alias for this type).
package main
import (
"fmt"
)
func main() {
// string (immutable value type)
var s string = "Hello!"
// Converting it to a slice of bytes
var b []uint8 = []byte(s)
// rune = alias for int32 to store UTF32 characters
var r rune = '🤡'
fmt.Println(b, r, string(r))
}
Output
[72 101 108 108 111 33] 129313 🤡
Constants
- Scoping and naming conventions: Same as in variables
- As in variables, they can be shadowed
- Must to be calculable/assignable at compile time
- Cannot change the value in runtime
const myConst int = 10
Enumerated constants (iota)
Special value iota
is set to the initial integer (0
) and scoped to block. All subsequent values will be set incrementally. In the example below, (a == a2 && b == b2 && c == c2)
is true
package main
import "fmt"
func main() {
const (
a = iota
b
c
)
const (
a2 = iota
b2
c2
)
// Use _ to avoid using 0 as initial value:
const (
_ = iota
a3
b3
)
// An initial operation (even bitwise/bitshifting operators)
// can be applied.
// Here a4 is set to 6, and so on:
const (
_ = iota + 5
a4
b4
)
fmt.Printf("a = %v, b = %v, c = %v, a2 = %v, b2 = %v, c2 = %v, a3 = %v, b3 = %v, a4 = %v, b4 = %v", a, b, c, a2, b2, c2, a3, b3, a4, b4)
}
Output
a = 0, b = 1, c = 2, a2 = 0, b2 = 1, c2 = 2, a3 = 1, b3 = 2, a4 = 6, b4 = 7
Arrays
- Mutable collections (with fixed size, zero based index) of items with same type.
- Order of items is guaranteed
- Its a value type meaning: whenever you assign an array to new variable you are actually copying the entire array creating a new one
Creating an array
package main
import "fmt"
func main() {
// Declare and set initial values:
var ar1 [3]int = [...]int{10, 15, 20}
// Declare and set values after that:
var ar2 [3]string
ar2[0] = "Lisa"
fmt.Printf("ar1 is %#v and ar2 is %#v", ar1, ar2)
}
Output
ar1 is [3]int{10, 15, 20} and ar2 is [3]string{"Lisa", "", ""}
Some array operations:
package main
import "fmt"
func main() {
// Declare and set initial values:
var ar1 [3]int = [...]int{10, 15, 20}
// Get array length:
l := len(ar1)
// By assigning a new variable to an existing array
// we are effectively copying the entire original array
// Here ar2 is an entirely new array where its content
// it's equal to the content in ar1
var ar2 = ar1
ar2[0] = 5 // Note that here only ar2 is modified
fmt.Printf("ar1: %#v, ar1 length: %v, ar2: %#v", ar1, l, ar2)
}
Output
ar1: [3]int{10, 15, 20}, ar1 length: 3, ar2: [3]int{5, 15, 20}
Slices
- Reference type
- You don't need to know the size beforehand
- Internally backed by an array
Creating a slice
package main
import "fmt"
func main() {
// Declare and set initial values:
var a1 []int = []int{10, 15, 20}
// Slices are reference types, so assigning them to a new variable
// does not copy the content, instead it maintains a reference
// to the original one
var a2 = a1 // Here a2 it's a reference to a1
a2[0] = 5 // Note that here both arrays changes
fmt.Printf("a1: %#v, a2: %#v", a1, a2)
}
Output
a1: []int{5, 15, 20}, a2: []int{5, 15, 20}
Some slice operations
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
b := a[:] // slice of all elements
c := a[3:] // slice from 4th element to the end
d := a[:6] // slice from the beginning to 6th element
e := a[3:6] // slice from 4th to 6th elements (1st number is inclusive, 2nd is exclusive)
fmt.Printf("a = %v, a[:] = %v, a[3:] = %v, a[:6] = %v, a[3:6] = %v", a, b, c, d, e)
}
Output
a = [1 2 3 4 5 6 7 8 9], a[:] = [1 2 3 4 5 6 7 8 9], a[3:] = [4 5 6 7 8 9], a[:6] = [1 2 3 4 5 6], a[3:6] = [4 5 6]
Make function
package main
import "fmt"
func main() {
// This will create a slice of 3 elements initialised to 0
a := make([]int, 3)
fmt.Printf("a = %#v", a)
}
Output
a = []int{0, 0, 0}
Append function
package main
import "fmt"
func main() {
a := make([]int, 3)
a = append(a, 1, 2, 3)
fmt.Printf("a = %#v", a)
}
Output
a = []int{0, 0, 0, 1, 2, 3}
Concatenate slices
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
b := []int{4, 5, 6}
c := append(a, b...) // The ... works as a spread operator
fmt.Printf("c = %#v", c)
}
Output
c = []int{1, 2, 3, 4, 5, 6}
Maps and Structs
Maps
- Its a reference type.
- Its mutable.
- Key must be testable for equality.
- Primitives and arrays are valid key types.
- Entry order is not guaranteed.
Creating a map
package main
import "fmt"
func main() {
// One-liner to create and set values:
var m1 map[string]int = map[string]int{
"key1": 1,
"key2": 2,
}
// Just create an empty map
var m2 map[string]int = make(map[string]int)
fmt.Println(m1, m2)
}
Output
map[key1:1 key2:2] map[]
Inspecting and manipulating a map
package main
import "fmt"
func main() {
var m1 map[string]int = map[string]int{
"key1": 1,
"key2": 2,
}
// Get a value for a key:
k1 := m1["key1"]
// Modify value for a key (or add a new key/value pair):
m1["key1"] = 10
m1["key3"] = 3
// Delete an entry by providing a key:
delete(m1, "key1")
// Getting a value for a key fails silently if key is not found,
// here is a way to know if key is found or not:
v, keyExists := m1["nonExistentKey"] // here keyExists is false
// Get amount of entries in a map:
l := len(m1) // here l is 2
fmt.Printf("Map m1 contains %v \n", m1)
fmt.Printf("Value for key 'key1' is %v \n", k1)
fmt.Printf("Value v for key 'nonExistentKey' is %v, and boolean keyExists for the same key is %v \n", v, keyExists)
fmt.Printf("Length for map m1 is %v \n", l)
}
Output
Map m1 contains map[key2:2 key3:3]
Value for key 'key1' is 1
Value v for key 'nonExistentKey' is 0, and boolean keyExists for the same key is false
Length for map m1 is 2
Structs
- Used to describe data
- Its a collection type (same as array, slice and map)
- Its a value type (not a reference type)
- Usual variable naming conventions and scoping applies
- Field names must be capitalized to be exposed globally
Declaring a struct type and creating an instance
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
aPerson := Person{
Name: "John",
Age: 20,
}
// One-liner to create and set an anonymous struct:
aCar := struct{ brand string }{brand: "Toyota"}
// Get value for a given field in a struct:
name := aPerson.Name
fmt.Printf("aPerson: %#v, aCar: %#v, name: %#v", aPerson, aCar, name)
}
Output
aPerson: main.Person{Name:"John", Age:20}, aCar: struct { brand string }{brand:"Toyota"}, name: "John"
Composition (thru Embedding)
Go does not support inheritance, but support composition thru embedding a struct properties into another struct. Applies to interfaces too.
package main
import "fmt"
type Animal struct {
Name string
}
// Now here I embed Animal properties into Bird struct:
type Bird struct {
Animal
CanFly bool
}
func main() {
// From now on any instance contains also properties
// from Animal struct:
b := Bird{}
b.Name = "Duck"
b.CanFly = true
// But to declare and create a Bird in one step,
// the internal Animal structure is needed:
c := Bird{
Animal: Animal{Name: "Duck"},
CanFly: true,
}
fmt.Printf("b: %#v, c: %#v", b, c)
}
Output
b: main.Bird{Animal:main.Animal{Name:"Duck"}, CanFly:true}, c: main.Bird{Animal:main.Animal{Name:"Duck"}, CanFly:true}
Tags
Adds metadata to properties. Accessible via Go reflection package.
package main
import (
"fmt"
"reflect"
)
type Animal struct {
Name string `required max:"100"`
}
func main() {
t := reflect.TypeOf(Animal{})
field, _ := t.FieldByName("Name")
tag := field.Tag
fmt.Println(tag)
}
Output
required max:"100"
Value vs Reference types
There are value types:
- primitive types (int, double)
- string
- array
- struct
And reference types:
- slice
- map
To avoid copying a value type when assigning, we can use &
(address) prefix
a := [3]int{1,2,3} // This is an array
b := a // Here b is a new array, a copy of a
c := &a // Here c is an array pointing to array a
String formatting and interpolation
- For console output use
fmt.Printf("Some string here...", arg1, arg2, ..., argn)
- For string creation use
fmt.Sprintf ("Some string here...", arg1, arg2, ..., argn)
- A placeholder in a string literal has the forma
%x
wherex
is the formatting verb
Default format: "%v"
package main
import (
"fmt"
)
func main() {
fmt.Printf("%v \n", true) // boolean
fmt.Printf("%v \n", 123) // integer
fmt.Printf("%v \n", 123.456) // floating point
fmt.Printf("%v \n", (3 + 7i)) // complex
fmt.Printf("%v \n", "Hello") // string
fmt.Printf("%v \n", []int{1, 2, 3}) // slice
fmt.Printf("%v \n", map[string]int{
"one": 1,
"two": 2,
})
fmt.Printf("%v \n", struct {
name string
age int
}{"John", 26}) // structs
}
Output
true
123
123.456
(3+7i)
Hello
[1 2 3]
map[one:1 two:2]
{John 26}
Code format: "%#v"
package main
import (
"fmt"
)
func main() {
fmt.Printf("%#v \n", true) // boolean
fmt.Printf("%#v \n", 123) // integer
fmt.Printf("%#v \n", 123.456) // floating point
fmt.Printf("%#v \n", (3 + 7i)) // complex
fmt.Printf("%#v \n", "Hello") // string
fmt.Printf("%#v \n", []int{1, 2, 3}) // slice
fmt.Printf("%#v \n", map[string]int{
"one": 1,
"two": 2,
})
fmt.Printf("%#v \n", struct {
name string
age int
}{"John", 26}) // structs
}
Output
true
123
123.456
(3+7i)
"Hello"
[]int{1, 2, 3}
map[string]int{"one":1, "two":2}
struct { name string; age int }{name:"John", age:26}
Default format with field name flag (applies to structs): "%#v"
package main
import (
"fmt"
)
func main() {
s := struct {
name string
age int
}{"John", 26}
fmt.Printf("%+v", s)
}
Output
{name:John age:26}
Type format: "%T"
- Shows the data type of a variable or value
package main
import (
"fmt"
)
func main() {
fmt.Printf("%#T \n", true) // boolean
fmt.Printf("%#T \n", 123) // integer
fmt.Printf("%#T \n", 123.456) // floating point
fmt.Printf("%#T \n", (3 + 7i)) // complex
fmt.Printf("%#T \n", "Hello") // string
fmt.Printf("%#T \n", []int{1, 2, 3}) // slice
fmt.Printf("%#T \n", map[string]int{
"one": 1,
"two": 2,
})
fmt.Printf("%#T \n", struct {
name string
age int
}{"John", 26}) // structs
}
Output
bool
int
float64
complex128
string
[]int
map[string]int
struct { name string; age int }
Decimal ("%d"), Floating point ("%w.pf"), Binary ("%d"), Hexa("%x") and Octa ("%o") formats
- For Floating Point format (
"%w.pf"
)w
is width andp
is precision
import (
"fmt"
)
func main() {
fmt.Printf("%v \n", 30) // Decimal
fmt.Printf("%v \n", 0xFF) // Decimal (from an hexa)
fmt.Printf("%5.3f \n", 123.45) // Floating point
fmt.Printf("%b \n", 9) // Binary (from decimal 9)
fmt.Printf("%x \n", 124) // Hexa (from decimal 124)
fmt.Printf("%o \n", 9) // Octa (from decimal 9)
}
Output
30
255
123.450
1001
7c
11
Control Flow
if statement
package main
import "fmt"
func main() {
// Reguar syntax:
if true {
// do something...
} else {
// won't happen...
}
// Initialiser syntax:
m := map[string]int{
"key": 1,
}
if v, ok := m["key"]; ok {
// do something with value v if ok is true (if key exists in map)...
// note that v and ok are scoped to this block
fmt.Println(v)
} else {
fmt.Println("Key 'key' does not exist in map m")
}
// v and ok are not accessible here...
}
Output
1
Boolean comparison operators
< less than
> greater than
== equal
!= not equal
<= less or equal than
>= greater or equal than
Boolean logical operators
&& and
|| or
! not
switch statemet
- Overlapping cases are not allowed (causes compile time error) with tag syntax
Tag syntax
package main
import "fmt"
func main() {
switch 2 {
case 1, 3:
fmt.Println("Alo")
case 4:
fmt.Println("Ola")
default:
fmt.Println("Not sure...")
}
}
Output
not sure...
Initialiser syntax
package main
import "fmt"
func main() {
// Note that i has block scope
switch i := 2 + 3; i {
case 10:
fmt.Println("Ten")
default:
fmt.Println("Not sure...")
}
// i is not accessible here
}
Output
Not sure...
Tagless syntax (cases can overlap)
package main
import "fmt"
func main() {
// Note that you can use fallthrough to avoid implicit break
// Also you can use break explicitly
i := 10
switch {
case i > 5:
fmt.Println("Its more than five")
fallthrough
case i > 7:
fmt.Println("Its more than seven")
default:
fmt.Println("... not sure what it is")
}
}
Output
Its more than five
Its more than seven
package main
import "fmt"
func main() {
var i interface{} = 1 // We may not know the type of i if received as parameter
switch i.(type) {
case int:
fmt.Println("int")
case string:
fmt.Println("string")
default:
fmt.Println("Not sure...")
}
}
Output
int
Looping
Regular syntax
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
// i is scoped to block and only exists here...
fmt.Println(i)
}
}
Output
0
1
2
3
4
Initialising and incrementing 2 variables
package main
import "fmt"
func main() {
for i, j := 0, 9; i < 5; i, j = i+1, j-1 {
// i and j are scoped to block and only exists here...
fmt.Println(i, j)
}
}
Output
0 9
1 8
2 7
3 6
4 5
"while" loop with explicit condition
package main
import "fmt"
func main() {
i := 0
for i < 5 {
i++
fmt.Println(i)
}
}
Output
1
2
3
4
5
"while" loop without explicit condition (using break instead)
package main
import "fmt"
func main() {
// Note that you can also use continue to avoid executing the rest
// of the iteration block
i := 0
for {
i++
if i == 1 {
continue // Wont execute the rest of the code in this for block
}
if i == 5 {
break // Leaves the closest loop
}
fmt.Println(i)
}
}
Output
2
3
4
Looping into a collection using range
package main
import "fmt"
var m map[string]int = map[string]int{
"key": 1,
}
var a [3]int = [...]int{1, 2, 3}
func main() {
fmt.Println("Looping a map (key and value):")
for k, v := range m {
fmt.Println(k, v) // k is key and v is value in map m
}
fmt.Println("Looping a map (only key):")
for k := range m {
fmt.Println(k) // k is key in map m
}
fmt.Println("Looping a map (only value):")
for _, v := range m {
fmt.Println(v) // v is value in map m
}
fmt.Println("Looping an array (works on slices too):")
for k, v := range a {
fmt.Println(k, v) // k is the index and v is the value in array a
}
fmt.Println("Looping a string (thru its bytes):")
for k, v := range "John Doe" {
fmt.Println(k, v, string(v)) // k is the index and v is the byte value
}
}
Output
Looping a map (key and value):
key 1
Looping a map (only key):
key
Looping a map (only value):
1
Looping an array (works on slices too):
0 1
1 2
2 3
Looping a string (thru its bytes):
0 74 J
1 111 o
2 104 h
3 110 n
4 32
5 68 D
6 111 o
7 101 e
Defer / Panic / Recover
Defer
- Will defer the execution of a function call to the end of the code flow.
- Scoped to current function, in correlation to
return
andpanic
statements (or the end of the function when none of these statements are reached). - Multiple defer will be executed in LIFO order.
- Useful to close resources.
- The usual pattern when working with resources is:
- Open resource
- Check for errors
- Defer resource close
- Avoid the above pattern when working with resources inside a loop (just close the resource as quickly as you finished using it)
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
// 1. Open resource:
res, err := http.Get("http://www.google.com/robots.txt")
// 2. Check for errors:
if err != nil {
log.Fatal(err)
}
// 3. Defer resource close:
defer res.Body.Close()
// ... here continue processing the resource:
robots, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%s", robots)
}
Output
API server listening at: 127.0.0.1:39016
User-agent: *
Disallow: /search
# AdsBot
User-agent: AdsBot-Google
Disallow: /maps/api/js/
Allow: /maps/api/js
Disallow: /maps/api/place/js/
Disallow: /maps/api/staticmap
Disallow: /maps/api/streetview
...
...
...
# Certain social media sites are whitelisted to allow crawlers to access page markup when links to google.com/imgres* are shared. To learn more, please contact [email protected].
User-agent: Twitterbot
Allow: /imgres
User-agent: facebookexternalhit
Allow: /imgres
Sitemap: https://www.google.com/sitemap.xml
Defer will eagerly evaluate the values on the deferred statement, as shown here:
package main
import (
"fmt"
)
func main() {
a := 1
defer fmt.Println(a)
a = 2
}
Output
1
Panic
- Generated by the runtime whenever the program causes an exception
- Can be invoked by
panic("some error message")
- Explicit
panic
in code always happens after all defer gets excecuted
Simple webserver example, panicking whenever the server cannot start (open browser in http://localhost:8080/
)
package main
import "net/http"
func main() {
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())
}
}
Recover
- Helps recovering from a
panic
call - Whenever an invoked function panics and then recover, the invoker will continue its excecution
package main
import (
"fmt"
)
func main() {
fmt.Println("start")
panicker()
fmt.Println("end")
}
func panicker() {
fmt.Println("...about to panic!")
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
// you can re-panic here if error is not recoverable:
// panic(err)
}
}()
panic("something bad happened")
fmt.Println("this will not be printed")
}
Output - Note that "end"
is printed out but "this will not be printed"
is not
start
...about to panic!
something bad happened
end
Pointers
- To define a pointer a prefix
*
should prefix the actual type - The value for a variable of pointer type is a reference
- To assign a reference to a variable of pointer type we need to use prefix
&
on the target variable - To de-reference a pointer variable to get the memory address the prefix
*
should be used - Pointer arithmetic (as in C language) is not allowed
- Reference types (slice, map) handles underlying references
- Structs are automatically de-referenced (no need to use
*
prefix)
package main
import (
"fmt"
)
func main() {
var a int = 42
var b *int = &a // here variable b is a pointer to variable a
*b = 10 // here variable a is being set the value 10
var c int = *b // here variable c is a new variable set to value 10
fmt.Println(a, b, c)
}
Output
10 0xc0000140a0 10
Functions
- Function
main
inpackage main
is the entry point for a Go program - Usual naming convention applies for scoping
- Functions are types too
func someFunc(a int, b String) {
// ...
}
func someOtherFunc(a, b, c int) {
// a, b and c are of type int
}
Variadic parameters can be handled as a slice, and must be set as the final paramater
package main
import (
"fmt"
)
func main() {
total := sum("the sum is", 1, 2, 3, 4)
fmt.Println(total)
}
func sum(msg string, values ...int) int {
result := 0
for v := range values {
result += v
}
fmt.Println(msg, result)
return result
}
Output
the sum is 6
6
Returning named variables
package main
import (
"fmt"
)
func main() {
total := sum(1, 2, 3, 4)
fmt.Println(total)
}
func sum(values ...int) (result int) {
for v := range values {
result += v
}
return
}
Output
6
Go can return multiple values, the types needs to be set at the end of the function signature as a comma separated list
package main
import (
"fmt"
)
func main() {
if d, err := divide(5.0, 0.0); err != nil {
fmt.Println(err)
return
} else {
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
}
Output
Cannot divide by zero
func
is also a type.
package main
import (
"fmt"
)
var f func(float64, float64) (float64, error) = func(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, fmt.Errorf("Cannot divide by zero")
}
return a / b, nil
}
func main() {
for i := 0; i < 3; i++ {
fmt.Println(f(100.0, float64(i)))
}
}
Output
0 Cannot divide by zero
100 <nil>
50 <nil>
Functions can also be added in the context of a struct (or any other type), in this scenario these functions are called methods
package main
import (
"fmt"
)
func main() {
g := &greeter{
greeting: "Hello",
name: "Go!",
}
fmt.Println(g.greet())
}
type greeter struct {
greeting string
name string
}
func (g *greeter) greet() string {
return fmt.Sprintf("%v %v", g.greeting, g.name)
}
Output
Hello Go!
Interfaces
- Used to describe behavior (in contrast to
struct
which is used to describe data) - Implementation for an
interface
goes into astruct
(mostly, but can go into any other type too) - The
struct
only needs to implement methods in interface using the same signature (implicit implementation) - You can compose interfaces thru embedding (as in
structs
) - A factory method is the usual way to create new instances of structs implementing an interface
package main
import (
"fmt"
)
func main() {
// note that w is type Writer
var w Writer = NewConsoleWriter()
w.Write([]byte("Hello!"))
}
type Writer interface {
Write([]byte) (int, error)
}
type ConsoleWriter struct {
prefix string
}
func (cw *ConsoleWriter) Write(data []byte) (int, error) {
n, err := fmt.Println(cw.prefix, string(data))
return n, err
}
func NewConsoleWriter() *ConsoleWriter {
return &ConsoleWriter{
prefix: "Go Says:",
}
}
Output
Go Says: Hello!
The empty interface
- The empty interface
interface{}
is a very common type in libraries, but should be seen as an intermediate step as you may need to convert it (by using the notationvariable.(type)
) to effectively use its implementation. - You can safely check if a pointer to an empty interface "can be seen" as a particular interface (this can be used to avoid panicking when conversion fails), this is called type assertion
package main
import (
"fmt"
)
func main() {
var i interface{} = 0
// You can check type with a switch/case:
switch i.(type) {
case int:
fmt.Println("It's an int")
case float64:
fmt.Println("It's a float64")
default:
fmt.Println("Not sure what it is...")
}
// Or with an if:
if tmp, ok := i.(int); ok {
fmt.Printf("%v it's an int", tmp)
} else {
fmt.Println("Not an int...")
}
}
Output
It's an int
0 it's an int
Best practices on Interfaces
These are useful when implementing a package to be used by others in the team.
- Better many small interfaces
- Export concrete types (structs) instead of interfaces, unless the library consumer need to use these interfaces explicitly
- Design functions receiving interfaces as parameter whenever possible
Goroutines
- Enables concurrent / synchronization / parallelism.
- Uses the concept of green thread (meaning, in most cases a goroutine won't use a real thread)
- Use
sync.WaitGroup
to synchronize goroutines - Use
sync.RWMutex
to synchronize access to shared resources, allowing single goroutine to write but multiple goroutines to read - Use
runtime.GOMAXPROCS
to fine tune (in PROD) the number of green threads, and force it toruntime.GOMAXPROCS(1)
to reveal race condition issues while developing
Waitgroups
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
var m = sync.RWMutex{}
var counter int = 0
func main() {
wg.Add(3) // Will work with 3 goroutines
// Each of the following goroutines will mark end of processing with wg.Done()
go func() {
m.Lock() // Lock for write (allow only this goroutine to write)
counter++
m.Unlock() // Unlock
fmt.Println("Let's Go!")
wg.Done()
}()
go func() {
m.RLock() // Lock for read (no goroutine can write, but all can read)
fmt.Println("Ho!", counter)
m.RUnlock() // Unlock
wg.Done()
}()
go func() {
m.Lock() // Lock for write (allow only this goroutine to write)
counter++
m.Unlock() // Unlock
fmt.Println("Hey!")
wg.Done()
}()
wg.Wait() // Wait for all 3 goroutines to finish
}
Output
Hey!
Ho! 1
Let's Go!
Channels
- Designed to synchronize data flow between multiple goroutines
- A channel is strongly typed (a single channel will handle data of a single type)
- A channel can be buffered, allowing sender and receiver to operate at different rates
- A
range
loop can be used to READ from a channel - A channel can be set to send
struct{}
type, this is a signal-only channel, useful to send ACK signals
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
// Buffered channel: up to 50 messages can be sent/received at a time in this channel
ch := make(chan int, 50)
wg.Add(2)
// Signature says: This goroutine READs from channel ch
go func(ch <-chan int) {
// You can read all messages in channel until it gets closed with a for range:
for i := range ch {
fmt.Println(i)
}
// And you can also try to get a single message in a safe way:
if j, ok := <-ch; ok {
fmt.Println(j)
} else {
fmt.Println("Cannot read from channel, seems closed")
}
wg.Done()
}(ch)
// Signature says: This goroutine WRITEs to channel ch
go func(ch chan<- int) {
j := 42
ch <- j // 42 (value of j here) will be sent to channel ch
ch <- 30 // Another plain int value sent here to channel ch
close(ch) // Needed to signal readers that no more messages will be sent
wg.Done()
}(ch)
wg.Wait()
}
Output
42
30
Cannot read from channel, seems closed
Select statement
- Similar to
switch
/case
but applies to channels - Will match the case for the channel that received a message
- It's a blocking operation, unless you use
default
clause
package main
import (
"fmt"
)
var writerCh = make(chan string, 50)
var doneCh = make(chan struct{}) // This a signal-only channel
func main() {
go consoleWriter() // Spawn the goroutine
writerCh <- "Hello"
writerCh <- "Go!"
doneCh <- struct{}{} // Just sending an empty struct as a signal
}
func consoleWriter() {
// This loop will repeat until break condition (it's a blocking loop)
for {
select {
case entry := <-writerCh:
fmt.Println(entry)
case <-doneCh:
break
// Make this select non-blocking by using a `default` case here
}
}
}
Output
Hello
Go!
Testing
- Usually a test goes in the same folder as the go file to be tested having same file name but with suffix
_test
(as inSample.go
vsSample_test.go
) - To run automated tests use
go test
which will execute all functions in the form offunc TestXxx(*testing.T)
- Use table tests (using structs as the source of the data) to provide multiple data for a single test
Example of unit tests including table tests
Sample.go
package main
// Function to be tested:
func IntMin(a, b int) int {
if a < b {
return a
}
return b
}
Sample_test.go
package main
import (
"fmt"
"testing"
)
// Sample unit test:
func TestIntMin(t *testing.T) {
expectede := -2
actual := IntMin(2, -2)
if expectede != actual {
t.Errorf("Expected %v but got %v", expectede, actual)
}
}
// Sample table test:
func TestIntMin_Table(t *testing.T) {
var tests = []struct {
a, b int
result int
}{
{0, 1, 0},
{10, 1, 1},
{-2, -11, -11},
{-20, -11, -20},
}
for _, v := range tests {
name := fmt.Sprintf("%v,%v", v.a, v.b)
t.Run(name, func(t *testing.T) {
expected := v.result
actual := IntMin(v.a, v.b)
if expected != actual {
t.Errorf("Expected %v but got %v", expected, actual)
}
})
}
}
Output (after running $ go test -v
for verbosity)
$ go test -v
=== RUN TestIntMin
--- PASS: TestIntMin (0.00s)
=== RUN TestIntMin_Table
=== RUN TestIntMin_Table/0,1
=== RUN TestIntMin_Table/10,1
=== RUN TestIntMin_Table/-2,-11
=== RUN TestIntMin_Table/-20,-11
--- PASS: TestIntMin_Table (0.00s)
--- PASS: TestIntMin_Table/0,1 (0.00s)
--- PASS: TestIntMin_Table/10,1 (0.00s)
--- PASS: TestIntMin_Table/-2,-11 (0.00s)
--- PASS: TestIntMin_Table/-20,-11 (0.00s)
PASS
ok _/Users/raulbajales/mygocode/src
Error handling
- The usual pattern for a function that may cause a failure in runtime is:
- The return type should be a tuple where the first element if the success value, and the second element is either an
error
or abool
- In success case: The first element of the tuple is set to the success value and the second element is set to
nil
ortrue
- In failure case: The first element of the tuple is set to a default value and the second element is set to
error
orfalse
- The return type should be a tuple where the first element if the success value, and the second element is either an
- If you need to generate an
error
you can useerrors.New(msg string)
orfmt.Errorf(msg string, args ...interface{})
- You can build your set or hierarchy of domain specific error types (just need to satisfy the predefined
error
interface), then whenever you need to handle the errors you can use type assertion for custom error handling
Example 1, showing:
- Return tuple (success, error)
- Handle success and error case
- Generate an error with custom message
package main
import (
"fmt"
)
func main() {
// Get tuple to check for success or failure
if d, err := divide(5.0, 0.0); err != nil {
fmt.Println(err)
return
} else {
fmt.Println(d)
}
}
func divide(a, b float64) (float64, error) {
if b == 0.0 {
// Failure case:
return 0.0, fmt.Errorf("Cannot divide %v by zero", a)
}
// Success case:
return a / b, nil
}
Output
Cannot divide 5 by zero
Example 2, showing same case but with return type (success, bool):
package main
import (
"fmt"
)
func main() {
// Get tuple to check for success or failure
if d, ok := divide(5.0, 0.0); ok {
fmt.Println(d)
} else {
fmt.Println("Cannot divide by zero")
}
}
func divide(a, b float64) (float64, bool) {
if b == 0.0 {
// Failure case:
return 0.0, false
}
// Success case:
return a / b, true
}
Output
Cannot divide zero
Next topics
That's it (: