tags : Programming Languages, Concurrency in Golang
Compiling, Linking and OS dependencies
Syscalls
How does go make syscalls?
- Differs by Operating system
- In Linux, because it has a stable ABI, it makes the syscalls directly skipping libc. w Linux, the kernel to userspace interface is stable(which syscalls use), but the in kernel interfaces are not stable.
- Enabling or disabling CGOhas nothing to do with whether or not go syscall goes vialibc, the C code used viaCGOofcourse will uselibc.
- Go does create wrappers around some syscalls (TODO: Need to dig into this)
- Support for different syscalls in different OS is incremental. Eg. If Xsyscall is available in OSAandB. Go might have support forXonly inAas of the moment.
Places where go handles syscalls
- Try fd syscall -ton the go source tree.
- /syscall : Frozen, except for changes needed to maintain the core repository.
- /internal/syscall : internal
- /runtime/internal/syscall : internal, some details
- /x/sys
- This is where new stuff goes and should be used by callers
- Contains 3 packages to hold their syscall implementations(Unix, Windows and Plan 9)
- Has the wrapper creation libraries such as mkwinsyscall.go and mksyscall.go
 
Portability
- syscalls are not portable by nature, they are specific to the system. We need to add a build tag for the ARCH and OS that syscall invocation is valid for.
Dynamic and Static Linking
Static linking
- 
Directly - CGO_ENABLED=0 go build
- Matt Turner - Statically Linking Go in 2022
 
- 
musl - Statically compiled Go programs, always, even with cgo, using musl
- You can also statically link with musl, but note that musl lacks features that people might want to use non-pure Go in the first place. For example, musl does’t support arbitrary name resolvers, e.g. no LDAP support; it only supports DNS, just like the pure Go net package.
- On the other hand musl does support os/user.
 
- 
glibc - STATIC LINKING OF GLIBC IS DISCOURAGED
- It makes extensive internal use of dlopen, to load nsswitch modules andiconvconversions.
 
Dynamic linking
- 
glibc versioning 
CGO
CGO is essentially utilizing C api calls to shared libraries exporting C interface. It is a tradeoff.
Using CGO
- CGO_ENABLED=1
- Some things are only available as C libraries, re-implementing that in Go would be costly.
- CGO is also used in some parts of standard library. Eg. (net and os/user). It’s not a strict requirement though, you can use these packages w/o CGO and they’ll use stripped down version written in Go. But if you want the full thing, you have no other option than to enable CGO
Without using CGO
Cross Compilation
Cross compilation in Golang
- Unless you’re using a native cross compiler(eg. Clang, Golang Compiler), to cross-compile a program, you need to separately build and install a complete gcc+binutils toolchain for every individual arch that you want to target.
- Which Go this is easy(cross compiler out of the box) + dependencies ensured to support.
Cross compilation and CGO
CGO allows us to access C libraries in the system we’re building/compiling on. It has no idea about C libraries of other systems. So mostly CGO is disabled by default if cross-compiling. However, if you need to cross-compile go code with CGO, you need a cross-compiling C compiler for the target machine. It can be done but it is a bit of PITA.
- 
Using Zig CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC="zig cc -target x86_64-linux" CXX="zig c++ -target x86_64-linux" go build --tags extended
- 
Naively Compiling - Compile in an actual machine w of target directly
- You can use chroot
- You can use containers
- Other stuff
 
Others
Packages and Modules
- Read Go Modules Reference
- module-aware mode is the way 2 go me fren. (GO111MODULE=""/auto)
- ditch gopath
Meta notes
- package pathand- module pathmay look similar, the difference lies in the existence of- go.modfile inside the directory. i.e Repository root need not be the place where the- moduleis defined.
Packages
- package path/- import path- Identity of a package
- module path+ subdirectory
- Eg. golang.org/x/net/html
 
- Each directory that has Go source code inside, possibly including the root directory, is a package.
- Example: x/...matchesxas well asx’s subdirectories.
Module
- The tree(module) with branches(packages) and leafs(*.gofiles) growing on branches.
- packagessharing the same lifecycle(- version number) are bundled into a- module.
- module path- Defined in the go.modfile by themodule directive
- Identity of a module
- Acts as a prefix for packageimport paths within the module.
- Eg. golang.org/x/net,golang.org/x/tools/gopls
 
