Compare commits
	
		
			No commits in common. 'master' and 'drew/sql-it' have entirely different histories. 
		
	
	
		
			master
			...
			drew/sql-i
		
	
		
	@ -1,26 +0,0 @@
 | 
				
			|||||||
# Using Context
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Remember that a common pattern of signaling events or completion is using a blocking channel that receives and empty struct. An empty struct has zero memory footprint. It is typically used to signal completion or stopping. 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The Context.Done() returns a channel that is closed when this Context is canceled `Done() <-chan struct{}`. Contexts have a root and can form a parent child tree structure. A child is "derived" from a parent context and when a Context is cancelled all derived contexts are also canceled. WithCancel and WithTimeout return derived Context values that can be canceled sooner than the parent Context.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a Context is canceled, all Contexts derived from it are also canceled.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## What about Context.Value
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The problem with context.Values is that it's just an untyped map so you have no type-safety and you have to handle it not actually containing your value. You have to create a coupling of map keys from one module to another and if someone changes something things start breaking.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
In short, if a function needs some values, put them as typed parameters rather than trying to fetch them from 
 | 
					 | 
				
			||||||
context.Value
 | 
					 | 
				
			||||||
. This makes it statically checked and documented for everyone to see.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Context.Value is good for things like "A trace ID". It's information that is not needed for by every function in the call stack.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
> Context.Value should inform not control. The content of context.Value is for maintainers not users. It should never be required input for documented or expected results.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Readings
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
https://go.dev/blog/context
 | 
					 | 
				
			||||||
https://go.dev/blog/pipelines
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
https://faiface.github.io/post/context-should-go-away-go2/
 | 
					 | 
				
			||||||
@ -1,3 +0,0 @@
 | 
				
			|||||||
module my_context
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
go 1.23.1
 | 
					 | 
				
			||||||
@ -1,47 +0,0 @@
 | 
				
			|||||||
