Skip to content

Golang

A short overview of Go.

Info

go vet ./... can detect common coding errors.

Variables

  • Casting means extending. Go supports conversion. Integrity is most important. Go just creates a new var during conversion.
  • Better to use var a string then a := "hello".
  • Also, a := 'hello' only within a function.
  • var when we want to initialize to zero by default. := (literal initialization) when we want to start with value other than zero.
  • Can declare multiple values at once: var a, b int = 10, 20
  • Also, if declaring multiple variables, we can write like:
vars.go
1
2
3
4
5
var (
    x int
    y float64 = 12.1
    z bool
)
  • := can only be used inside a function.
  • const for constants: const z int = 10.

Struct

structs_1.go
// order is important
// better in descending order of bytes allocation
// but consider only if memory profiling shows otherwise
type example struct {
    counter int64
    flag bool
}

func main() {
    var e1 example // set to zero value

    e2 := example{ // struct literal
        counter: 12,
        flag: true,
    }

    // Anonymous struct
    // better used in marshalling
    var e1 struct {
        flag bool
        counter int64
    }

    e2 := struct{
        flag bool
        counter int64
    }{
        flag: true,
        counter: 12
    }

}
  • Implicit conversion is disagreed.
  • Explicit conversion is promoted. Let's say we have two structs with same struct, values (bill a, alice b, just doing this a = b, can make compiler ambiguous. There can be loss of precision hence we get an error.
  • To avoid it, explicitly state, a = alice(b).

  • All values in a map must be of same types.

  • Not ideal for map to pass data from function to function.
  • When we have related data to group together, we use structs.
structs_2.go
type person struct {
    name string
    age int
}

var arnav person
arnav := person{
    name: "arnav"
    age: 21
}

Anonymous Struct

structs_3.go
1
2
3
4
5
6
7
pet := struct{
    name string
    kind string
}{
    name: "Frodo"
    kind: "dog"
}
  • handy during marshalling.
Note

While dealing with values of named type, there is no implicit conversion. While in literal values, we can.

Factory Functions

  • Functions that take input, process it and return the value.
1
2
3
4
5
6
7
8
func createUserV1() user {
    u := user{
        name: "Arnav"
        email: "arnav@arnav.arnav"
    }

    return u
}

Strings & Runes

  • Strings are immutable.
  • String is string.
  • Rune is a character (int32)
    • var x rune = 'J' // good
    • var x int32 = 'J' // ok but bad

Arrays

  • Arrays are rarely used in Go (look for slices).
arrays.go
1
2
3
var x [3]int
var x = [3]int{1, 2, 3}
var x = [...]int{1, 2, 3, 4} // automatic no of elements guess
  • No negative indexes.
  • Sizes are fixed: Drawback.
  • Better use Slices.

Slices

  • Length is not a part of slice's type.
slices.go
1
2
3
4
5
var x = [...]int{1, 2, 3} // [...] creates an array
var x = []int{1, 2, 3} // creates slice

// just a slice inbuilt function
slice.Equal(x, y) 
  • len() works.

append

  • To grow slices
slice_append.go
1
2
3
4
5
6
var x = []int{1, 2, 3}
x = append(x, 10)
x = append(x, 11, 12, 13)

y := int[]{20, 30, 40}
x = append(x, y...) // append one slice onto other
  • Can't create an empty slice. For that we use make().

make

makes.go
1
2
3
x := make([]int, 5) // all initialized to 0

y := make([]int, 5, 10) // a length of 5, capacity of 10

clear

  • Empty a slice using clear(x)

copy

slice_copy.go
1
2
3
num := copy(destination_slice, original_slice)

// num = 4 (returns no of elements copied)

Slices as buffers

  • When reading data from an external resource (like a file or a network connection), many languages use code like this:

1
2
3
4
5
6
r = open_resource()
while r.has_data() {
  data_chunk = r.next_chunk()
  process(data_chunk)
}
close(r)
- The problem with this pattern is that every time you iterate through that while loop, you allocate another data_chunk even though each one is used only once. - In Go, rather than allocating each time we read from a data source, we can create a slice of bytes once and use it as a buffer to read data from data source:

