Variables
var
initialises variable to its zero value
word = number of bits used for address space in the system (usually mentioned in the system arch). 64 bit arch = 8 bytes, word = 8 bytes
A string is made of 2 words: 1 word holding the pointer to the beginning of the string, another word holding the size of the string (number of bytes)
go has type conversion over casting for integrity purposes.
Structs (composite type)
Structs are variable types. We don’t use the term objects
of a struct type.
Memory Alignments in Hardware These exist to make reading and writing to memory more efficient. There exist memory boundaries or word boundaries.
Assume we have 2 memory word boundary and if we try to allocate a 2 byte integer as represented in red box in the above diagram, the machine needs to perform 2 read and 2 write operations which is quite inefficient. Instead if we have the 2 byte integer in a single word boundary, it only required 1 read and 1 write operation.
The idea of memory alignments is that, every allocation falls in a single word boundary only. a 1 byte value can fit in any word boundary.
a 2 byte or multiple byte value such as an integer can overflow, hence padding exists. The way it works is, a 2 byte value always start at an address that is multiple of 2, which is 2, 4, 6, etc.
If the word size is 2 bytes and we have the following struct
type example struct {
flag bool,
counter int16
}
Here counter is 2 bytes (16 bits) and flag is 1 byte. word size = 2 bytes The allocation is as follows:
hence the size of the struct variable = 4 bytes and not 3 bytes (1 bool + 2 byte int) due to padding in order for the integer allocation to not take space between 2 boundaries.
The numbers in orange is the address, the 2 byte integer starts at a multiple of 2, i.e 2
in this example.
if it was an 8 byte integer, there would be 7 padding instead of 1 so the integer address starts at 8
.
This situation, we have padding of 1 which is not a problem at all, but sometimes, there can be memory issues, where a struct is taking up much more space than expected due to lots of padding and this calls for micro level optimisation. But avoid this if the code is written for correctness in which case the padding is acceptable for readability.
Example of more padding:
type example struct {
flag bool, // 7 padding
counter int64, // starts at addr 8
flag2 bool, // 7 padding
counter2 int64, // starts at addr 32
}
The above example has padding = 14 due to the ordering of the struct values. This can be avoided by having larger values at the top and smaller ones at the bottom to reduce the padding.
But only optimise if there is a memory issue, always order for correctness and not premature optimisations unless we have a memory profile.
struct literals exist.
named types needs conversion whereas literal types can be converted implicitly.
you can assign a named type value to a literal struct with the same memory layout.
Pointers
Everything in go is pass by value.
OS thread has a stack(contiguous block of memory) of size 1MB go-routine has a stack size of 2KB
Every function call is a frame in that stack. Each function execution is isolated from other function calls as each function is in its own frame. Parameters are passed by value. This is value semantics. This can be in-efficient sometimes as we are always copying over data. It can be more efficient to just shared the data instead of copying it over.
Pointer semantics is used for sharing data across functions compared to value semantics where data is copied.
Passing a pointer to a function is still pass by value and not pass by reference. A pointer is also data
. We are just passing data that contains the address.
Pointer semantics introduce side effects. Functional programming removes side effects by not providing pointer semantics.
go doesn’t have constructors but have factory functions.
Escape Analysis The compiler does static analysis or escape analysis. For example, when a function returns a pointer, it cannot live on the stack as the function frame will be cleaned up, hence the pointer data will be allocated on the heap. This is where the garbage collector comes in.
Escape analysis works by asking if a value is being shared up the call stack, if yes, then construction happens on the heap instead of the stack.
Value semantics remain on the stack.
Example:
func foo() *user {
}
here the return *user lives on the stack. Garbage collectors manages values on the heap.
Never use pointer semantics during construction
u = &user { // never do this, always use value semantics
name: "Bill"
}
but you can use pointer semantics on a return statement or as a function parameter where using a variable for a single line is redundant.
Don’t start the life of a variable as pointer as you cannot tell where it lives.
Stack memory cannot be shared across go routines. heap is used. this is because the stack can be replaced by a bigger stack when required. This changes memory addresses.
Garbage collector
Tri color concurrent garbage collector.
Cost of concurrent garbage collector still causes stop the world latency.
Constants
Constants only exist at compile time. constants have higher level of precision. 256 bits compared to 64 bits for variables.
iota