GO
Possible Architecture
- layering
- handlers - external interface for http, rpc, message broker, that converts internal models to external
- service - contains logic and operate with internal models with a help of repo & external service
- external service - can be directly RPC service OR wrapped with custom interface
- repo - contains logic to retrieve data from DB by executing SQL (or similar) queries
- basically a sub-type of external service
- might or might not have own models and errors
- everything is passed through DI for testability
- commonly tests are done on each component as units, repo tests with lightweight DB AND functional tests when you spin-up your app and execute actions with it through public interface
- e2e is also a possibility, when you aren’t mocking external services, but using them
- context is stored as global var that populated on start
- logs can be written to VL or similar solution
- graphana+prometheus can be used for alerts and metrics
Roadmap
- intro
- great lang for BE services, CLI tools and distributed systems with GC, strong types, compilation (to single binary) and strong std, that features performance, great error handling and concurrency management
- simplistic & easy to learn
gocommand features:mod- depth managementbuild- compilationrun- dev runtest- testingfmt- formatting- etc
- syntax
- vars:
- changeable:
varOR:=shorthandvardeclares AND allows to avoid initialization & assignment- if value is not provided, go will assign default values:
0,"",falseORnil(pointers, slices, maps)
- if value is not provided, go will assign default values:
:=always must have value, infers type, can be used inside fn only
- unchangeable:
const- you can use
itoakeyword to create int sequence starting from 0 (will start from zero perconstblock)
- you can use
- can have explicit type OR infer it
- go has concept of shadowing AND scoping (block, fn, package)
- data types:
- int8-64, uint8-64, float32/64, complex numbers, boolean, string, bytes (alias for uint8), rune (alias for int32)
- usually you should use just
int, which is often int32 OR 64, based on architecture - go strict with types and require conversion
- for bools go have: &&, !, ||, ==, != logical operations and checks
- usually you should use just
- statically typed and will error at compile time
- int8-64, uint8-64, float32/64, complex numbers, boolean, string, bytes (alias for uint8), rune (alias for int32)
- composite data types:
- array - fixed size sequence of sam-type elements
- size is part of type
- initialize with zero values
- passed as value
- slice - more robust version of array, that supports dynamic sizing, append and copy operations
- each slice has capacity (can be pre-allocated:
make([]T, length, capacity); for performance), that determines when memory re-allocation occursmakealso creates mapsmake(map[string]int)and channelsmake(chan int)- you can convert slice to array
[N]T(slice)(will panic if slice has lower thenNelements) - if you need to operate on array memory as a slice you can convert it:
array[:]ORarray[start:end]
- each slice has capacity (can be pre-allocated:
- map - key/value data type, that supports data storage (with keys of comparable types)
- except basic map operations you can idiomatically check for existence
value, ok := map[key]- comma-ok idiom used to distinguish presence in map, even in case if value is default
- same idiom can be used to check type assertion without panic:
value, ok := interface.(Type)AND if channel closed:values, ok <-ch
- except basic map operations you can idiomatically check for existence
- string - immutable (manipulations will copy string) sequence of bytes (always encoded as UTF-8)
- to effectively iterate over UTF-8 strings you can use
rune, that will represent UTF-8 char - note that
lenprints number of bytes in str, not runes runecan also be created via''- “ can be used to define strings without escape sequences and WITH formatting preservation
""define regular strings with escape sequences- note:
for rangewill iterate string by runes, BUT index access will return bytes
- to effectively iterate over UTF-8 strings you can use
- struct - used to define interface for object with methods only
- functions attached separately
- struct support tags, that define how to serialize data into struct, ex:
json:"name,omitempty"omitemptywont add field to JSON if it has zero valuejson:"-"will prevent (un)marshal- note that not exported fields are ignored in (un)marshal process
- struct can be embedded with other struct, enabling composition pattern
-
- array - fixed size sequence of sam-type elements
- changeable:
- vars:
type Address struct { Street, City, State string }
type Person struct { Name string Address }
p := Person{
Name: “Alice”,
Address: Address{“123 Main St”, “Anytown”, “CA”},
}
fmt.Println(p.Street)
```
- conditions:
- if and if-else
- if supports initialization in condition: if num := 9; num < 0 {...}
- switch
- supports: multiple values per case, type switch, expression & fallthrough (need to define by fallthrough keyword)
- loops
- for is basic and single loop in go, supports condition, post statement, initialization, continue and break with labels for nested loops
- for initialisation; condition; post { ... }
- for range iterator style syntax that can be used for arrays, slices, maps, strings, and channels
- returns index/key (except channel), value for all types
- _ syntax is used to omit value
- notes:
- map:
- can’t be modified on iteration (except deletion)
- iterates in random order
- string:
- iterates by runes
- goto can be used to move control flow to chosen label within fn
- docs
- go has Godoc for comments as docs
- you can add comment before fn to act as inline doc AND at start of file to act as doc for whole file
- supports markup
- can be parsed by Godoc package to form HTML view of docs
- you can write specific types of tests, that will act as example, by using specific file name example_{FUNCTIONALITY_NAME}_test.go AND fn name (starts with Example)
- functions
- first class citizens (can be assigned to var, passed as param)
- has multiple returns (funct name () (Type1, Type2, commonly used for error handling), named returns (funct name () (res1 int, res1 int) { res1 = 1; res2 = 2; return; }, allows for variadic parameters (of same type, func name (args ...Type), treated as slice inside fn)
- can be anonymous (func(){}), which are common for lambdas OR as var
- functions create closures
- all variables accepted by value, except pointers, slices and maps (pointer itself is still copied, BUT memory, that it points to, isn’t)
- pointers
- declared as *Type, pointer of value obtained via &Var, value is read or assigned via *p OR *p = smth
- for structs: (*p).field OR p.field shorthand
- can’t do arithmetic
- memory is managed via GC, BUT it is still important to omit copying large amount of data, leaking memory with redundant allocations
- GC runs concurrently with program and tries to avoid large resource consumption
- methods
- similar to functions, BUT act as methods on defined types and can have pointer/value receiver
- pointer receiver is used for large structs OR when state modification needed func (p *Type) name () {}
- value receivers can also be used, even if called on pointer (dereferencing will be done under the hood) funt (v Type) name () {}
- interface
- used to define type without implementation
- interface{} accepts any type
- commonly used for generics before generics
- used to handle unknown data
- to use anything we can narrow type by:
- assertions
- value.(Type) with panic OR more safer value, ok := value.(Type)
- type switches
- switch v := i.(type) { ... }
- type Name interface { fn1 () ReturnType}
- can be embedded for composition (same as structs)
- generics
- introduces reusability without sacrificing performance OR strong types
- func name[T ConstrainType] (s T) T
- for any you can use interface{} OR any
- can be called like name[int] (...) OR generic can be inferred
- structs can be generic too type Struct[T any] struct {}
- you can build own constrains:
- type Number interface { int | int8 | float32 }
- type Number {int Calc () int}
- type Number interface { ~int | ~int8} to allow accepting types, who has int or int8 underlying type
- (they can be inlined, using same syntax)
- there is constraints package that have expected Float, Integer ect constraints and more specific: comparable, ordered
- errors
- errors are values that need to be explicitly handled
- created via fmt.Errorf() OR errors.New()
- errors.New great for static error messages OR predefined constant errors (Sentinel Errors)
- fmt.Errorf great for formatted errors, with possibility to pass other error via %w to wrap it inside, while preserving context
- errors.Is(wrapped, base) OR errors.As(wrapped, baseType) can be used to work with wrapped errors
- except default %s, %d, %f you have:
- %+v for key:val
structs
- %T for type
- %q for quoted string
- always returned as last value, defaults to nil and handled as if err != nil
- built-in error interface has single Error() string method that returns text representation of error
- for exceptional cases you have panic() that will unwind the stack and kill app
- to prevent app death you have recover() in deferred functions
- code organization
- module (app) in go is defined by
go.modfile, which includes path (often repo URL) to module & dependenciesgo mod initcreates basic modulego mod tidycleans-up, adds and optimizes depsgo.sumchecksum updated as result for reproducible builds
go mod vendorcreates minimal required local copy of all depth- app will fail in case of mismatch between vendor and actual deps
go getinstalls deps
- code is broken by packages
- defined by first line via
package name - exports are defined by capital letter
- import is done as:
import "name"import ( "name1", "name2" )import localName "name"
- rules:
- no circular imports
- main package for executable
- lowercase naming
- import path must be uniq
- to publish you required to share module via version control system with semantic versioning
- notes:
- files in package can “see” each other
- all files are compiled as single package, that can be linked to other package
- dead code elimination is done in compilation step to omit any unreachable from main symbols
- defined by first line via
- module (app) in go is defined by
- concurrency
- goroutines is way to execute concurrent, multi-threaded code
- lightweight and managed by runtime
- communication is done via channels
- syntax:
go fn()to execute fn as goroutine
- note that
maintermination will kill goroutines, it won’t await them by default, BUT we can block it via channels - data is shared via channels
make(chan, Type)- can be buffered (queue N entities in memory) OR unbuffered (block channel until entity in processing)
- you can multiplex several channels via
select/casestatement- will block until any
caseordefaultis ready - will execute
caseof ready channel OR random if multiple ready - will execute
defaultimmediately if other are blocking - use-case: channels with timeouts
- will block until any
- while gorouting is lightweight it might be beneficial to limit it by some amount via workerpool (allocate some fixed amount of goroutines and buffer other requests)
- for better management you can use
sync, that provides: mutex, reader-writer mutex, waitgroup (wait until all goroutines finish), once (call fn once even if done from multiple routines)context, that provides:- value passing down the stack
- deadlines with cancelations
- check
ctx.Done()and abort
- check
- ---
- often passed as first request
- use-cases: HTTP timeouts, database deadlines, goroutine cancellation coordination
- patterns:
- fan-in - merge work (done by select)
- fan-out - distribute work for faster parallel execution
- pipeline - chain parallel working steps
- you can use
-raceflag to detect unsynchronized used of shared memory by different routines
- goroutines is way to execute concurrent, multi-threaded code
- go has reach standard lib:
ioto handle files and streams- and it’s brother
bufiofor buffered io operations (better performance due to reduced sys calls)
- and it’s brother
osfor file, env, process, sys and network operations- cross-platform
flagsimple util for parsing CLI flagstimeworking with time- has rich timezones support and handles edge-cases
jsonmarshaling & unmarshaling structs into JSONlogfor logging- and newer
slogfor logs with built-in levels, cleaner key-val interface AND json formatting
- and newer
regexpfor RegExp- support compiled expressions to avoid recompiling
go:embedto create self-contained binariestesting- file must end with
_test& function must start withTest- fn accepts
t *testin.Tthat is used to validate results
- fn accepts
- goes encourage pattern of simple loops in tests, where you accept slice of structs to run against same setup (table-driven tests)
- mocks & stubs for checking fn calls & stubbing response in combination with DI is used to test without dependencies
httptestpackage can be used to record and emulate network requests without network- if fn starts with
Benchmarkyou can runb *testin.Bto write performance tests and run them viago test -bench=. go test -coverexposes coverage
- file must end with
net/httpfor building RESTful web servers- provides TLS, HTTP/2, cookies & multipart form handling
database/sqlfor executing queries against DB- go also supports extended libs like
pgxfor different DBs, ORMs & query builders - consider using tooling for migrations & connection pools
- go also supports extended libs like
- for ws and SSE you can use existing libs
- tooling
- standard:
go runexecute file without buildinggo buildbuild program as binary- cross OS, has custom flags, optimized by default AND creates single statically linked binary (by default)
- artifact is easy to deploy
- note that for conditional build (platform specific code, feature flags etc) you can utilize
//go:buildtagging -gcflags="-m"can be used to show what vars was heap allocated and utilized GC
go install package@versionto build and install programs system-wide- commonly used for CLI tooling
go fmtstandard opinionated formattergo mod init/tidy/downloadfor dependency managementgo testto find & run tests, examples & benchmarks- has coverage & parallel running
t.Parallel()marks test as possible to run concurrentlygo testwill rungo vetto check for potential bugs (incorrect printf, dead-code, problematic struct tags, nil pointer dereferencing)GOOS&GOARCHenv vars control targeted OS & architecture version
go cleanto clean package & build cachesgo docto extract docs of package/fn from go doc commentsgo versiongo generatefor executing commands in//go:generatecomments- use-cases: embedding data on build-time, meta-programing, code gen from templates
- can be used to avoid bash scripts and inlining & running needed commands in go file
go tool pproffor profiling (CPU, RAM, goroutines, blocking opearions)go tool tracefor analyzing traces (goroutines, sys-calls, GC, scheduling)- can be added by
runtime/trace
- can be added by
goimports- automatically remove & resolve missing imports- linting
golangci-lint- parallel runner for quality checkersstaticcheck,revive- configurable third-party linters
govulncheck- CVE finder for dependencies
- standard:
- memory management in depth
GOGCcan be used to increase/decrease heap-size (memory usage), while decreasing/increasing CPU load- 100(%) is default value and state that heap can use additional 100% of currently used RAM by program itself
GOMEMLIMITcan used to balance highGOGCby limiting max allowed memory for program- soft limit to avoid program stolling itself
- keep it 10% below actual limit of container
- focus on actual physical memory, go holds to reserved vMemory
reflectionstd package can be used to examine & manipulate unknown data (ex: ORM, JSON Marshaling) with additional performance overhead’unsafeallows for C-style memory management & pointer arithmetic- CGO can be used to link C and GO code via comments in both directions
- has performance cost and breaks cross-compilation & static linking
- non-preferred
- GO supports dynamic imports called plugins
- plugin must follow
type interfacethat defines interface and calledSomeNamePlugin - plugin must export
Pluginvar - must be built with
go build -buildmode=pluginas.sofile - in app is read from path, extracted by symbol and casted to type
- problems: unix-only, complex, versioning problem
- plugin must follow
Effective GO
- formatting
- tabs over space
- no line limit
- reduced number of parentheses (spacing to order math operations, no need for parentheses in control structs)
- naming
- package is imported by name AND it’s name is accessor
- should be short, consistent, understandable, lowercase and single word
- should match it’s directory name
GetName->NameToString->StringReaderfor single method interfaces- camelCase
- package is imported by name AND it’s name is accessor
- semicolons
- auto-inserted
- required in control statements, as delimiter for single line expressions
- control structs
- support internal var initialization
if err := fn(); err != nil { ... } :=can be used for reassignment and declarationcase ' ', '?', '&', '=', '#', '+', '%':is allowed- empty
switchwill fire ontrue deferwill run clean-up fn before fn returns- vars are evaluated when
deferis inited - multiple defers will be executed in LIFO order
- vars are evaluated when
- support internal var initialization
- data
- allocation
new(T)allocs zeroed memory and returns pointer to that region&Struct{}==new(Struct)
make(cn/slice/map)initializes underlying data structure with given params, returning value itself
- array
- useful for memory layouting & to avoid additional RAM usage
- foundation for slices
- arrays are treated as values
- type of array is bound to size
- slices
- richer for of array
- treated as references
- maps
- treated as references
- similar to slices, BUT represent a key-val data struct
- key is any comparable type
- double
deleteis safe
- printing
- Printf, Fprintf, SprintF - format string and output to stdout, passed buffer, as string
- Print, Println - take any number of values, format by type, join by
""or"\n"into output and print - for maps output is sorted by key
- custom
String()on interface can be defined to specific%vformatting
- allocation
- initialization
- constants
- compile time
- can have methods on them (as all other types, except pointer and interface)
- var
- expression can be runtime
- flow:
- evaluate dependency
- evaluate vars
- call
initfunction defined by file
- constants
- types
- type conversion may produce new value (int-> float) OR temporary change value type to allow accessing methods
- failed type conversion with
ok:=will return zero value
- failed type conversion with
- type conversion may produce new value (int-> float) OR temporary change value type to allow accessing methods
- blank identifier
- works similar-ish to
/dev/nullin Unix import _ "path"can be used to import smth to trigger side-effectvar _ T = (*Struct)(nil)statically check ifStructsatisfiesT’s interface- only used if you can’t do it via actual live code
- works similar-ish to
- embedding
- interfaces can be embedded with interfaces
- struct can be embedded with ref to other struct
- notes:
- shadowing will work from top to bottom (
A.mwill shadow embeddedA.B.m) - having same name on same level will cause error
- shadowing will work from top to bottom (
- concurrency
- due to channels each piece of data is only present in one routine at a time
- channel is first-class citizen and can be passed via channels for two-way communication
runtime.NumCPU()provides number of cores for parallel execution- or
runtime.GOMAXPROCS(0)if we wan’t to honor user requested num of cores
- or
- errors
- prefix your errors for better understanding of them
- when running long-running processes (ex: web-server), always have top-level recover to kill failing routines and preserve server
panicinside deferred fn will continue unwinding stack- re-panic will preserve stack trace
100 Mistakes
- be aware with shadowing
- don’t nest too much
- early return if possible
- don’t overuse init fns
- problems:
- only possible to init state in global vars
- may introduce dependencies, that will complicate testing
- limited error handling
- use-cases:
- static config initialization
- problems:
- don’t introduce redundant getter/setter
- avoid redundant interfaces & over-abstraction
- mostly clients should own abstractions, producer may expose minimalistic when needed
- same applies for return arguments
- generally avoid
any - generics introduce complexity, so don’t overuse
- avoid redundant type embedding
- redundant when you want
Foo.Baz()instead ofFoo.Bar.Baz() - redundant when you leak internals this way
- redundant when you want
- use builder OR options patter for complex structs
- options simplifies error handling & omits dealing with empty struct for state
type Option func(options *options) error[]
- have proper project organization
- decide on folder grouping that solves your problem and stick with it
- best practices:
- granularity is key
- don’t overuse packages prematurely
- choose proper name
- avoid generic names like
utiletc
- avoid generic names like
- avoid exporting too much
- declare package with name of repo URL, if it will be hosted
- place internals into
internalbecause it can’t be imported by consumer - recommended to place command packages under
cmddir
- be aware of package & var name collisions
- don’t disregard docs
- comments for exported code & packages
- start with name
- comment must be a sentence with punctuation
- must describe behavior & intention, not implementation
- should be enough info to use code without examples
- docs for all code
- should provide internal info & how code does things
- comments for exported code & packages
- use linters, formatters & analyzers
- use longer base identifier for number &
_delimeter for better understanding of code (0->0o) - be aware of int overflow
- silent at run-time
- causes compilation errors for constants
- remember that floating point arithmetics implies precision errors
-
When comparing two floating-point numbers, check that their difference is within an acceptable range.
-
When performing additions or subtractions, group operations with a similar order of magnitude for better accuracy.
-
To favor accuracy, if a sequence of operations requires addition, subtraction, multiplication, or division, perform the multiplication and division operations first.
-
- account for slice capacity & proper initial length
- nil slice vs empty slice
nilslice - no underlying array,len == 0,cap == 0, andslice == nilistruevar a []int- doesn’t have allocation
- empty slice - usually has
len == 0,cap == 0, butslice == nilisfalse[]int{}
- behaves differently in APIs and JSON
- when designing API choose carefully the intended behavior
copywill copy elements partially if slice len is smaller- if two slices share array, append may modify both if capacity allows for it
- better to avoid shared arrays OR limit capacity of copy to trigger re-allocation
- note that if one slice with shared array exists second can’t be freed
- this especially harmful with pointers to structs
- don’t create slices with redundant size
- init map with number of elements if possible
- map can’t shrink in size
- store pointers that can be zeroed
- re-create smaller map
- comparison edge-cases:
- channel -> check if both nil OR both created by same
makecall - interfaces -> check if both nil OR have dynamic types & values
- pointers -> check if both nil OR point to same memory
- structs & arrays -> compare val by val (only when composed of similar types)
>etc can also be used for lexical string comparisonreflect.DeepEqualcan be used with slices & maps- critical for performance
- alway look for standard comparison methods like
bytes.Comparefor performant[]bytecomparison - comparing non-comparable will panic
- channel -> check if both nil OR both created by same
- control struct edge-cases
rangeloop exposes copy of valuerangeloop evaluates copy before loop itself- adding element into map while doing iteration MIGHT still expose this element in current iteration
- defer won’t execute in each loop cycle, only before fn returns
- strings edge-cases
- trim removes all runes from set, trimSuffix/trimPrefix remove substring
- use
strings.Builderwhen appending to str in loop instead of+=which re-creates string over and over- don’t forget to pre-allocate proper buffer size via
.Grow - can’t be used concurrently
- don’t forget to pre-allocate proper buffer size via
- avoid
byte[]tostringconversion if possible - creating sub-string from string can lead to memory leaks just like in slices
- not that numbers correspond to bytes not runes here
- function edge-cases
- diff pointer & value param
- if param non-copiable, large or must be mutated - pointer
- map, channel & fn are always passed by val
- when returning
nilassigned to var of interface type you actually returning interface value, SO better to return justnil - defer params evaluated immediately
- to reference dynamic var use pointer to it OR closure
- diff pointer & value param
- error management
- wrapping
- wrap to add context
- %w to preserve error
- %v to transform error
- wrap to change type
- new error type is required
- wrapped errors can be compared via
errors.As
- wrap to add context
- known errors converted to sentinel once, unknown should be wrapped under custom error type
- log error on higher level to avoid double logging
- mark error as ignored via
_to make action intentional (additional comment is preferable)
- wrapping
- concurrency (enables parallelism, but not actually it)
- it won’t always be faster then sequential
- channel vs mutex
- when we need synchronization prefer mutex, BUT when we need some form of communication prefer channel
- channel preferable for concurrent flows, WHILE mutex for parallel
- context:
- has deadline - when to stop activity under context
- has cancelation signal - stops activity on demand
- has values - key-value list of any data
- http context is canceled when request is cancelled OR we send back a response
- channels:
- sent value is consumed by single receiver
- cancelation is fanned-out to all receivers
- just to send some signal, use empty channel
chan struct{} - sending & receiving from
nilchannel is forever blocking, BUTnilchannel i auto-removed fromswitch, so it might be useful - buffer size
- keep identical to pool size, keep similar to rate-limiting requirements, remember that > Queues are typically always close to full or close to empty due to the differences in pace between consumers and producers. They very rarely operate in a balanced middle ground where the rate of production and consumption is evenly matched. — and you need to tweak your config
- any clammed resources and started goroutines must be freed & closed, BUT some close operations aren’t sync, so we also need to do it in blocking manner
- string formatting can cause deadlocks, when implementing Stringer interface
- append on slice can cause data race due to shared backing array, thus create copy of it
- mutex can be used upon critical section only
wg.Addcan’t be called inside goroutine itselferrgrouppackage can be used to introduce shared cancellable context & error handling upon waitgroupssynccan be used only via pointer, otherwise you risk internals duplication
- std
- always use time duration for clearness
- JSON issues
- embedding structs with Marshaller interface will break default json marshalling
time.Timeis compared by both monotonic and wall clocks- untyped conversions will result in
float64numerics
- SQL issues
.Openwon’t always try to establish connections, usePing- configure proper pool size of connections
- prepared statement are more efficient and secure
- nullable columns are handled via pointer OR
sql.NullXXXtypes - when iterating over
.Rowscall.Erron each cycle
- don’t forget to return after responding to http request
- default HTTP client is missing timeouts & other nifty things
- testing
- optimizations