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
CGO
has nothing to do with whether or not go syscall goes vialibc
, the C code used viaCGO
ofcourse 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
X
syscall is available in OSA
andB
. Go might have support forX
only inA
as of the moment.
Places where go handles syscalls
- Try
fd syscall -t
on 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 andiconv
conversions.
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 path
andmodule path
may look similar, the difference lies in the existence ofgo.mod
file inside the directory. i.e Repository root need not be the place where themodule
is 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/...
matchesx
as well asx
’s subdirectories.
Module
- The tree(
module
) with branches(packages
) and leafs(*.go
files) growing on branches. packages
sharing the same lifecycle(version number
) are bundled into amodule
.module path
- Defined in the
go.mod
file by themodule directive
- Identity of a module
- Acts as a prefix for
package
import 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+meta
are valid versions.
VCS and pseudo versioning
- We can also get
modules
from VCS usingtags/branches/revisions/commits
that don’t follow semantic versioning. - In these cases, the go command will replace
golang.org/x/net@daa7c041
withv0.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-release
suffix: Unstable, doesn’t need to be backwards compatible. No major version suffix directory allowed. So when starting new projects be underv0
as 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 proxy
mentioned in theGOPROXY
env var. go tidy
/go get
does this automatically.
Generating build list
- When we run the go command, a list of final module versions is prepared from the
go.mod
file ofmain module
+transitively required modules
using minimal version selection. This final list ofmodule+version
is used forgo{build,list,test,etc}
. This is thebuild list
// indirect
: This is added togo.mod
of main module, when module is not directly required in themain module
. So you should have all the dependencies in thego.mod
file.
Workspaces
- New feature 1.18+
- You’re not meant to commit
go.work
files. They are meant for local changes only. - Has
use
andreplace
directives 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
npm
orcrates.io
. Go modules have nonames
, onlypaths
. The package management system uses thepackage path/module path
to 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.in
has 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.Context
to cancel it- It could do is
newCtx, cancel := context.WithCancel(origCtx)
. - It can listen for
Done
on thatctx
and do something(usually cancellation of ongoing task) based on it. Done
is triggered- when
cancel()
on thectx
is called. (requiresWithCancel
) - also can be triggered based on use on
WithTimeout
andWithDeadline
- when
- When a Context is canceled, all Contexts derived from it are canceled.
- Eg. when
cancel()
is called onnewCtx
,newCtx
and all Contexts derived from it are canceled. (origCtx
is NOT canceled) - Eg. when
cancel()
is called onorigCtx
,origCtx
and all Contexts derived from it are canceled. (origCtx
andnewCtx
are 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,
newCtx
will 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.TODO
if 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.Request
with.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.hmap
structure. - Since it’s a pointer, it should be written as
*map[int]int
instead 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
struct
andinterfaces
- 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
Read
method of abufio.ReadWriter
is invoked, receiver is the innerReader
and 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
recover
only makes sense insidedefer
defer
can modify named return values- We cannot return from
defer
but can use of named return values to have similar effect. eg. have a named return callederr
and seterr
in 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
fmt
like this:fmt.Errorf("failed to create user: %w", err)
(Go1.13): Useful to add info contextfmt.Errorf
can also be used independently ofc when not wrapping error%w
is meant for error arguments. (so this is the differentator)
- Go1.20 introduced
errors.Join
and then makefmt.Errorf
accept 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
,func
anything. Doesn’t matter. You can define a method on afunc
, on aint
just 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
io
but with a buffer - Wraps io.Reader and io.Writer and helps w automatic buffering
- Like
bytes
- Represent byte slice(
[]byte
) asbyte-stream
(strings
also provide this) - general operations on
[]byte
. bytes.Buffer
implementsio.Writer
(useful for tests)
- Represent byte slice(
io/ioiutil
(deprecated)Deprecated
- functionality moved to io or os packages
io
-
Reading
Read
- returns
io.EOF
as 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
tee
command. 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
TeeReader
tho but happens when writing shit
- Duplicate writes to multiple writers. Similar to
WriteString
- An performance improvement on
Write
on packages that support it. Falls back toWrite
- An performance improvement on
-
Transferring btwn Reading & Writing
Copy
: Allocates a 32KB temp buff to copy fromsrc:Reader
todst:Writer
CopyBuffer
: Provide your own buffer instead on lettingCopy
create oneCopyN
: 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.WriteTo
andReadFrom
are optimized methods that are supposed to transfer data without additional allocation. If available,Copy
will use these.
-
Files
Usually, you have a continuous stream of bytes. But files are exceptions. You can do stuff like
Seek
w 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
bytes
package has 2 typesbytes.Reader
which implementsio.Reader
(NewReader
)bytes.Buffer
which implementsio.Writer
bytes.Buffer
is OK for tests etc- Consider
bufio
for proper usecases w buffer related io. bytes.Buffer
is 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
for
loop will loop throughbyte
instring
butfor range
loop will loop throughrune
string
: Readonly slice ofbytes
. NOT slice of characters.- “poop” is a string. `poop` is a raw string.
string
can contain escape sequences, so they’re not always UTF-8.raw string
cannot 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 points
asrune
(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.Decoder
are for working wio.Writer
&io.Reader
(files eg.)x.Marshaler
&x.Unmarshaler
are for working w[]byte
(in memory)
Encoding for Primitives vs Complex objects
-
Primitive stuff
- bytes
- Text encoding(base64)/ binary encoding
encoding
package- 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/json
etc.
encoding/hex
,encoding/base64
etc.
- integers
encoding/binary
, wen we needs endian stuff and variable length encoding- For in-memory we have
ByteOrder
interface - For streams we have
Read
andWrite
. This also supports composite types but better to just use Custom Protocols.
- string
- ASCII, UTF8
unicode/utf16
,encoding/ascii85
,golang.org/x/text
,fmt
,strconv
etc.
- 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.Time
implementsTextMarshaler
which 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
Models
can be analogous totype
(core)Controller
can be analogous tohandlers
(does not do core)Service things
these contain specific core logic etc.
Handler vs HandlerFunc
- See HandleFunc vs Handle : golang
- Anything(struct/function etc.) that implements the
http.Handler
interface - The interface has the
ServeHTTP
method 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.HandlerFunc
is an example of a handler (of type function) which implementshttp.Handler
- When we write functions that contain the signature of
http.HandlerFunc
we’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 forsqlc.arg()
- If you’re using
pgx/v5
, you probably wantemit_pointers_for_null_types: true
in 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
DEFAULT
is for when you don’t provide a column value inINSERT
statement. If you provideNULL
as a value it’ll be considered as a value andDEFAULT
won’t apply.
Go Performance
Garbage Collection
Base heap size
is 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
2x
its 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 commandsdocs/
: human readable documentationinternal/
: 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 test
when you have a test file if you don’t have amain
func.- You cd into the directory where the test is and then
dlv test -- -test.v
or simplydlv 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
p
to 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:
Owner
instead ofGetOwner
,SetOwner
is 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
String
instead ofToString
- Prefer single words for methods, interfaces, and packages.
- Global variables become an invisible parameter to every function in your program!
- Using
Must
as a prefix for function or method names to indicate that the operation is expected tosucceed
orpanic
-
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/internal
but 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. main
packages 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 amain
package 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.go
don’t get exposed as it’s typically used as an entry point for executable programs.
- Only internal use :
- Things you want to expose
- If
web
is 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=vendor
otherwise-mod=readonly
. I think sane defaults.
-
module cache
- When
-mod=mod
(This is whatgo get
andgo mod tidy
does, doesn’t need the flag) - Downloaded
modules
are stored inGOMODCACHE
and 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 tidy
will 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.X
work inside tests but it’s not supposed to be used there, also it’ll format itself weird in the output. For logging in tests, uset.Log
etc.- 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
mux
is short for Multiplexerhttp.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.ServeMux
yourself 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.ConnState
hook 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
+sqlc
can 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. sqlx
work only with the standard interface and not the native interface ofpgx
. Sosqlx
can be used withpgx
with 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/pq
etc. butpgx
is 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
pgx
in 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 usingpgbouncer
on 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)