read.go
file, err := os.Open(fileName)
if err != nil {
    return err
}
defer file.Close()
data := make([]byte, 100)
for {
    count, err := file.Read(data)
    process(data[:count])
    if err != nil {
        if errors.Is(err, io.EOF) {
            return nil
        }
        return err
    }
}

Maps

  • like dict in python.
maps.go
1
2
3
4
5
6
7
8
var nilMap map[string]int

teams := map[string][]string {
    "Name": []string{"Arnav", "Tony", "Frodo"}
}

// using make
ages := make(map[int]string, 10)
  • can automatically grow like slices.
  • can pass to len()
  • Zero value for a map is nil.
  • Maps are not comparable.

ok idiom

  • To check whether a key is present in a map or not.
ok.go
1
2
3
4
5
6
// m is a map

v, ok := m["name"]
fmt.Println(v, ok)

// ok is bool: true or false

Some functions

funcs.go
1
2
3
delete(m, "name")
clear(m) // empty a map
maps.Equal(m, n) // comparing

Sets

  • Not built-in but can simulate using Maps.
sets.go
1
2
3
4
5
intSet := map[int]bool{}
vals := []int{1, 2, 3, 4, 4}
for _, v := range vals {
    intSet[v] = true
}
  • Can also use struct as an empty struct uses 0 bytes but bool uses 1.

Universe Block

  • Go is small. Only 25 keywords.
  • int, string, true, etc are not in that list.
  • They are predefined identifiers. They are always predefined in the universe block i.e. the block that encloses all other blocks.

Conditions

if

  • No () around the if.
  • Because of shadowing, we can declare variables that are only available when needed.
if_statement.go
1
2
3
4
5
6
7
if n := rand.Intn(10); n == 0 {
    fmt.Println("Low")
} else if n > 5 {
    fmt.Println("Medium")
} else {
    fmt.Println("High")
}

for

  • 4 ways:
    • a complete C-style
    • a condition-only for
    • an infinite for
    • for-range

C-style

c_style.go
// ex1
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// ex2
i := 0
for ; i < 10; i++ {
    fmt.Println(i)
}

// more complicated updation rule
for i := 0; i < 10; {
    if i % 2 == 0 {
        i++
    } else {
        i+=2
    }
}

Condition-only (like while)

while_style.go
1
2
3
4
i := 1
for i < 100 {
    i = i * 2
}

Infinite-for

infinite_loop.go
package main
import "fmt"

func main() {
    for {
        fmt.Println("Hello")

        if !CONDITION {
            break
        }
    }
}

// we also have continue

for-range

  • for iterating over built-in types.
for_range.go
// slice/array
evenVals := []int{2, 4, 6, 8, 10}
for i, v := range evenVals {
    fmt.Println(i, v)
}

// map
uniqueNames := map[string]bool{"Fred": true, "William": true}
for k := range uniqueNames { // can leave off the second variable
    fmt.Println(k)
}

switch

  • no () around
    switch_statement.go
    for _, word := range words {
        switch size := len(word); size {
        case 1, 2, 3, 4:
            fmt.Println("Yeah")
        case 5:
            fmt.Println("No")
        default:
            fmt.Println("NOTA")
        }
    }
    
  • No need of putting in break keyword after every case.

Blank Switches

  • Can also leave out the part of comparison in switch.
  • Can be helpful in place of if/else when we have multiple related cases.
    blank_switches.go
    for i := 1; i <= 100; i++ {
        switch {
        case i % 3 && i % 5 == 0:
            fmt.Println("Yeah")
        case i % 3 == 0:
            fmt.Println("No")
        default:
            fmt.Println("Nota")
        }
    }
    

Functions

  • Go doesn't have:
    • named parameters
    • optional input parameters
  • We must supply all the parameters for a function.
  • If we want to emulate named and optional parameters, define a struct that has fields that match the desired params, and pass the struct to the function.
functions_1.go
package main

import "fmt"

type info struct {
    name string
    age  int
}

func MyFunc(infos info) error {
    _, err := fmt.Println(infos)
    return err
}

func main() {
    err := MyFunc(info{
        name: "Arnav", // the age will be initialized to 0
    })
    if err != nil {
        return
    }
}

Variadic Parameters

  • Must be the last (or only) parameter in the input parameter list.
  • Indicate it with ... before the type.
  • The variable that is created within the function is a slice of the specified type.
