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 |
|---|
| 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 |
|---|
| 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.
| 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 |
|---|
| 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 |
|---|
| 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)
|
append
| slice_append.go |
|---|
| 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 |
|---|
| 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 |
|---|
| 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:
| 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
| maps.go |
|---|
| 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 |
|---|
| // m is a map
v, ok := m["name"]
fmt.Println(v, ok)
// ok is bool: true or false
|
Some functions
| funcs.go |
|---|
| delete(m, "name")
clear(m) // empty a map
maps.Equal(m, n) // comparing
|
Sets
- Not built-in but can simulate using
Maps.
| sets.go |
|---|
| 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 |
|---|
| 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 |
|---|
| 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 |
|---|
| 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 |
|---|
| 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 |
|---|
| 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 |
|---|
| 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 |
|---|
| 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
| 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
| 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.
- We can't expect a return value from a routine.
Example
| 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
| 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)
| 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
| for {
select {
case ....
}
}
|
Read-only Channels
| func main(){
ch := make(chan int, 100)
readCh(ch)
}
func readCh(ch <-chan int) {
// can only be read from
}
|
Write-only channels
| 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:
- 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.
| 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