package my_context
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Store interface {
 | 
					 | 
				
			||||||
	Fetch(ctx context.Context) (string, error)
 | 
					 | 
				
			||||||
	// instead of calling cancel we want to use context above to cross API boundries
 | 
					 | 
				
			||||||
	// Cancel()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func Server(store Store) http.HandlerFunc {
 | 
					 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// THIS WAS WHAT WE DID WHEN WE DIDN"T PROPOGATE CONTEXT
 | 
					 | 
				
			||||||
		// 	// This is the requests ctx not a derived context
 | 
					 | 
				
			||||||
		// 	ctx := r.Context()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 	data := make(chan string, 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 	go func() {
 | 
					 | 
				
			||||||
		// 		data <- store.Fetch()
 | 
					 | 
				
			||||||
		// 	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 	// Here we are simply waiting on the which channel
 | 
					 | 
				
			||||||
		// 	// will close out first. If the Done() channel on the request
 | 
					 | 
				
			||||||
		// 	// does store.Cancel() is called.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 	// Remember select lets use wait on multiple async operations and execute cases
 | 
					 | 
				
			||||||
		// 	// based on which one completes first as signaled by data being received on that channel.
 | 
					 | 
				
			||||||
		// 	select {
 | 
					 | 
				
			||||||
		// 	case d := <-data:
 | 
					 | 
				
			||||||
		// 		fmt.Fprint(w, d)
 | 
					 | 
				
			||||||
		// 	case <-ctx.Done():
 | 
					 | 
				
			||||||
		// 		store.Cancel()
 | 
					 | 
				
			||||||
		// 	}
 | 
					 | 
				
			||||||
		data, err := store.Fetch(r.Context())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return // todo log the error however you like
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		fmt.Fprint(w, data)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,175 +0,0 @@
 | 
				
			|||||||
package my_context
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"net/http/httptest"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// THIS WAS WHAT WE DID WHEN WE DIDN"T PROPOGATE CONTEXT
 | 
					 | 
				
			||||||
// type SpyStore struct {
 | 
					 | 
				
			||||||
// 	response string
 | 
					 | 
				
			||||||
// 	cancelled bool
 | 
					 | 
				
			||||||
// 	t *testing.T
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// func (s *SpyStore) assertWasCancelled() {
 | 
					 | 
				
			||||||
// 	s.t.Helper()
 | 
					 | 
				
			||||||
// 	if !s.cancelled {
 | 
					 | 
				
			||||||
// 		s.t.Error("store was not told to cancel")
 | 
					 | 
				
			||||||
// 	}
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// func (s *SpyStore) assertWasNotCancelled() {
 | 
					 | 
				
			||||||
// 	s.t.Helper()
 | 
					 | 
				
			||||||
// 	if s.cancelled {
 | 
					 | 
				
			||||||
// 		s.t.Error("store was told to cancel")
 | 
					 | 
				
			||||||
// 	}
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// func (s *SpyStore) Fetch() string {
 | 
					 | 
				
			||||||
// 	time.Sleep(100 * time.Millisecond)
 | 
					 | 
				
			||||||
// 	return s.response
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// func (s *SpyStore) Cancel() {
 | 
					 | 
				
			||||||
// 	s.cancelled = true
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// func TestServer(t *testing.T) {
 | 
					 | 
				
			||||||
// 	data := "Hello, Gopher"
 | 
					 | 
				
			||||||
// 	t.Run("Happy path", func(t *testing.T) {
 | 
					 | 
				
			||||||
// 		store := &SpyStore{response: data, t: t}
 | 
					 | 
				
			||||||
// 		svr := Server(store)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 		request := httptest.NewRequest(http.MethodGet, "/", nil)
 | 
					 | 
				
			||||||
// 		response := httptest.NewRecorder()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 		svr.ServeHTTP(response, request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 		if response.Body.String() != data {
 | 
					 | 
				
			||||||
// 			t.Errorf(`got "%s" want "%s"`, response.Body.String(), data)
 | 
					 | 
				
			||||||
// 		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 		store.assertWasNotCancelled()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	})
 | 
					 | 
				
			||||||
// 	t.Run("Should cancel", func(t *testing.T) {
 | 
					 | 
				
			||||||
// 		store := &SpyStore{response: data, t: t}
 | 
					 | 
				
			||||||
// 		svr := Server(store)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 		request := httptest.NewRequest(http.MethodGet, "/", nil)
 | 
					 | 
				
			||||||
// 		// we are copying the parent context but with a new Done channel.
 | 
					 | 
				
			||||||
// 		// The returned context's Done channel is closed when the returned cancel
 | 
					 | 
				
			||||||
// 		// function is called or when the parent context's Done channel is closed,
 | 
					 | 
				
			||||||
// 		// whichever happens first.
 | 
					 | 
				
			||||||
// 		cancellingCtx, cancel := context.WithCancel(request.Context())
 | 
					 | 
				
			||||||
// 		time.AfterFunc(5*time.Millisecond, cancel)
 | 
					 | 
				
			||||||
// 		// changing the current request context to our new cancellingCtx
 | 
					 | 
				
			||||||
// 		request = request.WithContext(cancellingCtx)
 | 
					 | 
				
			||||||
// 		response := httptest.NewRecorder()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 		svr.ServeHTTP(response, request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 		store.a
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	})
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type SpyStore struct {
 | 
					 | 
				
			||||||
	response string
 | 
					 | 
				
			||||||
	t        *testing.T
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *SpyStore) Fetch(ctx context.Context) (string, error) {
 | 
					 | 
				
			||||||
	data := make(chan string, 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	go func() {
 | 
					 | 
				
			||||||
		var result string
 | 
					 | 
				
			||||||
		for _, c := range s.response {
 | 
					 | 
				
			||||||
			select {
 | 
					 | 
				
			||||||
			case <-ctx.Done():
 | 
					 | 
				
			||||||
				log.Println("spy store got cancelled")
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			default:
 | 
					 | 
				
			||||||
				// simulates some network traffic?
 | 
					 | 
				
			||||||
				time.Sleep(10 * time.Millisecond)
 | 
					 | 
				
			||||||
				result += string(c)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		data <- result
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	select {
 | 
					 | 
				
			||||||
	case <-ctx.Done():
 | 
					 | 
				
			||||||
		return "", ctx.Err()
 | 
					 | 
				
			||||||
	case res := <-data:
 | 
					 | 
				
			||||||
		return res, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Rolling our own Spy for ResponseWriter because httptest.ResponseRecorder can't be
 | 
					 | 
				
			||||||
// used to determine if any response was writing in the error case
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SpyResponse Writer implements http.ResponseWriter so we can use it in the test.
 | 
					 | 
				
			||||||
type SpyResponseWriter struct {
 | 
					 | 
				
			||||||
	written bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *SpyResponseWriter) Header() http.Header {
 | 
					 | 
				
			||||||
	s.written = true
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *SpyResponseWriter) Write([]byte) (int, error) {
 | 
					 | 
				
			||||||
	s.written = true
 | 
					 | 
				
			||||||
	return 0, errors.New("not implemented")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *SpyResponseWriter) WriteHeader(statusCode int) {
 | 
					 | 
				
			||||||
	s.written = true
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestServer(t *testing.T) {
 | 
					 | 
				
			||||||
	data := "Hello, Gopher"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("Happy Path", func(t *testing.T) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		store := &SpyStore{response: data, t: t}
 | 
					 | 
				
			||||||
		svr := Server(store)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		request := httptest.NewRequest(http.MethodGet, "/", nil)
 | 
					 | 
				
			||||||
		response := httptest.NewRecorder()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		svr.ServeHTTP(response, request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if response.Body.String() != data {
 | 
					 | 
				
			||||||
			t.Errorf(`got "%s", want "%s"`, response.Body.String(), data)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) {
 | 
					 | 
				
			||||||
		store := &SpyStore{response: data, t: t}
 | 
					 | 
				
			||||||
		svr := Server(store)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		request := httptest.NewRequest(http.MethodGet, "/", nil)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		cancellingCtx, cancel := context.WithCancel(request.Context())
 | 
					 | 
				
			||||||
		time.AfterFunc(5*time.Millisecond, cancel)
 | 
					 | 
				
			||||||
		request = request.WithContext(cancellingCtx)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		response := &SpyResponseWriter{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		svr.ServeHTTP(response, request)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if response.written {
 | 
					 | 
				
			||||||
			t.Errorf("A response should not have been written")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
					Loading…
					
					
				
		Reference in New Issue