variadic_params.go
1
2
3
4
5
6
7
func addTo(base int, vals ...int) []int {
    out := make([]int, 0, len(vals))
    for _, v := range vals {
        out = append(out, base+v)
    }
    return out
}

Multiple Return Values

  • Go allows multiple return values.
  • If multiple returns, listed in ().
  • We need to return all the ones specified in the return ().
  • Error should always be the last (or only) value returned from a function.

Ignore Returned Values

  • Go doesn't allow unused variables.
  • Use _ if don't want to use.

Anonymous Functions

  • They don't have a name.
  • Declare with keyword func immediately followed by the:
    • input params
    • return values
    • opening braces
anonymous_funcs.go
1
2
3
4
5
6
7
8
9
func main() {
    f := func(j int) {
        fmt.Println(j)
    }

    for i := 0; i < 5; i++ {
        f(i)
    }
}

Closures

  • Functions declared inside functions are called closures.
  • Means that the inner func is able to access & modify vars declared in the outer function.

Modification

mods.go
func main() {
    a := 20
    f := func() {
        fmt.Println(a)
        a = 30
    }
    f()
    fmt.Println(a)
}

// Output
20
30
// = modifies

No Modification

no_mods.go
func main() {
    a := 20
    f := func() {
        fmt.Println(a)
        a := 30
        fmt.Println(a)
    }
    f()
    fmt.Println(a)
}

// Output
20
30
20
  • Using := instead of = inside the closure creates a new a that ceases to exist when the closure exits.

Returning Functions from Functions

  • Like if x is a function and we assign it to a var say a = x(), then we can also do a().
return_function.go
func makeMult(base int) func(int) int {
    return func(factor int) int {
        return base * factor
    }
}

func main() {
    twoBase := makeMult(2)
    threeBase := makeMult(3)
    for i := 0; i < 3; i++ {
        fmt.Println(twoBase(i), threeBase(i))
    }
}

defer

  • Programs often create temp resources, like files or network connections, that need to be cleaned up.
  • This cleanup needs to happen no matter whether a func completed successfully or not.
  • In Go, the cleanup keyword is defer.
  • We can use multiple defer. The last defer registered runs first.
  • The code within defer runs after the return statement.
defers.go
func deferExample() int {
    a := 10
    defer func(val int) {
        fmt.Println("first:", val)
    }(a)
    a = 20
    defer func(val int) {
        fmt.Println("second:", val)
    }(a)
    a = 30
    fmt.Println("exiting:", a)
    return a
}

// Output
exiting: 30
second: 20
first: 10
Note
  • Go is call by value. In general, this is a good thing.
  • It makes it easier to understand the flow of data through your program when functions don’t modify their input parameters and instead return newly computed values.
  • While this approach is easy to understand, at times you need to pass something mutable to a function.

Pointers

  • The zero value for a pointer is nill.
  • Pointer arithmetic is not allowed in Go.
  • &: address operator
  • *: precedes a var of pointer type. Called dereferencing
pointer_1.go
1
2
3
4
5
6
x := 10
pointerToX := &x
fmt.Println(pointerToX)  // prints a memory address
fmt.Println(*pointerToX) // prints 10
z := 5 + *pointerToX
fmt.Println(z)           // prints 15
  • A pointer type is a type that represents a pointer. It is written with a * before a type name.

    pointer_2.go
    1
    2
    3
    x := 10
    var pointerToX *int
    pointerToX = &x
    

  • Obviously, can't do this: var point *string = &"Hello". Not for primitive types.

Info

Go is a call-by-value language. The values passed to functions are copies. But if pointer is passed, then changes happen.

Note

Immutability should always be in mind unless pointers are really necessary as they make it harder to understand the flow or where the changes happened.

dos_and_donts.go
// dont do this
func MakeFoo(f *Foo) error {
  f.Field1 = "val"
  f.Field2 = 20
  return nil
}

// do this instead
func MakeFoo() (Foo, error) {
  f := Foo{
    Field1: "val",
    Field2: 20,
  }
  return f, nil
}
Tip

The only time we should use pointer params to modify a variable is when the function expects an interface.

  • The Unmarshal func populates a var from a slice of bytes containing JSON. The value passed in for the param must be a pointer otherwise an error is returned.
