diff --git a/_drafts/program-structure-and-composability.md b/_drafts/program-structure-and-composability.md index 0af4e99..ce14f40 100644 --- a/_drafts/program-structure-and-composability.md +++ b/_drafts/program-structure-and-composability.md @@ -8,6 +8,9 @@ description: >- TODO: * Double check if I'm using "I" or "We" everywhere (probably should use "I") +* Part 2: Full Example +* Standardize on "programs", not "apps" or "services" +* Prefix all relevant code examples with a package name ## Part 0: Introduction @@ -544,3 +547,101 @@ func main() { ### Full example ## Part 3: Annotations, Logging, and Errors + +Let's shift gears away from the component structure for a bit, and talk about a +separate, but related, set of issues: those related to logging and errors. + +Both logging and error creation share the same problem, that of collecting as +much contextual information around an event as possible. This is often done +through string formatting, like so: + +```go +// ServeHTTP implements the http.Handler method and is used to serve App's HTTP +// endpoints. +func (app *App) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + log.Printf("incoming request from remoteAddr:%s for url:%s", r.RemoteAddr, r.URL.String()) + + // begin actual request handling +} +``` + +In this example the code is logging an event, an incoming HTTP request, and +including contextual information in that log about the remote address of the +requester and the URL being requested. + +Similarly, an error might be created like this: + +```go +func (app *App) GetUsername(userID int) (string, error) { + userName, err := app.Redis.Command("GET", userID) + if err != nil { + return "", fmt.Errorf("could not get username for userID:%d: %s", userID, err) + } + return userName, nil +} +``` + +In that example, when redis returns an error the error is extended to include +contextual information about what was attempting to be done (`could not get +username`) and the userID involved. In newer versions of Go, and indeed in many +other programming languages, the error will also include information about where +in the source code it occurred, such as file name and line number. + +It is my experience that both logging and error creation often take up an +inordinate amount of space in many programs. This is due to a desire to +contextualize as much as possible, since in a large program it can be difficult +to tell exactly where something is happening, even if you're looking at the log +entry or error. For example, if a program has a set of HTTP endpoints, each one +performing a redis call, what good is it to see the log entry `redis command had +an error: took too long` without also knowing which command is involved, and +which endpoint is calling it? Very little. + +So many programs of this nature end up looking like this: + +```go +func (app *App) httpEndpointA(rw http.ResponseWriter, r *http.Request) { + err := app.Redis.Command("SET", "foo", "bar") + if err != nil { + log.Printf("redis error occurred in EndpointA, calling SET: %s", err) + } +} + +func (app *App) httpEndpointB(rw http.ResponseWriter, r *http.Request) { + err := app.Redis.Command("INCR", "baz") + if err != nil { + log.Printf("redis error occurred in EndpointA, calling INCR: %s", err) + } +} + +// etc... +``` + +Obviously logging is taking up the majority of the code-space in those examples, +and that doesn't even include potentially pertinent information such as IP +address. + +Another aspect of the logging/error dichotemy is that they are often dealing in +essentially the same data. This makes sense, as both are really dealing with the +same thing: capturing context for the purpose of later debugging. So rather than +formatting strings by hand for each use-case, let's instead use our friend, +`context.Context`, to carry the data for us. + +### Annotations + +I will here introduce the idea of "annotations", which are essentially key/value +pairs which can be attached to a Context and retrieved later. To implement +annotations I will introduce two new functions to the `mctx` package: + +```go +// Package mctx + +// Annotate returns a new Context with the given key/value pairs embedded into +// it, which can be later retrieved using the Annotations method. If any keys +// conflict with previous annotations, their values will overwrite the +// previously annotated values for those keys. +func Annotate(ctx context.Context, keyvals ...interface{}) context.Context + +// Annotations returns all annotations which have been set on the Context using +// Annotate. +func Annotations(ctx context.Context) map[interface{}]interface{} +```