👨🏻‍💻

Ultimate Go Programming: Introduction

Variables

  • Type is Life
  • Type gives two pieces of information:
    • Size
    • Representation
  • `var` initializes the variables to zero value
  • string → 2 words
    • word size varies based on architecture
      • In 32 bit like playground it is 4 bytes
      • In 64 bit is 8 bytes
    • [h|e|l|l|o] in an array -> [pointer / size of the array which is 5]
  • Go converts variables if type changed and never casts

Struct types

💡
Everything in Go is Pass by Value
  • Every code in the world ever written is either reading memory or writing memory or allocating memory
  • Data
    • Value
    • Address
  • Try to use data types with larger Bytes in the beginning and smaller one at the end while defining structs.

Pointers

  • For every stack 1MB memory has been allocated in OS level
  • For a Goroutine the stack size is only 2KB. (refer the diagram)
  • When a program needs to be executed a frame of memory (FM) allocated to the goroutine not the entire stack. All of the program needs to run only that frame of memory as goroutine will not have access to the memory outside of the FM.
  • This FM is creating a sandbox, a layer of isolation. It's giving us a sense of immutability that the goroutine can only mutate or cause problems here and nowhere else in our code. This is very, very powerful constructs that we're gonna wanna leverage and it starts to allow us to talk about things like semantics.
👉🏼
"The stack is for data that needs to persist only for the lifetime of the function that constructs it, and is reclaimed without any cost when the function exits. The heap is for data that needs to persist after the function that constructs it exits, and is reclaimed by a sometimes costly garbage collection." - Ayan George
  • Every time we do a function call we move out of program boundaries i.e. we change FM from the same stack.
  • FM or memory below the Active frame are of no use and no integrity, but above are good and valued.
  • Mechanics: how things work
  • Semantics: How things behave
    • Value semantics
      • It provides isolation based on FM/function
      • Reduces side effects on another part of the program
      • Performance can be more as it uses only stack memory
      • But, makes multiple copies
      • Takes more memory
    • Pointer Semantics
      • By passing the address data, goroutine can change a data value outside of the active FM.
      • Can use dynamic memory i.e., heap memory
      • When a function shares address data with the calling function up in the call stack the escape analysis (static code analyser) allocates the memory in the heap not in the stack anymore.

Escape analysis

  • I is a static code analyser which determines if a value needs to be remain on the stack or escaped in to the heap memory
  • When a memory allocation happens in the heap, the garbage collector comes into the picture. In C we need to free the memory from heap manually using free() command, here the compiler handles it.
  • Always use value semantics during construction i.e. creating a struct.

Stack growth

  • If the stack gets overflown, then a new stack with 2 * 1.25 = 2.5 KB size stack gets created and old values of the stacks gets copied to the new one. Latency hit happens. However, this is done for
    • Integrity &
    • Minimizing resources
  • This is a unique concept for any language.
  • Since a value can move in memory that's on the stack, what this means is,
    • no stack can have a pointer to another stack. One of the constraints or engineering choices here in Go is, since our stacks can move, it means that the only pointers to a stack would be local pointers.
    • Only that stack memory is for the Go routine, then the Go routine only. Stack memory cannot be shared across Go routines. Again, this is where the escape analysis come into the heap.
    • The heap basically now is used for any value that's going to be shared across Go routine boundaries, and any value that casting on the frame because there's an integrity issue, or any value where we don't know the size at compile time.
These are our three constraints around allocations in Go. Again, it's because we want to really be able to have hundreds of thousands of Go routines, which means we need to be able to have hundreds of thousands of stacks, we couldn't do that if they were MG. They have to be as small as possible. We take the small hit, always for integrity and for uses, small memory usage, because that is our higher priority in Go. What we care about is not that our code is the fastest it can be, we care about is it fast enough.

Garbage Collection

More can be found here on GC:
  • Heap memory gets created for non-literal type variables (like lambda functions dynamic)
  • Garbage collector
    • Performance can lack due to
      • Disk I/O or network latency &
      • Allocation in Heap memory which involves GC
    • Pacing algorithm
      • Smallest heap size
notion image

Constants

  • There's two types of constants:
    • Constants of a kind and
      • Your literal values in Go are constants of a kind, they're unnamed constants, constants of a kind can be implicitly converted by the compiler, which means that you can't really have enumerations in Go. You're not gonna get those compiler protections.
      • Remember, constants of a kind can have up to 256 bits of precision, we got really like a high precision calculator in Go. And then again, those literal values are all constants of a kind.
    • Constants of a type.
      • Once a compiler is based on a type, then the full laws of type are gonna restrict its ability to be anything other than its particular precision.

Why Go has only Arrays, Slices and Maps data strucuture?

  • Contiguous memory allocation makes the prefetcher to pre-populate the memories into cache.

Arrays:

  • LinkedList vs Matrix --> Row traversal is faster.
  • The main memory is very slow like it is not there at all
  • Total amount of cache in an i7 intel processor is 8MB combined memory of L1, L2 and L3 memory.
  • Small data is fast
  • Mechanical sympathy
    • In modern hardware systems the cache is everything, every time we miss a cache hit, it takes around 300 cycles to fetch that from the main memory (RAM)
    • To get around from this issue, we need the values our code needs to access to be there already in the cache before the code executes. This can happen by the prefetcher of the OS if the code memory access happens in a
      • Predictable way &
      • Under the same OS page
    • For these reasons Go does not have stack, queue or linked list type of data structure in them.
    • Slice are the most important data structure for Go. Slices internally uses Arrays.