when_pointers.go
1
2
3
4
5
f := struct {
  Name string `json:"name"`
  Age int `json:"age"`
}{}
err := json.Unmarshal([]byte(`{"name": "Bob", "age": 30}`), &f)
Warning
  • Maps are implemented as a pointer to a struct. The changes are reflected. So be careful while using maps for input params or return values, especially on Public APIs.
  • On an API-design level, maps are a bad choice. Use a struct instead.

Methods

  • Methods are functions with a special receiver argument. This reciever is a value or a pointer to a value of a specific type, which allows the method to operate on that value.
  • Basically, define a function that can operate on a value of that type.
  • These are used in place of objects.
  • Avoid inheritance. Encourages composition.
  • Can declare at any block-level. But can access type only from within its scope.

Syntax

1
2
3
func (receiver ReceiverType) MethodName(parameters) returnType {
    // method body
}

Info
  • ReceiverType: can be a named type or a pointer to a named type.

Method Example

methods.go
type Circle struct {
    Radius float64
}

// method on the Circle type
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

func main() {
    c := Circle{Radius: 5}
    fmt.Println("Area: ", c.Area())
}

Types of Receivers

  • We can have:
    • Value Receivers
    • Pointer Receivers
pointer_receiver.go
type Circle struct {
    Radius float64
}

func (c *Circle) SetRadius(r float64) {
    c.Radius = r
}

func main() {
    c := Circle{Radius: 5}
    c.SetRadius(10)
    fmt.Println("Updated Radius of the circle: ", c.Radius)
}
Tip
  • Use pointer receiver if the receiver struct is large in size.
  • Use value receiver if the receiver struct is small in size.

When to use Methods and Functions?

The differentiator is whether your function depends on other data. As I’ve covered several times, package-level state should be effectively immutable. Anytime your logic depends on values that are configured at startup or changed while your program is running, those values should be stored in a struct, and that logic should be implemented as a method. If your logic depends only on the input parameters, it should be a function.

Interfaces

  • collection of method signatures
    type shape interface {
        area() float64 # return value
    }
    
    type rect struct {
        width, height float64
    }
    
    func (r rect) area() float64 {
        return r.width * r.height
    }
    
    func getArea(sh shape) {
        sh.area()
    }
    
  • Accept interfaces, return structs
  • Are not classes, they are slimmer
  • don't have constructors or deconstructors.
  • define function signatures but not underlying behavior.

Errors

  • An Error is any type that implements the simple built-in error interface:
    type error interface {
        Error() string
    }
    
    # custom error
    type userError struct {
        name string
    }
    
    func (e userError) Error() string {
        return fmt.Sprintf("%v has a problem with their account", e.name)
    }
    
    # use
    func sendSMS(msg, userName string) error {
        if !canSentToUser(userName) {
            return userError{name: userName}
        }
    }
    
  • Also, has a errors package.
    var err error = errors.New("an error message")
    

Order Functions

First Class Functions

  • functions being passed around as data

Higher Order Functions

  • that accept function as an argument or return a function as a return value

Currying

  • takes function, returns function
    1
    2
    3
    4
    5
    6
    7
    8
    func multiply() ...
    func add() ...
    
    func selfMath(mathFunc(int, int)) func  (int) int {
        return func(x int) int {
            return mathFunc(x, x)
        }
    }
    

Concurrency

  • Ability to perform multiple tasks at the same time
  • Synchronous Execution means code is executed one line at a time, one after the other.
  • Use go for concurrency. It spawns a go routine (a new thread) parallely. The current code executes along with that routine.
    go doSomething()
    
  • We can't expect a return value from a routine.

Example

1
2
3
4
5
6
7
func sendEmail(message string) {
    go func() { // run at the sametie as the print statement after the anonymous func
        time.Sleep(time.Millisecond * 250)
        fmt.Printf("Email received: %s\n", message)
    }()
    fmt.Println("Email sent: %s\n", message)
}

Channels

  • typed, thread-safe queue
  • allow different goroutines to communicate with each other
    1
    2
    3
    4
    ch := make(chan int)
    ch <- 69 // channel operator; data flows in the direction of arrow
    // this operation will block until another goroutine is ready to receive the value
    v := <-ch // receives the value
    

