Go Advanced
Setting up
file structure
- some_folder/module_name/package_name/go_file.go
new project == new module
- name module with github repository is very common
go mod init github.com/yixianwang/module_namego run
- equals to
go buildand then run executable
Data Types
const myConst
var myVar
bool
float32 float64
int int16 int32 int64
rune
string
unit uint8 uint16 uint32 uint64built-in package
import "unicode/utf8"then useutf8.RuneCountInString("汉")
Functions & Control Structures
Function with multiple return values
func intDivision(numerator int, denominator int) (int, int) {
var result int = numerator / denominator
var remainder int = numerator % denominator
return result, remainder
}
// var result, remainder = intDivision(numerator, denominator)error handling
import "errors"
func intDivision(numerator int, denominator int) (int, int, error) {
var err error // default value is nil
if denominator == 0 {
err = errors.New("some message")
return 0, 0, err
}
var result int = numerator / denominator
var remainder int = numerator % denominator
return result, remainder, err
}
// var result, remainder, err = intDivision(numerator, denominator)
// if err != nil {
// fmt.Printf(err.Error())
// }switch keyword
// similar to if
switch {
case err != nil:
fmt.Printf(err.Error())
case remainder == 0:
fmt.Printf("%v", result)
default:
fmt.Printf("%v, %v", result, remainder)
}
// another syntax with switch
switch remainder {
case 0:
fmt.Printf("The division was exact")
case 1, 2:
fmt.Printf("The division was exact")
default:
fmt.Printf("The division was not close")
}Arrays, Slices, Maps && Looping Control Structures
var intArr [3]int32 = [3]int32{1,2,3}
// intArr := [3]int32{1,2,3}
// intArr := [...]int32{1,2,3}
var intSlice []int32 = []int32{4,5,6}
fmt.Printf("The length is %v with capacity %v", len(intSlice), cap(intSlice))
intSlice = append(intSlice, 7)
fmt.Printf("The length is %v with capacity %v", len(intSlice), cap(intSlice))
var intSlice2 []int32 = []int32{8, 9}
intSlice = append(intSlice, intSlice2...)
var intSlice3 []int32 = make(int32[], 3) // 3 is length
// var intSlice3 []int32 = make(int32[], 3, 8) // 8 is capacity (optional, default is equal to length of slice)
var myMap map[string]uint8 = make(map[string]uint8)
fmt.Println(myMap)
var myMap2 = map[string]uint8{"Adam":23, "Sam":45}
fmt.Println(myMap["Adam"])
fmt.Println(myMap["KeyNotExist"]) // get default value of uint8, which is 0
var age, ok = myMap2["Adam"] // ok is true if the key exist in the map, and false otherwise
delete(myMap2, "Adam") // no return value// iterate over i.e. map, array, slice
for key, val := range myMap2 {
fmt.Printf("key: %v, val: %v\n", key, val)
}
for idx, val := range intArr {
fmt.Printf("idx: %v, val: %v\n", idx, val)
} Strings, Runes, Bytes
%v, value%T, type of the value
var myString = "résumé"
var indexed = myString[1] // return 195 != 233, it's not correct
fmt.Printf("%v, %T\n", indexed, indexed)
for i, v := range myString {
fmt.Println(i, v)
}
// 114, uint8
// 0 114
// 1 233 // 233 is correct here with range keyword
// 3 115
// 4 117
// 5 109
// 6 233len(myString)return the number of bytes ofmyString
runes
- runes are just Unicode Point numbers which represent the character
- runes are just an alias for int32
- we can declare a rune type using a single quote
var myRune = 'a'
var myString = []rune("résumé")
var indexed = myString[1] // return 233 == 233, correct
fmt.Printf("%v, %T\n", indexed, indexed)
for i, v := range myString {
fmt.Println(i, v)
}string building
- strings are immutable in go, we cannot modify them once created
var strSlice = []string{"y", "i", "x", "i", "a", "n"}
var catStr = ""
for i := range strSlice {
catStr += strSlice[i]
}
fmt.Printf("\n%v", catStr)- best practice: we can import built-in
stringspackage, and create astrings.Builder - instead of using plus operator, we call
WriteStringmethod
var strSlice = []string{"y", "i", "x", "i", "a", "n"}
var strBuilder strings.Builder
for i := range strSlice {
strBuilder.WriteString(strSlice[i])
}
var catStr = strBuilder.String()
fmt.Printf("\n%v", catStr)Structs, Interfaces
package main
import "fmt"
// create a struct
type gasEngine struct {
mpg uint8
gallons uint8
// ownerInfo owner
owner // we can adding subfield directly
int // use type int directly, so we can use this syntax with any type
}
type owner struct {
name string
}
func main() {
// var myEngine gasEngine
// fmt.Println(myEngine.mpg, myEngine.gallons)
// 0, 0
// var myEngine gasEngine = gasEngine{mpg:25, gallons:15}
// var myEngine gasEngine = gasEngine{25, 15} // we can omit the field names, it will assign in order
// myEngine.mpt = 20 // we can also set the values by name directly
// fmt.Println(myEngine.mpg, myEngine.gallons)
// 25, 15
var myEngine gasEngine = gasEngine{25, 15, owner{"Alex"}}
fmt.Println(myEngine.mpg, myEngine.gallons, myEngine.ownerInfo.name) // if we adding subfield directly, we can omit ownerInfo field
}anonymous struct
- define and initialize in the same location
- the main difference is that is not reusable
package main
import "fmt"
type gasEngine struct {
mpg uint8
gallons uint8
}
func main() {
// var myEngine gasEngine = gasEngine{25, 15}
var myEngine2 = struct {
mpg uint8
gallons uint8
} {21, 12}
fmt.Println(myEngine2)
}struct method
package main
import "fmt"
type gasEngine struct {
mpg uint8
gallons uint8
}
func (e gasEngine) milesLeft() uint8 {
return e.gallons * e.mpg
}
func main() {
var myEngine gasEngine = gasEngine{25, 15}
fmt.Println(myEngine2)
}interface
package main
import "fmt"
type gasEngine struct {
mpg uint8
gallons uint8
}
type electricEngine struct {
mpkwh uint8
kwh uint8
}
func (e gasEngine) milesLeft() uint8 {
return e.gallons * e.mpg
}
func (e electricEngine) milesLeft() uint8 {
return e.kwh * e.mpkwh
}
type engine interface {
milesLeft() uint8 // 1. method signature
}
func canMakeIt(e engine, miles uint8) { // 2. use engine here
if miles <= e.milesLeft() {
fmt.Println("You can make it there!")
} else {
fmt.Println("Need to fuel up first!")
}
}
func main() {
var myEngine gasEngine = gasEngine{25, 15}
canMakeIt(myEngine, 50) // 3. apply with various Engine types
}Pointers
- same to c/c++
Goroutines
- use
gokeyword in front of the function we want to run concurrently - import
syncpackage, to let wait groups come in - then we create a wait group
var wg = sync.WaitGroup{}, they just like counters - add
wg.Add(1)andwg.Done() - add
wg.Wait(), it gonna wait for the counter to go back down to 0, meaning all the tasks have completed
package main
import (
"fmt"
"math/rand"
"time"
"sync"
)
var wg = sync.WaitGroup{}
var dbData = []string{"id1", "id2", "id3", "id4", "id5"}
func main() {
t0 := time.Now()
for i := 0; i < len(dbData); i++ {
wg.Add(1)
go dbCall(i)
}
wg.Wait()
fmt.Printf("\nTotal execution time: %v", time.Since(t0))
}
func dbCall(i int) {
// simulate DB call delay
var delay float32 = rand.Float32() * 2000
time.Sleep(time.Duration(delay) * time.Millisecond)
fmt.Println("The result from the database is:", dbData[i])
wg.Done()
}using locks to make threads safe
without lock
package main
import (
"fmt"
"time"
"sync"
)
var wg = sync.WaitGroup{}
var dbData = []string{"id1", "id2", "id3", "id4", "id5"}
var results = []string{} // 1. create a slice to store all the result from db
func main() {
t0 := time.Now()
for i := 0; i < len(dbData); i++ {
wg.Add(1)
go dbCall(i)
}
wg.Wait()
fmt.Printf("\nTotal execution time: %v", time.Since(t0))
fmt.Printf("\nThe results are %v", results) // 3. print the results
}
func dbCall(i int) {
// simulate DB call delay
var delay float32 = 2000
time.Sleep(time.Duration(delay) * time.Millisecond)
fmt.Println("The result from the database is:", dbData[i])
results = append(results, dbData[i]) // 2. append the result
wg.Done()
}Above code, WE WILL GET AN UNEXPECTED RESULT
with lock (sync.Mutex{})
- to make thread safe, we can use mutex (Mutual Exclusion) by
var m = sync.Mutex{} - with two main methods
m.Lock()andm.Unlock(), and place them around the part of our code which access the result slice - cons: it completely locks out other go routines to access the results slice
package main
import (
"fmt"
"time"
"sync"
)
var m = sync.Mutex{} // 1. create a mutex
var wg = sync.WaitGroup{}
var dbData = []string{"id1", "id2", "id3", "id4", "id5"}
var results = []string{}
func main() {
t0 := time.Now()
for i := 0; i < len(dbData); i++ {
wg.Add(1)
go dbCall(i)
}
wg.Wait()
fmt.Printf("\nTotal execution time: %v", time.Since(t0))
fmt.Printf("\nThe results are %v", results)
}
func dbCall(i int) {
// simulate DB call delay
var delay float32 = 2000
time.Sleep(time.Duration(delay) * time.Millisecond)
fmt.Println("The result from the database is:", dbData[i])
m.Lock() // 2. use lock
results = append(results, dbData[i])
m.Unlock() // 2. use unlock
wg.Done()
}with lock (sync.RWMutex{})
-
this has all the same functionality of of the mutex above
- and the
m.Lock()andm.Unlock()work exactly the same - but we also have
m.RLock()andm.RUnlock()methods
- and the
-
workflows:
- when go routine reaches
m.RLock(), it checks if there’s a full lock (m.Lock()) on the mutex
- if full lock exists, it(
m.RLock()) will wait until full lock is released before continuing - if no full lock exists, the go routine will acquire a read lock (
m.RLock()), and then proceed with the rest of the code
- when go routine reaches
-
Note:
- many go routines may hold read locks at the same time, these read locks will only block code execution up to the full lock
- when the a go routine hits full lock and in order to proceed, all locks must be cleared
pros: this prevents us from accessing the slice while other go routines are writing to or reading from the slice
- summary: it allows multiple go routines to read from our slice at the same time, only blocking when writes may be potentially be happening
package main
import (
"fmt"
"time"
"sync"
)
var m = sync.RWMutex{} // 1. use RWMutex
var wg = sync.WaitGroup{}
var dbData = []string{"id1", "id2", "id3", "id4", "id5"}
var results = []string{}
func main() {
t0 := time.Now()
for i := 0; i < len(dbData); i++ {
wg.Add(1)
go dbCall(i)
}
wg.Wait()
fmt.Printf("\nTotal execution time: %v", time.Since(t0))
fmt.Printf("\nThe results are %v", results)
}
func dbCall(i int) {
var delay float32 = 2000
time.Sleep(time.Duration(delay) * time.Millisecond)
save(dbData[i])
log()
wg.Done()
}
func save(result string) {
m.Lock()
results = append(results, result)
m.Unlock()
}
func log() {
m.RLock()
fmt.Printf("\nThe current results are: %v", results)
m.RUnlock()
}Channels
-
think of channels as a way to enable go routines to pass around information
-
main features:
- Hold Data: i.e. integer, slice, or anything else
- Thread Safe: i.e. we avoid data races when we’re reading and writing from memory
- Listen for Data: we can listen when data is added or removed from a channel and we can block code execution until one of these events happens.
-
to make a channel, we use
makefunction followed by thechankeyword, then the type of value we want the channel to hold. i.e.var c = make(chan int), so this channel can only hold a single int value -
channels also have a special syntax. i.e. we use
<-to add value to the channel -
we can think of a channel as containing an underlying array, in this case we have what’s called an Unbuffer Channel, which only has enough room for one value.
-
we can retrieve the value from a channel using
var i = <- c, so here the value gets popped out of the channel(the channel is now empty) and variableiholds the value
deadlock errors
- Why?:
- when we write to an Unbuffer Channel(
c <- 1), the code will block until something else reads from it. - so in effect we’ll be waiting here forever, unable to reach the line (
var i = <- c), where we actually read from the channel - luckily go’s runtime is smart enough to notice this and we will just throw a deadlock error rather than our code hanging here forever.
- when we write to an Unbuffer Channel(
To use it properly in conjunction with go routine
- Channel + Go Routines == Proper Way
package main
import "fmt"
func main() {
var c = make(chan int) // to make a channel
c <- 1 // add value to the channel
var i = <- c // retrieve the value from a channel
fmt.Println(i)
}(Channel + Go Routines) is the proper way
example 1
package main
import "fmt"
func main() {
var c = make(chan int) // 1. make a channel
go process(c) // 2. call go routine
fmt.Println(<- c) // !!! 3. the execution will be waiting here for a value to be set in the channel
// !!! 5. then our main function notices that a value has been set, and finally the print function gets called
}
func process(c chan int) {
c <- 123 // !!! 4. in this go routine, we set the value and exit the function
}example 2
-
we can iterate over the channel by using
rangekeyword -
work flows:
- we create the channel(
make(chan int)) and start the go routine(go process(c)) - in the main function we wait at the top of the
forloop for something to be added to the channel - in the process function we setup
forloop and add0to the channel - we wait (
c <- 0) until the main function reads from the channel - and then in a concurrent way both the value printed and
1is added to the channel at about the same time - this then continues until
iis equal to5.
- we create the channel(
package main
import "fmt"
func main() {
var c = make(chan int)
go process(c)
for i := range c {
fmt.Println(i) // i here is the value of the channel
}
}
func process(c chan int) {
for i := 0; i < 5; i++ {
c <- i
}
}Note: deadlock error happens again for above code
-
because after we print all of our values from
0to4, -
the main function will go back to wait at the top of the
forloop for another value -
but just like me on under after five messages it will get ghosted by the process function which won’t send any more messages and we get deadlock error
-
Solution:
- before exiting a process, we can close the channel like
close(c)ordefer close(c) close(c)notifies any other process using this channel that we’re done- and our main function will break out of the
forloop and exit.
- before exiting a process, we can close the channel like
defer close(c)usingdeferstatement and it go this just means do this stuff(close(c)) right before the function exits.
package main
import "fmt"
func main() {
var c = make(chan int)
go process(c)
for i := range c {
fmt.Println(i)
}
}
func process(c chan int) {
for i := 0; i < 5; i++ {
c <- i
}
close(c) // close the channel
}Buffer Channel
- now we can store multiple values in the channel at the same time.
- i.e. we can store 5 integers
var c = make(chan int, 5)
if we run the code with the regular channel, the process function stays active until the main function is done with the channel. But there’s no need for the process function to hang around. It can finish its work quickly and just exit. And let the main function do its thing.
package main
import (
"fmt"
"time"
)
func main() {
var c = make(chan int, 5) // the process function can add up to 5 values in the channel without having to wait for the main function to make room in the channel by popping out a value(at the for loop line)
go process(c)
for i := range c {
fmt.Println(i)
time.Sleep(time.Second * 1) // some work..., takes 1 second
}
}
func process(c chan int) {
defer close(c)
for i := 0; i < 5; i++ {
c <- i
}
fmt.Println("Exiting process")
}Note: the process function finishes almost immediately while the main function is still running reading the values in the channel.
Realistic example of Channels
package main
import (
"fmt"
"math/rand"
"time"
)
var MAX_CHICKEN_PRICE float32 = 5
var MAX_TOFU_PRICE float32 = 3
// there are three go routines running at the same time checking these three websites
// and sendMessage function is waiting there for value to be added to the channel to send off text
// so the first go routine to find a deal on chicken will trigger the text message in the program and exit.
func main() {
var chickenChannel = make(chan string) // the channel holds the website we found the sale on
var tofuChannel = make(chan string) // when we find a bargain on tofu we write to this channel
var websites = []string{"walmart.com", "costco.com", "wholefoods.com"}
for i := range websites {
go checkChickenPrices(websites[i], chickenChannel) // we spawn three go routines
go checkTofuPrices(websites[i], tofuChannel)
}
sendMessage(chickenChannel, tofuChannel) // send a message when a deal is found
}
func checkTofuPrices(website string, c chan string) {
for {
time.Sleep(time.Second * 1)
var tofu_price = rand.Float32() * 20
if tofu_price <= MAX_TOFU_PRICE {
c <- website
break
}
}
}
func checkChickenPrices(website string, chickenChannel chan string) {
for {
// every second check the website for the price of chicken
// and if it's below our threshold, it will set the value of the channel to the website
time.Sleep(time.Second * 1)
var chickenPrice = rand.Float32() * 20
if chickenPrice <= MAX_CHICKEN_PRICE {
chickenChannel <- website
break
}
}
}
func sendMessage(chickenChannel chan string, tofuChannel chan string) {
// fmt.Printf("\nFound a deal on chicken at %s", <- chickenChannel) // waiting here for value to be added to the channel
// select statement will listen for a result once it gets one it'll execute one of those statements and exit.
select {
// if we receive a message from the chicken channel, we set the variable website to the value in the channel and we execute the following statement
case website := <- chickenChannel:
fmt.Printf("\nText sent: Found deal on chicken at %v.", website)
// otherwise if we receive a message from the tofu channel, we execute the following statement
case website := <- tofuChannel:
fmt.Printf("\nEmail sent: Found deal on chicken at %v.", website)
}
}Generics
normal generic example
package main
import "fmt"
func main() {
var intSlice = []int{1, 2, 3}
fmt.Println(sumSlice[int](intSlice))
var float32Slice = []float32{1, 2, 3}
fmt.Println(sumSlice[float32](float32Slice))
}
func sumSlice[T int | float32 | float64](slice []T) T {
var sum T
for _, v := range slice {
sum += v
}
return sum
}any type example
package main
import "fmt"
func main() {
var intSlice = []int{}
fmt.Println(isEmpty(intSlice)) // we can omit the square bracket type input here
var float32Slice = []float32{1, 2, 3}
fmt.Println(isEmpty(float32Slice)) // and here
}
func isEmpty[T any](slice []T) bool {
return len(slice) == 0
}an example that we can’t infer the type of our generic parameter
package main
import (
"fmt"
"encoding/json"
"io/ioutil"
)
type contactInfo struct {
Name string
Email string
}
type purchaseInfo struct {
Name string
Price float32
Amount int
}
func main() {
var contacts []contactInfo = loadJSON[contactInfo]("./contactInfo.json")
fmt.Printf("\n%+v", contacts)
var purchases []purchaseInfo = loadJSON[purchaseInfo]("./purchaseInfo.json")
fmt.Printf("\n%+v", purchases)
}
func loadJSON[T contactInfo | purchaseInfo] (filePath string) []T {
data, _ = ioutil.ReadFile(filePath)
var loaded = []T{}
json.Unmarshal(data, &loaded)
return loaded
}struct generic
package main
import "fmt"
type gasEngine struct {
gallons float32
mpg float32
}
type electricEngine struct {
kwh float32
mpkwh float32
}
type car[T gasEngine | electricEngine] struct {
carMake string
carModel string
engine T
}
func main() {
var gasCar = car[gasEngine] {
carMake: "Honda",
carModel: "Civic",
engine: gasEngine {
gallons: 12.4,
mpg: 40,
},
}
fmt.Println(gasCar)
var electricCar = car[electricEngine] {
carMake: "Tesla",
carModel: "Model 3",
engine: electricEngine {
kwh: 57.5,
mpkwh: 4.17,
},
}
fmt.Println(electricCar)
}Building an API!
go mod init module_name(i.e. Github URL)mkdir api, api folder contains specs things like parameters and response type for our endpoint. This is also where we could put our yaml spec file.mkdir cmd/api, will contain our main.go.mkdir internal, will contain most of code for this API.