Slices

  • Most important data structure of Go
  • There are three types in go:
    • Built in types
      • There are few built in types:
        • Int
        • Float
        • String
        • Bool
      • Value semantics need to be used for these types, including fields in struct i.e. no pointer reference
  • Struct types or User-defined types
    • The default is Pointer semantics, if you are purely sure what are you doing then use Value semantics.
  • Reference types
    • There are the following reference types
      • Slice
      • Map
      • Channel
      • Interface values
      • Function
    • These are all data structures with Pointers.
    • When they are initialized i.e. zero values, the value gets set to nil. When a string is set to zero value its value becomes empty therefore it can not be a reference type.
    • Same as Built-in types. Value semantics need to be used. No pointers.
    • There's one exception to this, however. A slice and a map, you may take the address of a slice or a map only if you're sharing it down the call-stack and to a function that's either named decode or un-marshall.
  • If length is set t=in the make call then the capacity matches the length
  • A slice is 3 word length in AMD64 compiler each word is 8 Bytes length
  • Difference between empty and nil slice
    • Var Fruits []string
      • Here the values will be zero valued i.e. the slice is initialized
    • Fruits := []string{}
      • Here the values will be empty i.e. empty is assigned to them
      • It has a pointer and it points to an empty struct
  • Empty Struct:
    • Var es struct{}
    • Allocation size is zero, coz there is a 8 byte value tied to it inside runtime like a global variable every time Go runs.
  • Till 1024 size of the slice the capacity doubles on every capacity increase append calls
  • After it reaches 1024 size, it adds 25% (i.e. 1.25 * current capacity) of the capacity on each increased capacity append calls
  • Capacity if we know can be allocated up front, in fact we an get rid of the append call
  • Append works based on the position of the length value
  • Never create a new slice by appending to original slice, if you know what you are doing then fine. The original slice remains there and memory leak may happen.
  • Always try to use the make operator while creating the slices.
  • UTF-8 is a three layer character set.
    • You have bytes at the very bottom, and really, we would always consider strings just to be bytes at the end of the day. You've got bytes at the bottom.
    • In the middle you have what are call code points. And a code point is a 32-bit or 4-byte value.
    • And then, after code points, you have characters. And the idea is that a code point is anywhere from one to four bytes. And then a character is anywhere from one to multiple code points. You have this, kind of like, n-tiered type of character set.
  • In Non-English characters, three bytes for encoding of that character. All total one code point for one character. In English we need
  • Rune is an alias for int32. byte is also an alias for uint8
  • One of my favourite sayings in Go, is that every array is just a slice waiting to happen.

Decoupling

  • If a method has to mutate the data then use pointer receiver/parameter else use value receiver.
  • Before starting to code whether to use value semantics or pointer semantics we need to choose, we can access the following question:
    • Whether the method will update the data or make a copy of the data.
    • Like in the case of Time method it adds 5 seconds and create a new data, but in case of a user it updates the user name, therefore, the name points to the same user so update the data.
  • Therefore, whenever update use pointer and copy use value semantics.
  • Factory function can be used to check the semantic used.
  • Once semantics has been chosen rest all methods need to use the same semantics. Not mix with multiple semantics.
  • You do not have a right to make a copy of a value that a pointer points to when it's been shared with you like that. Assume that it is dangerous to make copies if something has been shared. Put that in your head, that is a major violation of semantic law. We do not make copies that pointers point to. Assume that that is very, very bad.

Interfaces

  • Polymorphism: A piece of code that changes its behaviour depending on the concrete data it's operating on.
  • There's nothing real about an interface, nothing at all. There's an implementation detail behind r, but from our programming model, r does not exist. It is not real, there's nothing concrete about it. This is gonna be an important concept for us to follow through with as we continue to look at this code. Okay, so I've defined this interface type as one active behavior read. And one of things you know we're looking at mechanics and we'll talk more about this. It's very important that your interfaces define a behaviour. Verbs, right? From my perspective, we're reading, we're writing, we're running, we're printing, right. Interfaces are about behavior. I don't wanna see interfaces that describe things. I don't wanna see an animal interface or a house or a user. These are not interfaces, those are not behaviors. Those are things, that's your concrete data. The more you get away from that and the more your interfaces describe behavior, here we've got reader, has one active behavior, read, this is behavior, we're gonna be much better off with the decoupling because again, we're focused on what? Behavior. Behavior, that's it.
  • Interface types are valueless.
Design Philosophy:
  • Interfaces give programs structure.
  • Interfaces encourage design by composition.
  • Interfaces enable and enforce clean divisions between components.
    • The standardization of interfaces can set clear and consistent expectations.
  • Decoupling means reducing the dependencies between components and the types they use.
    • This leads to correctness, quality and performance.
  • Interfaces allow you to group concrete types by what they do.
    • Don't group types by a common DNA but by a common behavior.
    • Everyone can work together when we focus on what we do and not who we are.
  • Interfaces help your code decouple itself from change.
    • You must do your best to understand what could change and use interfaces to decouple.
    • Interfaces with more than one method have more than one reason to change.
    • Uncertainty about change is not a license to guess but a directive to STOP and learn more.
  • You must distinguish between code that:
    • defends against fraud vs protects against accidents
Validation:
Use an interface when:
  • users of the API need to provide an implementation detail.
  • API’s have multiple implementations they need to maintain internally.
  • parts of the API that can change have been identified and require decoupling.
Don't use an interface:
  • for the sake of using an interface.
  • to generalize an algorithm.
  • when users can declare their own interfaces.
  • if it's not clear how the interface makes the code better.

Error Handling


⚠️Disclaimer: All the screenshots, materials, and other media documents used in this article are copyrighted to the original platform or authors.