├── .gitignore ├── go.mod ├── Makefile ├── go.sum ├── identity └── identity.go ├── tracker └── tracker.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | context_values 4 | 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/learning-go-book/context_values 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-chi/chi v4.1.2+incompatible 7 | github.com/google/uuid v1.1.1 8 | ) 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | 3 | .PHONY: build run run-client 4 | 5 | build: 6 | go build 7 | 8 | run: build 9 | ./context_values 10 | 11 | run-client: 12 | curl -b user=jon localhost:3000/?data=hello 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= 2 | github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 3 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 4 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | -------------------------------------------------------------------------------- /identity/identity.go: -------------------------------------------------------------------------------- 1 | package identity 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type userKey int 9 | 10 | const key userKey = 1 11 | 12 | func ContextWithUser(ctx context.Context, user string) context.Context { 13 | return context.WithValue(ctx, key, user) 14 | } 15 | 16 | func UserFromContext(ctx context.Context) (string, bool) { 17 | user, ok := ctx.Value(key).(string) 18 | return user, ok 19 | } 20 | 21 | // a real implementation would be signed to make sure 22 | // the user didn't spoof their identity 23 | func extractUser(req *http.Request) (string, error) { 24 | userCookie, err := req.Cookie("user") 25 | if err != nil { 26 | return "", err 27 | } 28 | return userCookie.Value, nil 29 | } 30 | 31 | func Middleware(h http.Handler) http.Handler { 32 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 33 | user, err := extractUser(req) 34 | if err != nil { 35 | rw.WriteHeader(http.StatusUnauthorized) 36 | return 37 | } 38 | ctx := req.Context() 39 | ctx = ContextWithUser(ctx, user) 40 | req = req.WithContext(ctx) 41 | h.ServeHTTP(rw, req) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /tracker/tracker.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | type guidKey int 12 | 13 | const key guidKey = 1 14 | 15 | func contextWithGUID(ctx context.Context, guid string) context.Context { 16 | return context.WithValue(ctx, key, guid) 17 | } 18 | 19 | func guidFromContext(ctx context.Context) (string, bool) { 20 | g, ok := ctx.Value(key).(string) 21 | return g, ok 22 | } 23 | 24 | func Middleware(h http.Handler) http.Handler { 25 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 26 | ctx := req.Context() 27 | if guid := req.Header.Get("X-GUID"); guid != "" { 28 | ctx = contextWithGUID(ctx, guid) 29 | } else { 30 | ctx = contextWithGUID(ctx, uuid.New().String()) 31 | } 32 | req = req.WithContext(ctx) 33 | h.ServeHTTP(rw, req) 34 | }) 35 | } 36 | 37 | type Logger struct{} 38 | 39 | func (Logger) Log(ctx context.Context, message string) { 40 | if guid, ok := guidFromContext(ctx); ok { 41 | message = fmt.Sprintf("GUID: %s - %s", guid, message) 42 | } 43 | // do logging 44 | fmt.Println(message) 45 | } 46 | 47 | func Request(req *http.Request) *http.Request { 48 | ctx := req.Context() 49 | if guid, ok := guidFromContext(ctx); ok { 50 | req.Header.Add("X-GUID", guid) 51 | } 52 | return req 53 | } 54 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/go-chi/chi" 10 | "github.com/learning-go-book/context_values/identity" 11 | "github.com/learning-go-book/context_values/tracker" 12 | ) 13 | 14 | func main() { 15 | bl := BusinessLogic{ 16 | RequestDecorator: tracker.Request, 17 | Logger: tracker.Logger{}, 18 | Remote: "http://www.example.com/query", 19 | } 20 | 21 | c := Controller{ 22 | Logic: bl, 23 | } 24 | 25 | r := chi.NewRouter() 26 | r.Use(tracker.Middleware, identity.Middleware) 27 | r.Get("/", c.handleRequest) 28 | http.ListenAndServe(":3000", r) 29 | } 30 | 31 | type RequestDecorator func(*http.Request) *http.Request 32 | 33 | type Logger interface { 34 | Log(context.Context, string) 35 | } 36 | type BusinessLogic struct { 37 | RequestDecorator RequestDecorator 38 | Logger Logger 39 | Remote string 40 | } 41 | 42 | func (bl BusinessLogic) businessLogic(ctx context.Context, user string, data string) (string, error) { 43 | bl.Logger.Log(ctx, fmt.Sprintf("starting businessLogic for %s with %s", user, data)) 44 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, bl.Remote+"?query="+data, nil) 45 | if err != nil { 46 | bl.Logger.Log(ctx, fmt.Sprintf("error building remote request: %v", err)) 47 | return "", err 48 | } 49 | req = bl.RequestDecorator(req) 50 | resp, err := http.DefaultClient.Do(req) 51 | if err != nil { 52 | bl.Logger.Log(ctx, fmt.Sprintf("error making remote request: %v", err)) 53 | return "", err 54 | } 55 | body := resp.Body 56 | defer body.Close() 57 | out, err := ioutil.ReadAll(body) 58 | if err != nil { 59 | bl.Logger.Log(ctx, fmt.Sprintf("error reading response: %v", err)) 60 | return "", err 61 | } 62 | return string(out), nil 63 | } 64 | 65 | type Logic interface { 66 | businessLogic(ctx context.Context, user string, data string) (string, error) 67 | } 68 | 69 | type Controller struct { 70 | Logic Logic 71 | } 72 | 73 | func (c Controller) handleRequest(rw http.ResponseWriter, req *http.Request) { 74 | ctx := req.Context() 75 | user, ok := identity.UserFromContext(ctx) 76 | if !ok { 77 | rw.WriteHeader(http.StatusInternalServerError) 78 | return 79 | } 80 | data := req.URL.Query().Get("data") 81 | result, err := c.Logic.businessLogic(ctx, user, data) 82 | if err != nil { 83 | rw.WriteHeader(http.StatusInternalServerError) 84 | rw.Write([]byte(err.Error())) 85 | return 86 | } 87 | rw.Write([]byte(result)) 88 | } 89 | --------------------------------------------------------------------------------