Lifecycle management ensures that application components like servers, databases, and background workers are correctly initialized, started in the right order, and gracefully stopped.
Go developers typically wire dependencies manually, instead of relying on heavy frameworks or reflection based dependency injection. This approach improves readability and reduces hidden behavior, but it also shifts the burden of managing component interactions and lifecycle sequencing onto the developer.
As applications scale, the lack of built-in orchestration increases the risk of subtle bugs and inconsistent shutdown behavior.
Event logging is one of the three pillars of observability (traces and metrics being the other two).
A log entry is a timestamped record about the application’s activity, which can be used for troubleshooting, monitoring or auditing purposes.
Logs may have different formats:
unstructured or plain text: similar to print statements, free-form text
semi-structured: some values have a structure and the remaining data free-form
structured: there is a well-defined and consistent format, such as: W3C Log Format, NCSA Log Format, Key/Value Pair, CSV or JSON, among others. Structured logs can be easily machine parsed, which simplifies analytics, filtering and aggregation.
This article is a non-exhaustive walkthrough of logging in Go.
Opentelemetry provides vendor neutral standards and implementations to generate, collect, and export tracing data.
A Context is a propagation mechanism which carries execution-scoped values across API boundaries and between logically associated execution units. Cross-cutting concerns access their data in-process using the same shared Context object.
Context propagation is required when the tracing needs to cross process or service boundaries. Common ways to propagate the context is using W3C Trace Context or Zipkin B3 headers, while to inject the context is, for example, http headers or metadata fields in event messages.
Similar in syntax to scala3/dotty union types, although in Go is only allowed in constraints. The type set of this union element is the set {int, int8, int16, int32, int64}.
Doing code reviews, I have sometimes noticed libraries forcing dependencies on the users.
While it is perfectly acceptable if well justified and documented, it sometimes feels like adding unnecessary baggage.
A typical example would be something like the following snippet:
func(c*Client)HealthCheck(ctxcontext.Context)error{logrus.Debug("sending get request")req,err:=http.NewRequestWithContext(ctx,http.MethodGet,c.url,nil)iferr!=nil{returnerr}resp,err:=c.client.Do(req)iferr!=nil{returnerr}logrus.Debugf("http status code was %d",resp.StatusCode)deferresp.Body.Close()returnnil}
Using logrus, as example, picking this code, one would need to check how logrus works, which configurations are available, which version should be used and so on.
Each field is treated as an integer and has its value printed as a
zero-filled hexadecimal digit string with the most significant
digit first. The hexadecimal values "a" through "f" are output as
lower case characters and are case insensitive on input.
There are multiple ways of writing client libraries in Go. In this post, I will explore what I look for in a library depending if I am developing or checking if it is suitable for a project.
Without any specific order of importance, these are my thoughts on the subject.
Usage and Examples
Like a book can be judged by its cover, a project can be judged by its README. Therefore providing examples helps getting an idea of how the library works: if it will fit a project or how good the developer usability is (this includes function naming, usage difficulty or how easy it is to misuse).