2. Go is a statically typed, compiled programming language designed at Google in 2007 by Robert Griesemer(JVM, V8 JavaScript
engine),
Rob Pike(Unix, UTF-8), and Ken Thompson(B, C, Unix) to improve programming productivity in an era
of multicore, networked machines and large codebases.
The designers wanted to address criticism of other languages in use at Google, but keep their useful characteristics:
- Static typing and run-time efficiency (like C)
- Readability and usability (like Python or JavaScript)
- High-performance networking and multiprocessing
It is syntactically like C, but with memory safety, garbage collection, structural typing, and CSP-style concurrency.
There are two major implementations:
1. Google's self-hosting "gc" compiler toolchain, targeting multiple operating systems and WebAssembly.
2. gofrontend, a frontend to other compilers, with the libgo library. With GCC the combination is gccgo; with LLVM the combination is
gollvm.
A third-party source-to-source compiler, GopherJS,[20] compiles Go to JavaScript for
front-end web development.
The guarantee: code written for Go 1.0 will build and run with Go 1.X.
6. Zero Values
var i int // my zero value is 0
var f float64 // my zero value is 0
var b bool // my zero value is false
var s string // my zero value is ""
8. Switch
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.n", os)
}
// Switch with no condition
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.")
}
9. Pointers
func main() {
i, j := 42, 2701
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
}
A pointer holds the memory address of a value.
10. Struct (“class”)
type Vertex struct {
X int
Y int
}
func NewVertex(x, y int) *Vertex{
return &Vertex{
X: x,
Y: y,
}
}
A struct is a collection of fields
12. Slices(“Lists”)
Slices are pointers to arrays, with the length of the
segment, and its capacity.
The length of a slice is the number of elements it contains.
The capacity of a slice is the number of elements in the
underlying array, counting from the first element in the
slice.
a := make([]int, 5) // len(a)=5
var s []int
// append works on nil slices.
s = append(s, 0)
What is the zero value of a slice?
13. Slices(“Lists”)
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %dn", i, v)
}
for i := range pow {
pow[i] = 1 << uint(i) // == 2**i
}
for _, value := range pow {
fmt.Printf("%dn", value)
}
}
14. Maps
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"])
}
What is the zero value of a map?
15. Methods
Go does not have classes. However, you can define methods on types.
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
}
16. Methods
Choosing a value or pointer receiver
There are two reasons to use a pointer receiver.
The first is so that the method can modify the value that its receiver points to.
The second is to avoid copying the value on each method call.
This can be more efficient if the receiver is a large struct, for example.
In general, all methods on a given type should have either value or pointer
receivers, but not a mixture of both.
17. Enums
type AccountStatus int
const (
CreateInProgress AccountStatus = iota
CreateDone
CreateFailed
DeleteInProgress
DeleteFailed
)
func (a AccountStatus) String() string {
return [...]string{
"CreateInProgress",
"CreateDone",
"CreateFailed",
"DeleteInProgress",
"DeleteFailed",
}[a]
}
iota is an identifier that is used with constant, and which can
simplify constant definitions that use auto-increment
numbers. The iota keyword represents integer constant
starting from zero.
18. Interfaces
type Abser interface {
Abs() float64
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
* Interface are pointers in GO!
Interfaces are named collections of method signatures.
19. Interfaces “trick”!
Let's say we are using a third-party library that defines this struct:
type Vertex struct {}
func (v *Vertex) DoSmt() float64 {
return 1.1
}
We can create an interface in our code and make Vertex struct implicity implement our interace!
It might be very usfull for unit testing.
type Bla interface {
DoSmt() float64
}
20. Composition(Embedding) over inheritance
type Request struct {
Resource string
}
type AuthenticatedRequest struct {
Request
Username, Password string
}
func main() {
ar := new(AuthenticatedRequest)
ar.Resource = "example.com/request"
ar.Username = "bob"
ar.Password = "P@ssw0rd"
fmt.Printf("%#v", ar)
}
Go supports embedding of structs and interfaces to
express a more seamless composition of types.
21. The empty interface (Any/Object)
The interface type that specifies zero methods is known as the
empty interface: interface{}
An empty interface may hold values of any type. (Every type implements at least zero
methods.)
22. Imports
1. Default import
2. Using alias
3. Access package content without typing the package name
4. Import a package solely for its side-effact (initialization)
import "fmt"
import format "fmt"
import . "fmt"
import _ "fmt"
24. Panic
A panic typically means something went unexpectedly wrong. Mostly we use it to fail
fast on errors that shouldn’t occur during normal operation, or that we aren’t
prepared to handle gracefully.
Running this program will cause it to panic, print an error message and goroutine
traces, and exit with a non-zero status.
func main() {
panic("a problem")
}
25. Defer
A defer statement defers the execution of a function until the
surrounding function returns.
When to use?
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
26. Recover
Go makes it possible to recover from a panic, by using
the recover built-in function. A recover can stop a panic from aborting
the program and let it continue with execution instead.
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:n", r)
}
}()
mayPanic()
fmt.Println("After mayPanic()")
}
27. Error handling
Error handling in Golang is done through the built-in interface type, error.
It’s zero value is nil; so, if it returns nil, that means that there were no error.
By convention, errors are the last return value
func divide(x int, y int) (int, error) {
if y == 0 {
return -1, errors.New("Cannot divide by 0!")
}
return x/y, nil
}
func main() {
answer, err := divide(5,0)
if err != nil {
// Handle the error!
fmt.Println(err) // will print “Cannot divide by 0!”
} else {
// No errors!
fmt.Println(answer)
}
}
type error interface {
Error() string
}
28. Error handling – where is my stacktrace?
func FindUser(username string) (*db.User, error) {
u, err := db.Find(username)
if err != nil {
return nil, fmt.Errorf("FindUser: failed executing db query: %w", err)
}
return u, nil
}
func SetUserAge(u *db.User, age int) error {
if err := db.SetAge(u, age); err != nil {
return fmt.Errorf("SetUserAge: failed executing db update: %w", err)
}
}
func FindAndSetUserAge(username string, age int) error {
var user *User
var err error
user, err = FindUser(username)
if err != nil {
return fmt.Errorf("FindAndSetUserAge: %w", err)
}
if err = SetUserAge(user, age); err != nil {
return fmt.Errorf("FindAndSetUserAge: %w", err)
}
return nil
}
Calling FindAndSetUserAge result:
FindAndSetUserAge: SetUserAge: failed executing db update: malformed request
Golang authors thought that the stack trace should be printed
only on Panic to reduce the overhead of unwinding the call stack.
It goes hand in hand with the error handling approach -
since errors must be handled, the developer should take care also
for the stack trace.
However, if you still want a stack trace you can get it using go runtime/debug package.
There are some third-party libraries that can be used for that:
https://pkg.go.dev/golang.org/x/xerrors
https://github.com/pkg/errors
https://github.com/rotisserie/eris
30. Dependecies (Go Modules)
The go.mod file is the root of dependency management in GoLang.
All the modules which are needed or to be used in the project are
maintained in go.mod file.
After running any package building command like go build, go test for
the first time, it will install all the packages with specific versions i.e. which
are the latest at that moment.
It will also create a go.sum file which maintains the checksum so when you
run the project again it will not install all packages again. But use the cache
which is stored inside $GOPATH/pkg/mod directory
(module cache directory).
go.sum is a generated file you don’t have to edit or modify this file.
“require” will include all dependency modules and the related version we
are going to use in our project
“replace” points to the local version of a dependency in Go rather than the
git-web. It will create a local copy of a vendor with versions available so no
need to install every time when we want to refer the vendor.
“//indirect” implies that we are not using these dependencies inside our
project but there is some module which imports these.
all the transitive dependencies are indirect, these include dependencies
which our project needs to work properly.
31. Dependecies (Go Modules)
go mod tidy -
It will bind the current imports in the project and packages listed in go.mod.
It ensures that the go.mod file matches the source code in the module. It adds any missing
module requirements necessary to build the current module’s packages and dependencies, if
there are some not used dependencies go mod tidy will remove those from go.mod accordingly.
It also adds any missing entries to go.sum and removes unnecessary entries.
go mod vendor -
It generates a vendor directory with the versions available. It copies all third-party dependencies
to a vendor folder in your project root.
This will add all the transitive dependencies required in order to run the vendor package.
When vendoring is enabled, the go command will load packages from the vendor directory
instead of downloading modules from their sources into the module cache and using packages
those downloaded.
32. Generics
Starting with version 1.18, Go has added support for generics, also known
as type parameters.
func MapKeys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
func main() {
var m = map[int]string{1: "2", 2: "4", 4: "8"}
fmt.Println("keys:", MapKeys(m))
}
33. Context
A Context carries deadlines, cancellation signals, and other request-scoped values across API boundaries and
goroutines.
Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context.
The chain of function calls between them must propagate the Context, optionally replacing it with a derived
Context created using WithCancel, WithDeadline, WithTimeout, or WithValue.
When a Context is canceled, all Contexts derived from it are also canceled.
The WithCancel, WithDeadline, and WithTimeout functions take a Context (the parent) and return a derived
Context (the child) and a CancelFunc. Calling the CancelFunc cancels the child and its children, removes the
parent's reference to the child, and stops any associated timers. Failing to call the CancelFunc leaks the child
and its children until the parent is canceled or the timer fires.
The go vet tool checks that CancelFuncs are used on all control-flow paths.
- Do not pass a nil Context, even if a function permits it.
Pass context.TODO if you are unsure about which Context to use.
- Use context Values only for request-scoped data that transits processes and APIs,
not for passing optional parameters to functions.
- The same Context may be passed to functions running in different goroutines;
Contexts are safe for simultaneous use by multiple goroutines (context is immutable)
35. Unit Testing - Mocking
type MyRepo interface {
Get(ctx context.Context, id string) (interface{}, error)
Update(ctx context.Context, model interface{}) (interface{}, error)
}
type MyRepoImpl struct {
pg interface{}
}
func (m *MyRepoImpl) Get(ctx context.Context, id string) (interface{}, error) {
panic("implement me")
}
func (m *MyRepoImpl) Update(ctx context.Context, model interface{}) (interface{}, error) {
panic("implement me")
}
type MyRepoMock struct {
MockGet func(ctx context.Context, id string) (interface{}, error)
MockUpdate func(ctx context.Context, model interface{}) (interface{}, error)
}
func (a *MyRepoMock) Get(ctx context.Context, id string) (interface{}, error) {
return a.MockGet(ctx, id)
}
func (a *MyRepoMock) Update(ctx context.Context, model interface{}) (interface{}, error) {
return a.MockUpdate(ctx, model)
}
type MyService struct {
myRepo MyRepo
}
func (s *MyService) DoSmt(ctx context.Context) error {
// some logic...
model, err := s.myRepo.Get(ctx, "123")
if err != nil {
return err
}
// some logic...
}
36. Context - WithValue
func doSomething(ctx context.Context) {
fmt.Printf("doSomething: myKey's value is %sn", ctx.Value("myKey"))
}
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "myKey", "myValue")
doSomething(ctx)
}
Output
doSomething: myKey's value is myValue
37. Context - WithCancel
func doSomething(ctx context.Context) {
ctx, cancelCtx := context.WithCancel(ctx)
printCh := make(chan int)
go doAnother(ctx, printCh)
for num := 1; num <= 3; num++ {
printCh <- num
}
cancelCtx()
time.Sleep(100 * time.Millisecond)
fmt.Printf("doSomething: finishedn")
}
func doAnother(ctx context.Context, printCh <-chan int) {
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Printf("doAnother err: %sn", err)
}
fmt.Printf("doAnother: finishedn")
return
case num := <-printCh:
fmt.Printf("doAnother: %dn", num)
}
}
}
Output
doAnother: 1
doAnother: 2
doAnother: 3
doAnother err: context canceled
doAnother: finished
doSomething: finished
38. Context – WithDeadline / WithTimeout
const shortDuration = 1 * time.Millisecond
func main() {
d := time.Now().Add(shortDuration)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancellation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
Output:
context deadline exceeded
39. Context – OpenTelemetry
OpenTelemetry also relies heavily on context for what is
called Context Propagation. That is a way to tied up
requests happening in different systems. The way to
implement that is to Inject span information into the
context you are going to send as part of the protocol you
are using (HTTP or gRPC, for instance). On the other
service you need to Extract the span information out of the
context.
40. Concurrency model
According to Rob Pike, concurrency is the composition of independently
executing computations, and concurrency is not parallelism: concurrency
is about dealing with lots of things at once, but parallelism is about doing
lots of things at once. Concurrency is about structure, parallelism is
about execution, concurrency provides a way to structure a solution to
solve a problem that may (but not necessarily) be parallelizable.
If you have only one processor, your program can still be concurrent, but it
cannot be parallel.
On the other hand, a well-written concurrent program might run efficiently in
parallel on a multiprocessor.
41. Goroutine
A goroutine is a lightweight thread(faster context switching & smaller size(2kB) compared with OS thread(1MB))
managed by the Go runtime.
It has its own call stack, which grows and shrinks as required.
It's very cheap. It's practical to have thousands, even hundreds of thousands of goroutines.
It's not a thread!
There might be only one thread in a program with thousands of goroutines.
Instead, goroutines are multiplexed dynamically onto threads as needed to keep all the goroutines running.
But if you think of it as a very cheap thread, you won't be far off.
The GOMAXPROCS variable limits the number of operating system threads
that can execute user-level Go code simultaneously.
There is no limit to the number of threads that can be blocked in system calls on behalf of Go code;
those do not count against the GOMAXPROCS limit. This package's GOMAXPROCS function
queries and changes the limit.
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
42. Channels
Don't communicate by sharing memory, share memory by communicating!
A channel in Go provides a connection between two goroutines, allowing them to communicate.
// Declaring and initializing.
var c chan int
c = make(chan int)
// or
c := make(chan int)
// Sending on a channel.
c <- 1
// Receiving from a channel.
// The "arrow" indicates the direction of data flow.
value = <-c
43. Channels
Buffered Channels - Sends to a buffered channel block only when the buffer is full.
Receives block when the buffer is empty.
ch := make(chan int, 100)
A sender can close a channel to indicate that no more values will be sent.
Only the sender should close a channel, never the receiver!
Closing is only necessary when the receiver must be told there are no more
values coming!
v, ok := <-ch
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
44. Channels
Channel Directions- When using channels as function parameters, you can specify if a channel is
meant to only send or receive values. This specificity increases the type-safety of the program.
This ping function only accepts a channel for sending values. It would be a compile-time error to try
to receive on this channel.
The pong function accepts one channel for receives (pings)
and a second for sends (pongs).
func ping(pings chan<- string, msg string) {
pings <- msg
}
func pong(pings <-chan string, pongs chan<- string) {
msg := <-pings
pongs <- msg
}
func main() {
pings := make(chan string, 1)
pongs := make(chan string, 1)
ping(pings, "passed message")
pong(pings, pongs)
fmt.Println(<-pongs)
}
45. Channels - Select
The select statement lets a goroutine wait on multiple
communication operations.
A select blocks until one of its cases can run, then it
executes that case. It chooses one at random if multiple
are ready.
A default clause, if present, executes immediately if no
channel is ready.
select {
case v1 := <-c1:
fmt.Printf("received %v from c1n", v1)
case v2 := <-c2:
fmt.Printf("received %v from c2n", v1)
case c3 <- 23:
fmt.Printf("sent %v to c3n", 23)
default:
fmt.Printf("no one was ready to communicaten")
}
46. Channels - Timeout using select
The time.After function returns a channel that blocks for the
specified duration.
After the interval, the channel delivers the current time, once.
func main() {
c := boring("Joe")
for {
select {
case s := <-c:
fmt.Println(s)
case <-time.After(1 * time.Second):
fmt.Println("You're too slow.")
return
}
}
}
47. WaitGroups
To wait for multiple goroutines to finish, we can use a wait group.
func worker(id int) {
fmt.Printf("Worker %d startingn", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d donen", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
worker(i)
}(i)
}
wg.Wait()
}
48. sync.Mutex
We've seen how channels are great for communication
among goroutines.
But what if we don't need communication? What if we just
want to make sure only one goroutine can access a
variable at a time to avoid conflicts?
This concept is called mutual exclusion, and the
conventional name for the data structure that provides it
is mutex.
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
49. Atomic Counters
Here we’ll look at using the sync/atomic package for atomic
counters accessed by multiple goroutines.
func main() {
var ops uint64
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
for c := 0; c < 1000; c++ {
atomic.AddUint64(&ops, 1)
}
wg.Done()
}()
}
wg.Wait()
fmt.Println("ops:", ops)
}
50. Go Fuzzing
Go supports fuzzing in its standard toolchain beginning in Go 1.18.
Fuzzing is a type of automated testing which continuously manipulates inputs to a
program to find bugs. Go fuzzing uses coverage guidance to intelligently walk
through the code being fuzzed to find and report failures to the user. Since it can
reach edge cases which humans often miss, fuzz testing can be particularly valuable
for finding security exploits and vulnerabilities.
51. Best practices
- Do not ignore errors.
- Handle error only once.
- Use Channels for goroutines comunication.
- Return an error instead of using Panic (especially in libs).
- Return an Error interface instead of a custom error struct.
- Do not be lazy; Use interfaces!
- Do not pass function arguments on context.Context.
- Pass context.Context as the first argument to a function.
- Do not use package level vars & func init(magic is bad; global state is magic).
- Handle errors in deferred functions.
- When choosing a third-party lib make sure it is following the GO
Standard lib(context.Context, net/http…).
- Avoid Reflection.
52. What is the output of this program?
var WhatIsThe = AnswerToLife()
func AnswerToLife() int {
println("Hello from AnswerToLife")
return 42
}
func init() {
println("Hello from init")
WhatIsThe = 0
}
func main() {
println("Hello from main")
if WhatIsThe == 0 {
println("It's all a lie.")
}
}
Hello from AnswerToLife
Hello from init
Hello from main
It's all a lie.
Do not use package level vars & func init!
magic is bad; global state is magic!
53. What would we like Go to be better at?
• Error handling (& stack trace).
• Function Overloading and Default Values for Arguments.
• Extension funcs.
• Get Goroutine “result” – like Kotlin Coroutines:
• Null safety
• Define Enum type
• Functional programming
• Macros / inline function
fun String.removeFirstLastChar(): String = this.substring(1, this.length - 1)
val result = "Hello Everyone".removeFirstLastChar()
val x string?
x?.toLowercase()
val deferred: Deferred<String> = async {
"Hello World"
}
val result = deferred.await()