- Defined in the 
Semantic versioning & versions from VCS
- A version identifies an immutable snapshot of a module. Each version starts with the letterv+ semantic versioning.
- v0.0.0, v1.12.134, v8.0.5-pre, v9.2.2-beta+meta and v2.0.9+metaare valid versions.
VCS and pseudo versioning
- We can also get modulesfrom VCS usingtags/branches/revisions/commitsthat don’t follow semantic versioning.
- In these cases, the go command will replace golang.org/x/net@daa7c041withv0.0.0-20191109021931-daa7c04131f5.
- This is called pseudo-version. You usually won’t be typing a pseudo version by hand.
Why separate directory for Major versions
Golden rule: If an old package and a new package have the same import path ⇒ The new package must be backwards compatible with the old package.
- v0/- pre-releasesuffix: Unstable, doesn’t need to be backwards compatible. No major version suffix directory allowed. So when starting new projects be under- v0as long as possible.
- v1: Defines the compatibility/stability. No major version suffix directory allowed.
- v2/- v2+: Since major version bump by definition means breaking changes, by the golden rule, we need it to have separate module path.
Building
What happens when go command tries to load a package?
- When we try to load a package, indirectly we need to find themodule path
- It first looks into the build list, if not found it’ll try to fetch themodule(latest version) from amodule proxymentioned in theGOPROXYenv var.
- go tidy/- go getdoes this automatically.
Generating build list
- When we run the go command, a list of final module versions is prepared from the go.modfile ofmain module+transitively required modulesusing minimal version selection. This final list ofmodule+versionis used forgo{build,list,test,etc}. This is thebuild list
- // indirect: This is added to- go.modof main module, when module is not directly required in the- main module. So you should have all the dependencies in the- go.modfile.
Workspaces
- New feature 1.18+
- You’re not meant to commit go.workfiles. They are meant for local changes only.
- Has useandreplacedirectives that can be useful for scratch work
Module Proxy
- module proxy is an HTTP server that can respond to GET requests for certain paths
- We don’t have a central package authority in the vein of npmorcrates.io. Go modules have nonames, onlypaths. The package management system uses thepackage path/module pathto learn how to get the package. If it can’t find the package locally, it’ll try getting it from amodule proxy.
- module proxy related vars: GOPRIVATE,GONOPROXY,GOPROXY="https://proxy.golang.org,direct"
- Different module proxies can have their own conventions (Eg. gopkg.inhas some diff conventions)
Access private packages is a PITA
- I haven’t faced this issue yet, but for when I do.
- SuperQue comments on What “sucks” about Golang?
- languitar/pass-git-helper
Project organization and dependencies
Standard Library
- In tree : You can find these in the go source tree. Check Standard library
- Out of tree : Part of go but out of tree, at /x. Check Sub-repositories
Project structure
- multi-module monorepos is unusual
- multi-package monorepo is common
Language topics
Pointers
- There’s no pointer arithmetic in go
- Go guarantees that, thing being pointed to will continue to be valid for the lifetime of the pointer.
func f() *int { i := 1 return &i } // Go will arrange memory to store i after f returns.
Methods
- In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.
Context
Signaling and Request cancellation
- Example: a client timeout - > your request context is canceled - > every I/O operations and long running processes will be canceled too
- It’s not possible for a function that takes a context.Contextto cancel it- It could do is newCtx, cancel := context.WithCancel(origCtx).
- It can listen for Doneon thatctxand do something(usually cancellation of ongoing task) based on it.
- Doneis triggered- when cancel()on thectxis called. (requiresWithCancel)
- also can be triggered based on use on WithTimeoutandWithDeadline
 
- when 
- When a Context is canceled, all Contexts derived from it are canceled.
- Eg. when cancel()is called onnewCtx,newCtxand all Contexts derived from it are canceled. (origCtxis NOT canceled)
- Eg. when cancel()is called onorigCtx,origCtxand all Contexts derived from it are canceled. (origCtxandnewCtxare canceled)
 
- Eg. when 
 
- It could do is 
- context.Background()is never canceled.
Storing values
- The storage of values in a context is a bit controversial. main use case for “context” is cancellation signals.
- In the above example, newCtxwill have access to the same values asorigCtx
- context.Value()is like Thread Local Storage (see Threads, Concurrency) for goroutines but in a cheap suit.
Other notes on context
- 
Context is that it should flow through your program. - Imagine a river or running water.
- Do pass from function to function down your call stack, augmented as needed. (Usually as the first argument)
- Don’t want to store it somewhere like in a struct.
- Don’t want to keep it around any more than strictly needed.
 
- 
When to create context? - Good practice to add a context to anything that might block on I/O, regardless of how long you assume it might take.
- Context object is created with each request and expires when the request is over. (request is general sense)
- context.Background()- Use pure context.Background()ONLY to handle your app lifecycle, never in a io/request function.
- Just passing context.Background()there offers no functionality.
 
- Use pure 
- context.WithCancel- In io/request functions, use something like context.WithCancel(context.Background())because that’ll allow you to cancel the context.
- Fresh context
- Eg. context.WithCancel(context.Background()),context.WithCancel(context.TODO())
 
- Eg. 
- Derived context
- Eg. context.WithCancel(someExistingCtx)
 
- Eg. 
 
- In io/request functions, use something like 
- context.TODO- Adding context to program later can be problematic, so consider using context.TODOif unsure what context to use. It’s similar to usingcontext.Background()but it’s a clue to your future self that you are not sure about the context yet rather than you explicitly want a background context.
 
- Adding context to program later can be problematic, so consider using 
 
- 
Separation of context - General rule: If the work(i/o) you’re about to perform can outlive the lifetime of outer function, you’d want to create a fresh context instead of deriving from the context of the outer function(if there is one)
- Eg. HTTP requests context are not derived from the server context as you still want to process on-going request while the app shuts down. (IMPORTANT). You’d usehttp.Request.Context()
 