Deadlocks & Blocking

  • when a group of goroutines are all blocking so none of them can continue.
  • eg. value is being sent to channel but it is not being accepted by any goroutine.

Tokens

  • empty structs are often used as tokens in go.
  • a token is a unary value (in this context)
  • we don't care what is passed through the channel, we care when and if it is passed.
  • we can block and wait until something is sent on a channel using the syntax: <-ch
  • This will block until it pops a single item off the channel, then continue, discarding the item.

Buffered Channels

  • now they can store upto x
    ch := make(chan int, 100)
    

Closing Channels

  • can be explicitly closed by the sender
    ch := make(chan int, 100)
    
    // do some stuff
    
    close(ch)
    
    // checking if a channel is closed
    v, ok := <-ch // ok is false if channel is empty & closed
    
    // for-range on channels
    for item := range ch {
        // do stuff
    }
    
Warning

dont send on a closed channel, causes panic.

Select

  • sometimes, we have a single goroutine listening to multiple channels and want to process data in the order it comes through each channel.
  • select statement is used to listen to multiple channels at the same time. (similar to switch but for channels)
    1
    2
    3
    4
    5
    6
    7
    8
    select {
    case i, ok := chInts:
        fmt.Println(i)
    case s, ok := chStrings:
        fmt.Println(s)
    default:
        // this case stops the select statement from blocking
    }
    
  • whatever received first, that body will fire.
  • if multiple channels are ready at the same time, one is chosen randomly.

NOTE: Infinite For-Loop

1
2
3
4
5
for {
    select {
        case ....
    }
}

Read-only Channels

1
2
3
4
5
6
7
8
func main(){
    ch := make(chan int, 100)
    readCh(ch)
}

func readCh(ch <-chan int) {
    // can only be read from
}

Write-only channels

1
2
3
4
5
6
7
8
func main(){
    ch := make(chan int, 100)
    writeCh(ch)
}

func writeCh(ch chan<- int) {
    // can only be written to
}

Mutexes

  • Mutual Exclusion
  • only one goroutine can access certain data while others are blocked.
  • type is sync.Mutex and has 2 methods:
    • .Lock()
    • .Unlock()
  • we can protect a block of code by surrounding it with a call to Lock and Unlock as shown on the protected() method below:
    func protected(){
        mux.Lock()
        defer mux.Unlock() // never forget to unlock the mutex
        // all the other thing
    }
    
Info

Maps are not THREAD-SAFE. MUTEX THEM. (not safe for concurrent use)

RW Mutex

  • Lock() & Unlock(): no concurrent read operations (they are safe)
  • RLock() & RUnlock(): allow multiple goroutines to only-read the data

Generics

  • generalizing a type
  • If we have a function, let's say which returns the last element of an array. Now as go is a static typed language, we need to define the type of the array. So, if we have a str array, an int array and other ones, we may need to write multiple functions for the same functionality just because of the different types.
  • Generics allow us to create a simple function but allowing a generalization which is compiler safe.
    1
    2
    3
    4
    func splitAnySlice[T any](s []T) ([]T, []T) {
        mid := len(s)/2
        return s[:mid], s[mid:]
    }
    
  • T is the name of the type parameter for the above function.
  • And it can match any constraint 'cause the body of the function doesn't care about the types of things stored in the slice.
  • var myZero T : zero value

Constraints

  • Sometimes, we need the logic in our generic function to know something about the types it operates on: any can be anything.
  • Constraints are just interfaces that allow us to write generics that only operate within the constraint of a given interface type.
    type stringer interface {
        String() string
    }
    
    func concat[T stringer](vals []T) string {
        // logic
    }
    
    // interface type lists can also be written as
    type Ordered interface {
        ~int | ~string | ~float64
    }
    

Functions to a Struct

package main

import "log"

type myStruct struct {
    FirstName string
}

func (m *myStruct) printFirstName() string {
    return m.FirstName
}

func main() {
    var myVar myStruct

    myVar.FirstName = "John"

    myVar2 := myStruct{
        FirstName: "Mary",
    }

    log.Println("myVar is set to", myVar.printFirstName())
    log.Println("myVar2 is set to", myVar2.printFirstName())
}
- Receivers should be pointers (much faster; recommended by GO)

Sessions

  • for storing the state/sessions
  • a good lib: github.com/alexedwards/scs