- Eg. HTTP requests context are not derived from the server context as you still want to process on-going request while the app shuts down. (
- Think clearly about the boundaries and lifetimes, don’t mess app context to handle async function, internal consumer or request etc.
 
- General rule: If the work(i/o) you’re about to perform can outlive the lifetime of outer function, you’d want to create a fresh context instead of deriving from the context of the outer function(if there is one)
- 
Context package and HTTP package - You can get the context from http.Requestwith.Context(). It’s like this is because the http package was written before context was a thing.
- Outgoing client requests, the context is canceled when- We explicitly cancel the context
 
- Incoming server requests, the context is canceled when- The client’s connection closes
- The request is canceled (with HTTP/2)
- The ServeHTTP method returns
 
 
- You can get the context from 
- 
Context an Instrumentation - Instrumentation libraries generally use the context to hold the current span, to which new child spans can be attached.
 
Resources on context
- More on context: Break The Golang Context Chain » Rodaine
- Even more: Chris’s Wiki blog/programming/GoContextValueMistake
- justforfunc #9: The Context Package - YouTube
- Go Class: 25 Context - YouTube
Maps
- Go Maps are hashmap. O(1) ACR, O(n) WCR
- A Map value is a pointer to a runtime.hmapstructure.
- Since it’s a pointer, it should be written as *map[int]intinstead ofmap[int]int. Go team changed this historically cuz it was confusing anyway.
- Maps change it’s structure
- When you insert or delete entries
- The map may need to rebalance itself to retain its O(1) guarantee
 
- What the compiler does when you use map
v := m["key"] // → runtime.mapaccess1(m, ”key", &v) v, ok := m["key"] // → runtime.mapaccess2(m, ”key”, &v, &ok) m["key"] = 9001 // → runtime.mapinsert(m, ”key", 9001) delete(m, "key") // → runtime.mapdelete(m, “key”)
- If you want a map where you only care about the key and not the value, we can do: set := make(map[string]struct{})so it’s assigned to empty struct.
Embedding interfaces & structs
- It’s not subclassing, but we can borrow types in structandinterfaces
- See Embedding in Go: Part 3 - interfaces in structs - Eli Bendersky’s website
Embedding Interface
// combines Reader and Writer interfaces
type ReadWriter interface {
    Reader
    Writer
}Embedding Struct
- 
Embedding directly, no additional bookkeeping - When invoked, the receiver of the method is the inner type not the outer one.
- i.e when the Readmethod of abufio.ReadWriteris invoked, receiver is the innerReaderand notReadWriter.
 // bufio.ReadWriter type ReadWriter struct { *Reader // *bufio.Reader *Writer // *bufio.Writer *log.Logger } // - the type name of the field, ignoring the package // qualifier, serves as a field name // - Name conflicts are ez resolvable var poop ReadWriter poop.Reader // refers to inner Reader poop.Logger // refers to inner Logger
- 
Embedding in-directly, additional bookkeeping type ReadWriter struct { reader *Reader writer *Writer } func (rw *ReadWriter) Read(p []byte) (n int, err error) { return rw.reader.Read(p) }
Error and panics
See How we centralized and structured error handling in Golang | Hacker News
- recoveronly makes sense inside- defer
- defercan modify named return values- We cannot return from deferbut can use of named return values to have similar effect. eg. have a named return callederrand seterrin the defer function after recover.
 
- We cannot return from 
Creation
- errors.New
- fmt.Errorf
- which one?
Error wrapping
https://lukas.zapletalovi.com/posts/2022/wrapping-multiple-errors/
you wrap to give context about what went wrong in the function you called. You do not wrap to say that the function you called failed.
- using fmtlike this:fmt.Errorf("failed to create user: %w", err)(Go1.13): Useful to add info context- fmt.Errorfcan also be used independently ofc when not wrapping error
- %wis meant for error arguments. (so this is the differentator)
 
- Go1.20 introduced errors.Joinand then makefmt.Errorfaccept multiple%w. We can use either to join multiple errors. Eg. from goroutines.
- 
Error wrapping in practice package main import ( "errors" "fmt" ) // common HTTP status codes var NotFoundHTTPCode = errors.New("404") var UnauthorizedHTTPCode = errors.New("401") // database errors var RecordNotFoundErr = errors.New("DB: record not found") var AffectedRecordsMismatchErr = errors.New("DB: affected records mismatch") // HTTP client errors var ResourceNotFoundErr = errors.New("HTTP client: resource not found") var ResourceUnauthorizedErr = errors.New("HTTP client: unauthorized") // application errors (the new feature) var UserNotFoundErr = fmt.Errorf("user not found: %w (%w)", RecordNotFoundErr, NotFoundHTTPCode) var OtherResourceUnauthorizedErr = fmt.Errorf("unauthorized call: %w (%w)", ResourceUnauthorizedErr, UnauthorizedHTTPCode) func handleError(err error) { if errors.Is(err, NotFoundHTTPCode) { fmt.Println("Will return 404") } else if errors.Is(err, UnauthorizedHTTPCode) { fmt.Println("Will return 401") } else { fmt.Println("Will return 500") } fmt.Println(err.Error()) } func main() { handleError(UserNotFoundErr) handleError(OtherResourceUnauthorizedErr) }“What may not look obvious from such artificial code snippet is that errors declarations are typically spread across many packages and it is not easy to keep track of all possible errors ensuring the required HTTP status codes. In this approach, all application-level wrapping errors declared in a single place also have HTTP codes wrapped inside them.” 
Error comparison
- Value comparison
- Eg. if err != nil {
 
- Eg. 
- Type comparison
- Basically type assert but useful when we want to check error type
- Eg. if nerr, ok := err.(net.Error) {
- Eg. if errors.As(err, &nerr) {but need to intanciate first,var nerr *net.Error
 
- Existence check
- This can be used when you wrap errors
- if errors.Is(err, RecordNotFoundErr) {
 
Error stacktrace
Aliases
- type byte = uint8
- type rune = int32
- type any = interface{}
Interfaces
- Interfaces are just description of what something should resemble, by the methods.
- The implementation of the interface can be done by a struct,int,funcanything. Doesn’t matter. You can define a method on afunc, on aintjust the same way you can define a method on astruct.
Interface and Pointers
From chatgpt
Certainly. The statement “interfaces in Go are already pointer-like” refers to how interfaces are implemented and behaved in Go. This is an important concept that affects how we pass and use interfaces in Go programs. Let’s dive deeper into this:
Interface Structure: In Go, an interface is represented internally as a two-word structure:
The first word is a pointer to information about the type stored in the interface (called the “type descriptor”). The second word is a pointer to the actual data.
This structure is often referred to as an “interface value”. Passing Interfaces: When you pass an interface to a function, you’re actually passing this two-word structure. Even though it looks like you’re passing by value, you’re effectively passing a pointer to the data. No Need for Double Pointers: Because of this implementation, you don’t need to use pointers to interfaces (like *pgx.Tx) when you want to modify the underlying data. The interface itself already contains a pointer to the data.
io stuff
Ben Johnson has great blogpost series covering these in good depth
Overview of io related packages
- io- Abstractions on byte-stream
- General io utility functions that don’t fit elsewhere.
 
- Abstractions on 
- bufio- Like iobut with a buffer
- Wraps io.Reader and io.Writer and helps w automatic buffering
 
- Like 
- bytes- Represent byte slice([]byte) asbyte-stream(stringsalso provide this)
- general operations on []byte.
- bytes.Bufferimplements- io.Writer(useful for tests)
 
- Represent byte slice(
- io/ioiutil(deprecated)- Deprecated
- functionality moved to io or os packages
 
io
- 
Reading - Read- returns io.EOFas normal part of usage
- If you pass an 8-byte slice you could receive anywhere between 0 and 8 bytes back.
 
- returns 
- ReadFull- for strict reading of bytes into buffer.
 
- MultiReader- Concat multiple readers into one
- Things are read in sequence
- Eg. Concat in memory header with some file reader
 
- TeeReader- Like the teecommand. Specify an duplicate writer when reader gets read. Might be useful for debugging etc.
 
- Like the 
 
- 
Writing - MultiWriter- Duplicate writes to multiple writers. Similar to TeeReadertho but happens when writing shit
 
- Duplicate writes to multiple writers. Similar to 
- WriteString- An performance improvement on Writeon packages that support it. Falls back toWrite
 
- An performance improvement on 
 
- 
Transferring btwn Reading & Writing - Copy: Allocates a 32KB temp buff to copy from- src:Readerto- dst:Writer
- CopyBuffer: Provide your own buffer instead on letting- Copycreate one
- CopyN: Similar to copy but you can set a limit on total bytes. Useful when reader is continuously growing for example or want to do limited read etc.
- WriteToand- ReadFromare optimized methods that are supposed to transfer data without additional allocation. If available,- Copywill use these.
 
- 
Files Usually, you have a continuous stream of bytes. But files are exceptions. You can do stuff like Seekw them.
- 
Reading and Writing Bytes(uint8) & Runes(int32) - ByteReader
- ByteWriter
- ByteScanner
- RuneReader
- RuneScanner
- There’s no RuneWriter btw
 
bytes and strings package
Provides a way to interface in-memory []byte and string as io.Reader and io.Writers
- bytespackage has 2 types- bytes.Readerwhich implements- io.Reader(- NewReader)
- bytes.Bufferwhich implements- io.Writer
 
- bytes.Bufferis OK for tests etc- Consider bufiofor proper usecases w buffer related io.
- bytes.Bufferis a buffer with two ends- can only read from the start of it
- can only write to the end of it
- No seeking
 
 
- Consider 
strings, bytes, runes, characters
- Formal forloop will loop throughbyteinstringbutfor rangeloop will loop throughrune
- string: Readonly slice of- bytes. NOT slice of characters.
- “poop” is a string. `poop` is a raw string.
- stringcan contain escape sequences, so they’re not always UTF-8.
- raw stringcannot contain escape sequences, only UTF-8 because Go source code is UTF-8. (almost always)
 
- Unicode
- See Unicode
- code point U+2318, hex val2318, (bytese28c98) represents the symbol⌘.
 
- character- May be represented by a number of different sequences of code points- i.e different sequences of UTF-8 bytes
 
- In Go, we call Unicode code pointsasrune(int32).
 
- May be represented by a number of different sequences of 
Encoding
Encoding vs Marshaling
- Usually these mean the same thing, but Go has specific meanings.
- x.Encoder&- x.Decoderare for working w- io.Writer&- io.Reader(files eg.)
- x.Marshaler&- x.Unmarshalerare for working w- []byte(in memory)
Encoding for Primitives vs Complex objects
- 
Primitive stuff - bytes
- Text encoding(base64)/ binary encoding
- encodingpackage- BinaryMarshaler, BinaryUnmarshaler, TextMarshaler, TextUnmarshaler
- These are not used so much because there’s not a single defined way to marshal an object to binary format. Instead we have Custom Protocols which is covered w other packages such as encoding/jsonetc.
 
- encoding/hex,- encoding/base64etc.
 
- integers
- encoding/binary, wen we needs endian stuff and variable length encoding
- For in-memory we have ByteOrderinterface
- For streams we have ReadandWrite. This also supports composite types but better to just use Custom Protocols.
 
- string
- ASCII, UTF8
- unicode/utf16,- encoding/ascii85,- golang.org/x/text,- fmt,- strconvetc.
 
 
- bytes
- 
Complex obj stuff - Complex objects is where Custom Protocols comes in
- This is mostly about encoding more complex stuff like language specific data structure etc.
- Here we can go JSON, CSV, Protocol Buffers, MsgPack etc etc.
- In a sense, Database‘es also encode data for us.
- Example packages: encoding/json,encoding/xml,encoding/csv,encoding/gob. Other external stuff is always there like Protocol Buffers.
 
More on encoding/json
- Encoding process
- For primitives we have in-built mapping for json
- For custom objects, it checks if types for json.Marshaler, if not thenencoding.TextMarshaler. Eg.TimeimplementsTextMarshalerwhich creates RFC3339 string. Otherwise it builds it from primitives then that’s cached for future use.
 
- Decoding
- 2 parts
- 1st parse (Scanner)
- convert stuff to appropriate data type. Eg. Base 10 numbers to base 2 ints. (Decodestate) Uses reflect
 
- JSON is LL(1) Parsable. (See Context Free Grammar (CFG)) so uses uses a single byte lookahead buffer
 
- 2 parts
- Also see (Nested JSON parsing)
- Also see JSON Parsers
Generics in Go
Current limitations
- 
Co-variance You can’t use RecipeServiceClient[ templatefill.StartRequest, templatefill.StartResponse, templatefill.StopRequest, templatefill.StopResponse, ]where RecipeServiceClient[any,any,any,any]is expected. They’re different types- ATM, generic methods aren’t supported (where the generic type is scoped to the method, instead of the parent type)
- i.e you can’t have type parameters scoped to a particular method
- i.e Go’s generics do not support covariance, which means that even though if T satisfies the any interface, Client[T] is not a Client[any], as these are distinct and mutually exclusive types and not interchangeable in either direction.
 
Type Conversion and Type assertion
Type conversion
Eg. interface{}(42), here we’re taking 42(which is an int) and converting it into type interface{}
Type assertion
package main
 
import "fmt"
 
type t int
 
//this will panic
// func main() {
//   i := interface{}(42)
//   _ = i.(t)
// }
 
// because we have two _, ok, this will not panic
func main() {
	i := interface{}(42)
	if _, ok := i.(t); !ok {
		fmt.Println("assertion failed")
		return
	}
	fmt.Println("not hmm")
}- Can be only done on interfaces. (Not limited to interface{}though, but has to be interface)
Type switch
Main hatch: switch c := v.(type)
// p := map[string]interface{}{}
p := map[string]any{}
err := json.Unmarshal(data, &p)
for k, v := range p {
    switch c := v.(type) {
    case string:
        fmt.Printf("Item %q is a string, containing %q\n", k, c)
    case float64:
        fmt.Printf("Looks like item %q is a number, specifically %f\n", k, c)
    default:
        fmt.Printf("Not sure what type item %q is, but I think it might be %T\n", k, c)
    }
}cgo
| Go Pointer, Pass to Go | Go Pointer, Pass to C | |
|---|---|---|
| Go code | YES | YES, must point to C memory | 
| C code | NO | 
- Go’s pointer type can contain C pointers aswell as Go pointers
- Go pointers, passed to C may only point to data stored in C
Other topics
Deep copy
- There’s no default deep copy, if primitive types assigning is copy by value but if struct contains pointers etc, you’d need to implement the deep copy yourself
time.Ticker
If an operation in a select case takes longer than the ticker duration (5 seconds in your case), the ticker will continue to tick every 5 seconds, but you’ll miss those ticks while the operation is running. When the operation finally completes, the next case that reads from terminationCheckTicker.C will only get the most recent tick - any ticks that occurred during the operation are dropped.
Application supervison
NOTE: We’re running multiple services here, the idea is similar to erlang supervisor trees. See https://www.jerf.org/iri/post/2930/
But instead of using https://github.com/thejerf/suture, we’re using oklog/run as suture doesn’t seem to handle net/http (doesn’t fit in the model). The idea is similar to errgroup package aswell.
Application architecture
- Accept interfaces(broader types)
- Return structs(specific types)
- See Design Patterns
- Watch Workshop: Practical Go - GoSG Meetup - YouTube
- Watch Dave Cheney - SOLID Go Design - YouTube
- Read Go and a Package Focused Design | Gopher Academy Blog
- Modelscan be analogous to- type(core)
- Controllercan be analogous to- handlers(does not do core)
- Service thingsthese contain specific core logic etc.
Handler vs HandlerFunc
- See HandleFunc vs Handle : golang
- Anything(struct/function etc.) that implements the http.Handlerinterface
- The interface has the ServeHTTPmethod for handling HTTP requests and generating response
- Avoid putting business logic in handlers the same way you won’t put business logic into controllers
- http.HandlerFuncis an example of a handler (of type function) which implements- http.Handler
- When we write functions that contain the signature of http.HandlerFuncwe’ve written a handler function.
Logging and Error Handling
- In short: log the error once, at the point you handle it.
- Only log the error where it is handled, otherwise wrap it and return it without logging.
- At some point, you will log it as either an error if there is nothing you can do about it, or a warning if somehow you can recover from it (not panic recover).
- However your log record will contain the trace from the point where the error occurred, so you have all the information you need.
 
Testing
Note on testing.Log
- When using t.Log
- this debug info printed only on test fail, else with -v. If program fails to compile nothing runs
 
Go and Databases
See Organising Database Access in Go – Alex Edwards 🌟
Notes on using sqlc with golang
- How We Went All In on sqlc/pgx for Postgres + Go
- sqlc.narg()does not have a shorthand like- @which is available for- sqlc.arg()
- If you’re using pgx/v5, you probably wantemit_pointers_for_null_types: truein sqlc config. This makes sure that generated struct fields can be set to null, (in many cases we want to pass NULL to the query)
- sqlc and pgx/v5
Postgres Gotchas
- PostgreSQL DEFAULTis for when you don’t provide a column value inINSERTstatement. If you provideNULLas a value it’ll be considered as a value andDEFAULTwon’t apply.
Go Performance
Garbage Collection
Base heap sizeis like the “starting point” memory usage when your program has loaded and initialized, but before it starts doing its main work. Think of it as the program’s “idle” memory state.In Go specifically:
- It’s dynamically determined based on what your program needs at startup
- Can vary between runs of the same program
- Affected by factors like OS memory layout, system state, and program inputs
- Used by Go’s garbage collector as a reference point (GC typically allows heap to grow to 2x this size)
- By default, Go GC allows the heap to grow up to 2xits current size before triggering garbage collection
- Example case
- Allocated Mem for executable: 250M
- Base heap: 170MB
- Potential growth allowed: up to 340MB (2 × 170MB)
- Available memory: only 80MB (250-170)
- Now the GC won’t kick in unless we reach 340M, but we have only allocated 250.
- In this case, we can set GOMEMLIMIT=200MiB, which will trigger the GC at 200 instead of waiting for the 2x number.
 
Template (base-go)
About the template
Why this template?
- Remove friction of setup of new project
- Remove friction of writing the initial tests
- Suitable for: Basic starter, CLI stuff
- 
Improving the template 
Project layout
- Inspired by this layout.
- Essentials
- cmd/: executable commands
- docs/: human readable documentation
- internal/: code not intended to be used by others- Contains code that others(other things inside the repo aswell outside) shouldn’t consume.
- It isolates things in a way that files under the same import path can use it, exposing it is out of question. (??)
 
- scripts/: any scripts needed for meta-operations
 
- Additional
- api/: OpenAPI/Swagger specs, JSON schema files, protocol definition files.
- configs/: Configuration file templates or default configs.
- tests/: Integration tests w data etc. Otherwise, test code lives in the same dir that its code is written in.
- tools/: Supporting tools for this project.
 
Style suggestions and other notes on Go
Workflow
- Running tests in watch mode w gosumtest
- Using go run instead of go build (use with entr): eg. git-entr go run main.go --some-flag
- 
Debugging (dlv) - Usually use dlv testwhen you have a test file if you don’t have amainfunc.- You cd into the directory where the test is and then
- dlv test -- -test.vor simply- dlv test
 
 
- You cd into the directory where the test is and then
- funcs: lists all functions.- funcs [some_func]filters
- setting breakpoints
- break [func_name]
 
- Location specifiers: https://github.com/go-delve/delve/blob/master/Documentation/cli/locspec.md
- Use pto print symbols etc.
 
- Usually use 
- 
Logging - Question: Is it okay to mix slog and log in same package? Eg. in some cases i’d like to avoid slog+osExit and simply use log.Fatal
- slog doesn’t pass values by map unlike say logurus, which makes this a little bit weird but passing maps has performance implications but you could use attrs if you want.
- Opinions on logging levels
- My opinion
- I think when using structured logging, using these levels has some merit unlike what the author suggests. Color coding, easy filtering etc.
 
- Warning: It’s either an informational message, or an error condition. (prefer not using)
- Fatal: In golang, it’s similar to panic. Better to avoid it only. (prefer not using)
- Error: There’s no point in logging the error, either handle it or pass it up the stack. (prefer not using)
 
- My opinion
 
Basics
- Practical Go: Real world advice for writing maintainable Go programs
- Naming & Comments
- Overall Docs should answer why not how, code is how, variables are code
- Variable & Constants
- Name: Should define its purpose(How). Do no mention the type in the name.
- Comment: Should describe the contents(Why) not the purpose(How)
 
- Getter&setters: Ownerinstead ofGetOwner,SetOwneris fine.
- Interface names have a -er- If methods of interface has name such as Read, Write, Close, Flush, String, try using the canonical signatures.
- If the functionality of a method is same as provided by std lib, keep name same. Eg. If method prints sring representation, call it Stringinstead ofToString
 
- Prefer single words for methods, interfaces, and packages.
 
- Global variables become an invisible parameter to every function in your program!
- Using Mustas a prefix for function or method names to indicate that the operation is expected tosucceedorpanic
- 
Organizing files - All Go code is organized into packages. And we organize code and types by their functional responsibilities.
- Keep things close
- Keep types close to source, pref. in top of the file.
- Code should be as close to where it’s used as possible. It might be tempting to put things into repo-root/internalbut if it makes better sense, put it close to the source, maybe in its own/internal.
 
 
- 
About main- 3 things: package main,main()andmain.go
- Executable program you need an entry point
- package main+- main()func is the entry point
 
- The file with the main()inside the packagemain, is conventionally namedmain.go. But can be called anything else. But good to follow the convention.
- i.e. Have a file(that we want to create an executable out of) inside a directory, name it main.go+package main+main()func. This will give us an executable when go build is run on it.
- mainpackages are not importable, so don’t export stuff from it.
- If your project is just a package and not an executable, it doesn’t need a main(), hence doesn’t need amainpackage at all.
 
- 3 things: 
- 
About init()- Special function that is executed before main(), usually to perform any initialization tasks
- Can be defined in any package, multiple such functions can be defined in the same package
- Nothing related to being an executable unlike main()
 
- Special function that is executed before 
Packages in Go
- 
Creating packages - Go only allows one package per directory
- Package Name/Path
- What it provides, not what it contains.
- No plurals for package name. don’t name a package httputils, name it httputil
- Avoid overly broad package names like “common” and “util”.
 
- Enforce vanity URLs. It ensures this package can only be imported via the mentioned path, even if the other url is serving it.
package datastore // import "cloud.google.com/go/datastore"
 
- 
Exposing packages // from xeiaso repo-root ├── cmd │ ├── paperwork │ │ ├── create // exposed to other parts of the module and outside │ │ │ └── create.go │ │ └── main.go │ ├── hospital // not exposed cuz internal │ │ ├── internal │ │ │ └── operate.go │ │ └── main.go │ └── integrator // not exposed │ ├── integrate.go │ └── main.go ├── internal // not exposed │ └── log_manipulate.go └── web // exposed to other parts of the module and outside ├── error.go └── instrument.go- Things you don’t want to expose
- Only internal use : /internal(subdirectories can have their own/internal)
- Other things with main.godon’t get exposed as it’s typically used as an entry point for executable programs.
 
- Only internal use : 
- Things you want to expose
- If webis used all over your package. (/repo-root/web)
- From subdirectories/subpackage, close to the code. (repo-root/cmd/paperwork/create)
- If exposing a package to users, avoid exposing your custom repository structure to your users. i.e Avoid having src/, pkg/ sections in your import paths. So stick to the two points above^
 
- If 
 
- Things you don’t want to expose
- 
Vendoring - Why vendor?
- I sometimes work offline, so vendoring is important for me
- Helps to ensure that all files used for build are in a single file tree.
- No network access we can build stuff always (we supposed to push the vendor to vcs)
 
- Default behavior is, if vendor/directory is present, the go command acts as if-mod=vendorotherwise-mod=readonly. I think sane defaults.
 - 
module cache - When -mod=mod(This is whatgo getandgo mod tidydoes, doesn’t need the flag)
- Downloaded modulesare stored inGOMODCACHEand maderead-only. This cache may be shared by multiple Go projects developed on the same machine.
 
- When 
 - 
vendor - When -mod=vendor, go command will use thevendor/directory
- Will not use the network or the module cache
- Useful things to know about vendoring
- Local changes should not be made to vendored packages. Workspaces can probably help here.
- go mod vendor omits go.mod and go.sum files for vendored dependencies
- go mod vendor records the go version for each deps in vendor/modules.txt
- go get, go mod download, go mod tidywill bypass vendor directory and download stuff as expected
 
 
- When 
 
- Why vendor?
Testing
- Test code usually lives in the same dir as the code with <file>_test.go
- fmt.Xwork inside tests but it’s not supposed to be used there, also it’ll format itself weird in the output. For logging in tests, use- t.Logetc.
- Assertion are not popular in go and I do not plan to use them, but if I need, there’s testify.
- You don’t want to test private functions, those are implementation details. Better focus on testing the behavior.
- If the test has > 3 mocks, might be time to reconsider code
- 
Additional testing helper packages - unit tests & mocking : Go’s testing framework and dependency injection via interfaces
- Acceptance tests: Black box test/Functional tests
- These are usually separate packages or somthing that run against the running shit
- Usually great for working w legacy codebase or unknown ones
 
- github.com/approvals/go-approval-tests : For goldens
 
HTTP related
- 
Service lifecycle - Waitgroups, errgroup or oklog/run.
- The later 2 are alternatives trying to improve waitgroup’s interface. Choose based on preference.
 
- 
Routers muxis short for Multiplexer- http.ServeMux- Good to go with in most cases, but doesn’t support variables in URL in which case you might consider something else
- Avoid using http.DefaultServeMux; any package you import can have access to it, eg. if anything importsnet/http/pprof, clients will be able to get CPU profiles. Instead instantiate anhttp.ServeMuxyourself and set it as theServer.Handler.
- A metric you’ll want to monitor is the number of open file descriptors when dealing with webservers. One can use Server.ConnStatehook to get more detailed metrics of what stage the connections are in.
 
- go-chi/chi- for anything else go w chi
 
- Use unrolled/secure for security headers
- Additional notes
- Parsing body
- Sending response
- Routing techniques
- Middleware
- Management
 
 
- 
Responses - We need to set w.Header()before callingw.WriteHeader- first w.Header().Set("Content-Type", "application/octet-stream")
- then w.WriteHeader(http.StatusOK)
- otherwise you are making change after headers were written to the response and it will have no effect.
 
- first 
 
- We need to set 
Databases
- I’ve written something about sqlite drivers in my wiki
- Overall pgx+sqlccan be a good combination- Transactions: Is there a way for sqlc to generate code that can use pgxpool
- Official docs has incomplete example
 
 
- Transactions: Is there a way for sqlc to generate code that can use pgxpool
- 
Interface - These interfaces do need a driver to work with
- database/sql- See Go database/sql tutorial
- Basic usage
- Write a query, pass in the necessary arguments, and scan the results back into fields.
- Programmers are responsible for explicitly specifying the mapping between a SQL field and its value
 
- sqlx- Extensions on go’s standard database/sql library. (superset of database/sql) Eg. allows you to avoid manual column <-> field mapping etc.
- sqlxwork only with the standard interface and not the native interface of- pgx. So- sqlxcan be used with- pgxwith stdlib compatibility.
 
- Extensions on go’s standard database/sql library. (superset of 
 
- ORMs
 
- 
Drivers - pgx(for postgres)- If needed, it still can be used with database/sql, sqlc, sqlx etc. But usually, if you’re only dealing with postgres, you don’t really need these additional layers. Simply pgx’s native interface, it can handle most things.
- pgx’s native interface works with scany
- It has postgree specific optimizations that are impossible in the standard driver. (for example support for Native Postgres types: arrays, json etc)
- Alternatives are lib/pqetc. butpgxis good.
 
- If needed, it still can be used with database/sql, sqlc, sqlx etc. But usually, if you’re only dealing with postgres, you don’t really need these additional layers. Simply 
 
- 
SQL query builders/code generators/mappers - These basically convert go’s syntax into pure SQL.
- Example: squirrel, jet(preferred), sqlboiler
- sqlc- It’s NOT a go package, it’s a cli tool
- From SQL -> Go code
- See Compile SQL queries to type-safe Go
- See How We Went All In on sqlc/pgx for Postgres + Go
- Works with pgx
- It does not use struct tags, hand-written mapper functions, unnecessary reflection etc.
- It’s the opposite of sql query builders
- It generates type-safe code for your raw SQL schema and queries.
 
 
- 
Transaction and Request cancellation 
- 
Pooling for postgres - 
Why pgxpool? - Usually it’s preferred to go with server side pooling than to even worry about client side pooling.
- But if you’re using pgxin a concurrent application, eg. webserver where each web session will create a new database connection then you must you pgxpool whether or not you’re usingpgbounceron the server side because pgx.conn by itself is not threadsafe, i.e multiple goroutines cannot safely access it.
- With pgxpool is you can have long standing connections with db and pgxpool also tries to restablish connections if they are closed due to environment factors. You don’t get it with plain pgx.Conn
- By default pgx automatically uses prepared statements. Prepared statements are incompaptible with PgBouncer. This can be disabled by setting a different QueryExecMode in ConnConfig.DefaultQueryExecMode.
- This has changed! See Prepared Statements in Transaction Mode for… | Crunchy Data Blog
- Also see PreferSimpleProtocol(v4)
 
 
 
- 
Patterns
See Design Patterns
- Inorder processing : blog/programming/GoAndPromisesPattern
Resources and Links
Go and sqlite
- have a dedicated object for writing to the database, and run db.SetMaxOpenConns(1) on it. I learned the hard way that if I don’t do this then I’ll get SQLITE_BUSY errors from two threads trying to write to the db at the same time.
- if I want to make reads faster, I could have 2 separate db objects, one for writing and one for reading
- See sqlite also see Go and SQLite in the Cloud
- mattn/go-sqlite3 : Uses cgo (most commonly used). But cgo takes os thread.
- See this link for some usage tips
 
- cznic/sqlite : Somehow translates C code to Go.
- ncruces/go-sqlite3 : wraps a WASM build of SQLite, and uses wazero to provide cgo-free SQLite bindings.
- https://github.com/cvilsmeier/go-sqlite-bench
- ORM
- go-gorm/sqlite: GORM sqlite driver
- glebarez/sqlite: The pure-Go SQLite driver for GORM (fork)
- glebarez/go-sqlite: pure-Go SQLite driver for Go (SQLite embedded)