├── go-context ├── images │ └── context_in_google.png ├── README.md ├── examples │ ├── logger │ │ ├── context.go │ │ └── logger.go │ ├── go_keep_alive.go │ ├── user │ │ └── user.go │ ├── go_keep_alive_1.go │ ├── cancel_ctx.go │ ├── database │ │ ├── context.go │ │ └── database.go │ ├── context_with_value.go │ ├── ctx_and_http_client.go │ ├── logger_in_context.go │ ├── create_context.go │ └── database_in_the_context.go └── ctx.slide └── README.md /go-context/images/context_in_google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkondratovych/golang-ua-meetup/HEAD/go-context/images/context_in_google.png -------------------------------------------------------------------------------- /go-context/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Presentation preview: http://go-talks.appspot.com/github.com/dkondratovych/golang-ua-meetup/go-context/ctx.slide 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-ua-meetup 2 | 3 | List of presentations 4 | 5 | - Context package in golang http://go-talks.appspot.com/github.com/dkondratovych/golang-ua-meetup/go-context/ctx.slide 6 | -------------------------------------------------------------------------------- /go-context/examples/logger/context.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import "context" 4 | 5 | type key int 6 | 7 | const loggerKey key = 0 8 | 9 | // NewContext sets logger to context 10 | func NewContext(ctx context.Context, logger Logger) context.Context { 11 | return context.WithValue(ctx, loggerKey, logger) 12 | } 13 | 14 | // FromContext retrieves logger from context 15 | func FromContext(ctx context.Context) (Logger, bool) { 16 | l, ok := ctx.Value(loggerKey).(Logger) 17 | 18 | return l, ok 19 | } 20 | 21 | // MustFromContext retrieves logger from context. Panics if not found 22 | func MustFromContext(ctx context.Context) Logger { 23 | l, ok := ctx.Value(loggerKey).(Logger) 24 | if !ok { 25 | panic("logger not found in context") 26 | } 27 | 28 | return l 29 | } 30 | -------------------------------------------------------------------------------- /go-context/examples/go_keep_alive.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/test", handlerRequest) 13 | 14 | log.Fatal(http.ListenAndServe(":8181", nil)) 15 | } 16 | 17 | // START1 OMIT 18 | // handlerRequest on each incoming request is handled in its own goroutine // HL 19 | func handlerRequest(w http.ResponseWriter, r *http.Request) { 20 | longRunningCalculation := func() { 21 | for i := 0; ; i++ { 22 | time.Sleep(1 * time.Second) 23 | fmt.Printf("Worker %d \n", i) 24 | } 25 | } 26 | 27 | // goroutine keep working when we return response to a client // HL 28 | go longRunningCalculation() 29 | 30 | io.WriteString(w, "bazinga!") 31 | return 32 | } 33 | 34 | // STOP1 OMIT 35 | -------------------------------------------------------------------------------- /go-context/examples/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "context" 4 | 5 | type User struct { 6 | Name string 7 | Age int64 8 | } 9 | 10 | type contextKey string 11 | 12 | var userContextKey contextKey = "user" 13 | 14 | // NewUserContext adds user to the context OMIT 15 | func NewUserContext(ctx context.Context, user *User) context.Context { 16 | return context.WithValue(ctx, userContextKey, user) 17 | } 18 | 19 | // FromContext retrieves user from context OMIT 20 | func UserFromContext(ctx context.Context) (*User, bool) { 21 | u, ok := ctx.Value(userContextKey).(*User) // HL 22 | return u, ok 23 | } 24 | 25 | // UserMustFromContext retrieves user from context and panics if not found OMIT 26 | func UserMustFromContext(ctx context.Context) *User { 27 | u, ok := ctx.Value(userContextKey).(*User) // HL 28 | if !ok { // HL 29 | panic("user not found in context") // HL 30 | } // HL 31 | return u 32 | } 33 | 34 | // STOP OMIT 35 | -------------------------------------------------------------------------------- /go-context/examples/go_keep_alive_1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | http.HandleFunc("/test", handlerRequestWithCancelation) 14 | 15 | log.Fatal(http.ListenAndServe(":8181", nil)) 16 | } 17 | 18 | // START1 OMIT 19 | // handlerRequest on each incoming request is handled in its own goroutine // HL 20 | func handlerRequestWithCancelation(w http.ResponseWriter, r *http.Request) { 21 | longRunningCalculation := func(ctx context.Context) { 22 | for i := 0; ; i++ { 23 | select { 24 | case <-ctx.Done(): // HL 25 | return 26 | default: 27 | time.Sleep(1 * time.Second) 28 | fmt.Printf("Worker %d \n", i) 29 | } 30 | } 31 | } 32 | 33 | // the context is canceled when the ServeHTTP method returns // HL 34 | go longRunningCalculation(r.Context()) // HL 35 | 36 | // give some time for longRunningCalculation to do some work 37 | time.Sleep(5 * time.Second) 38 | 39 | io.WriteString(w, "bazinga!") 40 | return 41 | } 42 | 43 | // STOP1 OMIT 44 | -------------------------------------------------------------------------------- /go-context/examples/cancel_ctx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | 13 | http.HandleFunc("/test", handlerSearchCancel) 14 | 15 | log.Fatal(http.ListenAndServe(":8181", nil)) 16 | } 17 | 18 | // START1 OMIT 19 | func handlerSearchCancel(w http.ResponseWriter, r *http.Request) { 20 | var ctx context.Context 21 | var cancel context.CancelFunc 22 | 23 | ctx, cancel = context.WithCancel(r.Context()) // HL 24 | defer cancel() // HL 25 | 26 | // Close context.Done channel in 4 seconds 27 | go func() { 28 | time.Sleep(4 * time.Second) 29 | cancel() // HL 30 | }() 31 | 32 | select { 33 | case <-ctx.Done(): // HL 34 | log.Print(ctx.Err()) 35 | return 36 | case result := <-longRunningCalculation(): // HL 37 | io.WriteString(w, result) 38 | } 39 | 40 | return 41 | } 42 | 43 | // STOP1 OMIT 44 | 45 | func longRunningCalculation() <-chan string { 46 | r := make(chan string) 47 | 48 | go func() { 49 | time.Sleep(10 * time.Second) 50 | r <- "I am done" 51 | }() 52 | 53 | return r 54 | } 55 | -------------------------------------------------------------------------------- /go-context/examples/database/context.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "context" 4 | 5 | type key int 6 | 7 | const databaseKey key = 0 8 | 9 | // FromContext retrieves database from context 10 | func FromContext(ctx context.Context) (Database, bool) { 11 | sp, ok := ctx.Value(databaseKey).(Database) 12 | return sp, ok 13 | } 14 | 15 | // START1 OMIT 16 | 17 | // NewContext adds database to context 18 | func NewContext(ctx context.Context, database Database) context.Context { 19 | return context.WithValue(ctx, databaseKey, database) 20 | } 21 | 22 | // MustFromContext retrieves database from context and Panics if not found 23 | func MustFromContext(ctx context.Context) Database { 24 | sp, ok := ctx.Value(databaseKey).(Database) 25 | if !ok { 26 | panic("database was not found in context") 27 | } 28 | return sp 29 | } 30 | 31 | // NewTransactionContext gets database connections from 32 | // existing context, starts transaction and puts it back to context 33 | func NewTransactionContext(ctx context.Context) (context.Context, Database) { // HL 34 | tx := MustFromContext(ctx).MustBeginTransaction() // HL 35 | 36 | return NewContext(ctx, tx), tx // HL 37 | } // HL 38 | 39 | // STOP1 OMIT 40 | -------------------------------------------------------------------------------- /go-context/examples/context_with_value.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/dkondratovych/golang-ua-meetup/go-context/examples/user" 10 | ) 11 | 12 | var ( 13 | ErrUserNotFound = errors.New("User not found") 14 | ) 15 | 16 | func main() { 17 | http.HandleFunc("/test", setUserMiddleware(handleUserRequest)) 18 | 19 | log.Fatal(http.ListenAndServe(":8181", nil)) 20 | } 21 | 22 | func setUserMiddleware(h http.HandlerFunc) http.HandlerFunc { 23 | return func(w http.ResponseWriter, r *http.Request) { 24 | 25 | ctx := user.NewUserContext(r.Context(), &user.User{ // HL 26 | Name: "Gopher", // HL 27 | Age: 20, // HL 28 | }) // HL 29 | 30 | // WithContext returns a shallow copy of r with its context changed to ctx. 31 | r = r.WithContext(ctx) // HL 32 | 33 | h(w, r) 34 | } 35 | } 36 | 37 | // STOP1 OMIT 38 | 39 | func handleUserRequest(w http.ResponseWriter, r *http.Request) { 40 | // Use EntityNameFromContext if value is not critical for function execution 41 | u, ok := user.UserFromContext(r.Context()) // HL 42 | if !ok { // HL 43 | log.Print(ErrUserNotFound) // HL 44 | } // HL 45 | 46 | // Use EntityNameMustFromContext if value from context 47 | // is critical for execution (logger, db =)) 48 | u = user.UserMustFromContext(r.Context()) // HL 49 | 50 | io.WriteString(w, u.Name) 51 | } 52 | 53 | // STOP OMIT 54 | -------------------------------------------------------------------------------- /go-context/examples/ctx_and_http_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | 13 | http.HandleFunc("/test", handlerSearchTimeout) 14 | 15 | http.HandleFunc("/timeout", func(w http.ResponseWriter, r *http.Request) { 16 | time.Sleep(4 * time.Second) 17 | }) 18 | 19 | log.Fatal(http.ListenAndServe(":8181", nil)) 20 | } 21 | 22 | // START1 OMIT 23 | func handlerSearchTimeout(w http.ResponseWriter, r *http.Request) { 24 | var ctx context.Context 25 | var cancel context.CancelFunc 26 | 27 | ctx, cancel = context.WithTimeout(r.Context(), 2*time.Second) // HL 28 | defer cancel() 29 | 30 | request, err := http.NewRequest(http.MethodGet, "http://localhost:8181/timeout", nil) 31 | if err != nil { 32 | log.Println(err.Error()) 33 | return 34 | } 35 | // You can context separately for each request 36 | request = request.WithContext(ctx) // HL 37 | 38 | client := &http.Client{} // HL 39 | response, err := client.Do(request) // HL 40 | // You will get an error "net/http: request canceled" when request timeout exceeds limits // HL 41 | if err != nil { 42 | log.Println(err.Error()) 43 | return 44 | } 45 | // ...... 46 | // STOP2 OMIT 47 | 48 | b, err := ioutil.ReadAll(response.Body) 49 | if err != nil { 50 | log.Println(err.Error()) 51 | return 52 | } 53 | 54 | log.Printf("%s", b) 55 | 56 | return 57 | } 58 | 59 | // STOP1 OMIT 60 | -------------------------------------------------------------------------------- /go-context/examples/logger_in_context.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/dkondratovych/golang-ua-meetup/go-context/examples/logger" 9 | "github.com/seesawlabs/go-flake" 10 | ) 11 | 12 | var globalLogger logger.IRequestScopedLogger 13 | 14 | func main() { 15 | 16 | globalLogger = logger.NewLogger() 17 | 18 | http.HandleFunc("/test", setLoggerMiddleware(handleAndLog)) 19 | 20 | log.Fatal(http.ListenAndServe(":8181", nil)) 21 | } 22 | 23 | // START1 OMIT 24 | func setLoggerMiddleware(h http.HandlerFunc) http.HandlerFunc { 25 | // generate new 64 bits long unique flake id 26 | f, err := flake.New() 27 | if err != nil { 28 | panic("could not initialize flake") 29 | } 30 | 31 | return func(w http.ResponseWriter, r *http.Request) { 32 | // next values in real app will be retrieved from db, cache, http headers etc 33 | requestScopedLogger := globalLogger.GetRequestScoped( // HL 34 | fmt.Sprintf("%x", f.NextId()), // request id // HL 35 | "web", // application name // HL 36 | int64(0), // current user id // HL 37 | ) // HL 38 | 39 | lctx := logger.NewContext(r.Context(), requestScopedLogger) // HL 40 | r = r.WithContext(lctx) // HL 41 | 42 | h(w, r) 43 | } 44 | } 45 | 46 | // STOP1 OMIT 47 | 48 | // START2 OMIT 49 | func handleAndLog(w http.ResponseWriter, r *http.Request) { 50 | l := logger.MustFromContext(r.Context()) // HL 51 | 52 | l.Printf("Bazinga!") 53 | 54 | return 55 | } 56 | 57 | // STOP2 OMIT 58 | -------------------------------------------------------------------------------- /go-context/examples/create_context.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net/http" 8 | // "time" 9 | ) 10 | 11 | func main() { 12 | http.HandleFunc("/test", middleware(handleRequest)) 13 | 14 | log.Fatal(http.ListenAndServe(":8181", nil)) 15 | } 16 | 17 | func middleware(h http.HandlerFunc) http.HandlerFunc { 18 | return func(w http.ResponseWriter, r *http.Request) { 19 | // Create context from context package 20 | // and attach it to request later with r.WithContext() 21 | ctx := context.Background() // HL 22 | // OR 23 | 24 | // Use request method context, which returns context, if ctx is nil 25 | // then creates new background context - context.Background() 26 | ctx = r.Context() // HL 27 | 28 | // Build context variations on top of background context 29 | ctx = context.WithValue(ctx, "some_key", "some_value") 30 | 31 | // tctx, cancelFunc := context.WithTimeout(ctx, time.Duration(5 * time.Second)) 32 | // deadline := time.Now().Add(time.Duration(30 * time.Second)) 33 | // dctx, cancelFunc := context.WithDeadline(ctx, deadline) 34 | 35 | // WithContext returns a shallow copy of r with its context changed to ctx. 36 | r = r.WithContext(ctx) // HL 37 | 38 | h(w, r) 39 | } 40 | } 41 | 42 | func handleRequest(w http.ResponseWriter, r *http.Request) { 43 | ctx := r.Context() // HL 44 | 45 | if err := doSomething(ctx, "payload"); err != nil { // HL 46 | log.Print(err) 47 | } 48 | 49 | io.WriteString(w, "Bazinga!") 50 | } 51 | 52 | // STOP OMIT 53 | 54 | func doSomething(ctx context.Context, payload string) error { 55 | 56 | _ = payload // OMIT 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /go-context/examples/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Sirupsen/logrus" 7 | ) 8 | 9 | const ( 10 | defaultAppName = "GLOBAL" 11 | defaultRequestID = "GLOBAL" 12 | defaultUserID = 0 13 | ) 14 | 15 | // START1 OMIT 16 | type IRequestScopedLogger interface { 17 | Logger 18 | 19 | GetRequestScoped(requestID, appName string, userID int64) Logger // HL 20 | } 21 | 22 | type Logger interface { 23 | Printf(format string, args ...interface{}) 24 | Errorf(format string, args ...interface{}) 25 | } 26 | 27 | type logger struct { 28 | *logrus.Logger 29 | requestID string // HL 30 | appName string // HL 31 | userID int64 // HL 32 | } 33 | 34 | // STOP1 OMIT 35 | 36 | // START2 OMIT 37 | func (l *logger) Printf(format string, args ...interface{}) { 38 | l.WithFields(logrus.Fields{ 39 | "datetime": time.Now(), 40 | "request_id": l.requestID, 41 | "app_name": l.appName, 42 | "user_id": l.userID, 43 | }).Infof(format, args...) 44 | } 45 | 46 | // STOP2 OMIT 47 | func (l *logger) Errorf(format string, args ...interface{}) { 48 | l.WithFields(logrus.Fields{ 49 | "datetime": time.Now(), 50 | "request_id": l.requestID, 51 | "app_name": l.appName, 52 | "user_id": l.userID, 53 | }).Errorf(format, args...) 54 | } 55 | 56 | func (l *logger) GetRequestScoped(requestID, appName string, userID int64) Logger { 57 | return &logger{l.Logger, requestID, appName, userID} 58 | } 59 | 60 | // START1A OMIT 61 | func NewLogger() IRequestScopedLogger { 62 | l := logrus.New() 63 | 64 | return &logger{l, defaultAppName, defaultRequestID, defaultUserID} 65 | } 66 | 67 | // STOP1A OMIT 68 | -------------------------------------------------------------------------------- /go-context/examples/database_in_the_context.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "context" 8 | 9 | "github.com/dkondratovych/golang-ua-meetup/go-context/examples/database" 10 | ) 11 | 12 | func main() { 13 | var err error 14 | DB, err = database.NewDatabase(database.Config{ 15 | IP: "", 16 | User: "", 17 | Password: "", 18 | Name: "", 19 | }) 20 | 21 | if err != nil { 22 | log.Fatal(err.Error()) 23 | } 24 | 25 | http.HandleFunc("/test", setDatabaseMiddleware(handleAndQuery)) 26 | 27 | log.Fatal(http.ListenAndServe(":8181", nil)) 28 | } 29 | 30 | // START1 OMIT 31 | var DB database.Database 32 | 33 | func setDatabaseMiddleware(h http.HandlerFunc) http.HandlerFunc { 34 | 35 | return func(w http.ResponseWriter, r *http.Request) { 36 | dctx := database.NewContext(r.Context(), DB) // HL 37 | 38 | r = r.WithContext(dctx) // HL 39 | 40 | h(w, r) 41 | } 42 | } 43 | 44 | // STOP1 OMIT 45 | 46 | // START2 OMIT 47 | func handleAndQuery(w http.ResponseWriter, r *http.Request) { 48 | // You can retrieve db from context 49 | // if you need to perform single db request 50 | db := database.MustFromContext(r.Context()) // HL 51 | _ = db // OMIT 52 | 53 | // You can create new ctx with tx inside and pass it into functions 54 | txctx, tx := database.NewTransactionContext(r.Context()) // HL 55 | 56 | if err := foo(txctx); err != nil { 57 | tx.Rollback() // HL 58 | return 59 | } 60 | tx.Commit() // HL 61 | 62 | return 63 | } 64 | 65 | func foo(ctx context.Context) error { 66 | db := database.MustFromContext(ctx) 67 | _ = db //OMIT 68 | // Perform db operations 69 | return nil 70 | } 71 | 72 | // STOP2 OMIT 73 | -------------------------------------------------------------------------------- /go-context/examples/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | var ( 12 | // ErrInvalidTransaction invalid transaction when you are trying to `Commit` or `Rollback` 13 | ErrInvalidTransaction = errors.New("no valid transaction") 14 | 15 | // ErrCantStartTransaction can't start transaction when you are trying to start one with `Begin` 16 | ErrCantStartTransaction = errors.New("can't start transaction") 17 | ) 18 | 19 | // START 1A OMIT 20 | type sqlCommon interface { 21 | Exec(query string, args ...interface{}) (sql.Result, error) 22 | Prepare(query string) (*sql.Stmt, error) 23 | Query(query string, args ...interface{}) (*sql.Rows, error) 24 | QueryRow(query string, args ...interface{}) *sql.Row 25 | } 26 | 27 | type sqlDb interface { 28 | Begin() (*sql.Tx, error) 29 | MustBegin() *sql.Tx 30 | } 31 | 32 | type sqlTx interface { 33 | sqlCommon 34 | Commit() error 35 | Rollback() error 36 | } 37 | 38 | // STOP 1A OMIT 39 | 40 | // START1 OMIT 41 | type database struct { 42 | db sqlCommon 43 | } 44 | 45 | type Database interface { 46 | Sql() *sql.DB 47 | 48 | Commit() error 49 | Rollback() error 50 | PingDB() error 51 | 52 | MustBeginTransaction() Database // HL 53 | } 54 | 55 | // STOP1 OMIT 56 | 57 | type Config struct { 58 | IP string 59 | User string 60 | Password string 61 | Name string 62 | } 63 | 64 | func NewDatabase(c Config) (Database, error) { 65 | var err error 66 | 67 | database := new(database) 68 | 69 | database.db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", c.User, c.Password, c.IP, c.Name)) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return database, nil 75 | } 76 | 77 | func (d *database) Sql() *sql.DB { 78 | return d.db.(*sql.DB) 79 | } 80 | 81 | // MustBeginTransaction returns new database object with opened transaction. 82 | // Panics if transaction cannot be opened 83 | func (d *database) MustBeginTransaction() Database { 84 | var db sqlDb 85 | var tx sqlTx 86 | 87 | db, ok := d.db.(sqlDb) 88 | if ok { 89 | tx = db.MustBegin() 90 | } else { 91 | panic(ErrCantStartTransaction) 92 | } 93 | 94 | return &database{ 95 | db: tx, 96 | } 97 | } 98 | 99 | // START2 OMIT 100 | func (d *database) Commit() error { 101 | if tx, ok := d.db.(sqlTx); ok { // HL 102 | return tx.Commit() 103 | } 104 | 105 | return ErrInvalidTransaction 106 | } 107 | 108 | // STOP2 OMIT 109 | func (d *database) Rollback() error { 110 | if tx, ok := d.db.(sqlTx); ok { // HL 111 | return tx.Rollback() 112 | } 113 | 114 | return ErrInvalidTransaction 115 | } 116 | 117 | func (d *database) PingDB() error { 118 | return d.Sql().Ping() 119 | } 120 | -------------------------------------------------------------------------------- /go-context/ctx.slide: -------------------------------------------------------------------------------- 1 | Context package in Go 2 | 20 Sep 2016 3 | Tags: go, golang, context, http 4 | 5 | Dmytro Kondratovych 6 | Gopher, SeeSawLabs 7 | dmytro.kondratovych@gmail.com 8 | @dkondratovych 9 | 10 | * Some history 11 | 12 | The idea to create Context was posted by [[https://groups.google.com/forum/#!msg/golang-nuts/teSBtPvv1GQ/U12qA9N51uIJ][Brad Fitzpatrick]] to the go-nuts mailing list back in 2011. 13 | 14 | Later package Context was developed in Google to make it easy to pass request specific values, cancelation, timeout and deadline signals to all goroutines involved in handling a request. Package started it's life under *golang.org/x/net/context* 15 | 16 | After becoming more popular and proving to be essential to many Go applications, package was moved [[https://github.com/golang/go/issues/14660][(GitHub issue)]] to standard library and named *context* since context by itself has little to do with networking by itself. 17 | 18 | In Go 1.7 support for contexts has been added to the [[https://golang.org/pkg/net/][net]], [[https://golang.org/pkg/net/http/][net/http]], and [[https://golang.org/pkg/os/exec/][os/exec]] packages. 19 | 20 | 21 | * Context interface 22 | 23 | type Context interface { 24 | // Deadline returns the time when work done 25 | // on behalf of this context should be canceled. 26 | Deadline() (deadline time.Time, ok bool) 27 | 28 | // Done returns a channel that's closed when work done 29 | // on behalf of this context should be canceled. 30 | Done() <-chan struct{} 31 | 32 | // Err returns a non-nil error value after Done is closed. 33 | Err() error 34 | 35 | // Value returns the value associated with this context for key 36 | Value(key interface{}) interface{} 37 | } 38 | 39 | 40 | * Context functions 41 | 42 | func Background() Context 43 | 44 | func TODO() Context 45 | 46 | func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 47 | 48 | func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) 49 | 50 | func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 51 | 52 | func WithValue(parent Context, key, val interface{}) Context 53 | 54 | 55 | * How to create context ? (1/1) 56 | 57 | 58 | Before Go 1.7, to create and pass context through middleware chain and access it in handler, you had to choose between three possible options 59 | - use a global request-to-context mapping 60 | - create a http.ResponseWriter wrapper struct 61 | - create your own handler types 62 | 63 | 64 | In Go 1.7 it became more easy and clear to create context. Context has become a part of *http.Request*. 65 | 66 | * How to create context ? (1/2) 67 | 68 | .code examples/create_context.go /func middleware/,/^}/ 69 | 70 | 71 | * How to access context in handler ? 72 | .code examples/create_context.go /func handleRequest/,/STOP/ 73 | 74 | 75 | * context.WithValue (1/1) 76 | 77 | func WithValue(parent Context, key, val interface{}) Context 78 | 79 | WithValue returns a copy of parent in which the value associated with key is val. 80 | 81 | Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions or dependecy injection container. 82 | 83 | * context.WithValue (1/2) 84 | 85 | Context key should be unexported type for keys defined in this package. This prevents collisions with keys defined in other packages. 86 | 87 | 88 | package user 89 | 90 | type contextKey int 91 | 92 | var userKey contextKey = 0 93 | 94 | 95 | You can have multiple package.contextKey type variables per one package, just make sure that provided strings, numbers are not collide. 96 | 97 | package user 98 | 99 | type contextKey string 100 | 101 | var userProfileKey = contextKey("user-profile") 102 | var userKey = contextKey("user") 103 | 104 | * context.WithValue(1/3) 105 | Since context keys are unexported you have to use helper functions to set and get value from the context. 106 | 107 | .code examples/user/user.go /type contextKey/,/STOP/ 108 | 109 | * context.WithValue(1/4) 110 | An example how to write to context 111 | 112 | .code examples/context_with_value.go /func setUserMiddleware/,/STOP1/ 113 | 114 | * context.WithValue(1/5) 115 | An example how to read from context 116 | 117 | .code examples/context_with_value.go /func handleUserRequest/,/STOP/ 118 | 119 | 120 | * What to put into context ? (1/1) 121 | 122 | [[https://blog.golang.org/context][Go Blog]] says us to put into context: 123 | - user identity 124 | - authorization tokens 125 | - request-scoped values 126 | 127 | Basically all data during request lifetime can be treated as request-scoped - that's the main problem. It adds uncertainty to what is appropriate to put into context and what is not. 128 | 129 | In addition context may be used to store specific values: 130 | - request id 131 | - user ip 132 | - session id 133 | - cookies 134 | 135 | * What to put into contxt ? (1/2) 136 | 137 | .image images/context_in_google.png 550 _ 138 | 139 | 140 | * What you should remember before seting anything into context ? 141 | 142 | - It’s completely type-unsafe, you get interface{} from the context. 143 | - Cannot be checked by the compiler. 144 | - Doesn't make dependencies explicit. User has to read the implementation, which is not good. 145 | - Context.Value obscures your program’s flow 146 | 147 | 148 | * Do not "over context" 149 | 150 | Try to keep dependencies explicit. Make functions easy to read, test and check by the compiler. 151 | 152 | // Function dependency is not clear, avoid using context like this 153 | func calculatePaysleep(ctx context.Contex) int64 { 154 | user := user.UserMustFromContext(ctx) 155 | rate := rate.RateMustFromContext(ctx) 156 | work := work.WorkFromContext(ctx) 157 | 158 | return user * rate * work 159 | } 160 | 161 | 162 | * What Go community members recommend to set into context ? (1/1) 163 | 164 | [[http://peter.bourgon.org/blog/2016/07/11/context.html][Peter Bourgon]] recommends using context to store values, like strings and data structs and avoid using it to store references, like pointers and handlers. Also store only request-scoped logger, if needed. 165 | 166 | * What Go community members recommend to set into context ? (1/2) 167 | 168 | [[https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39#.85mq6xwv6][Jack Lindamood]] recomends to store request scooped data related to who is making the request (user ID), how are they are making it (internal or external), from where are they making it from (user IP) and how important this request should be. 169 | 170 | A database connection may be considered request-scooped if it contains some metadata about current user. 171 | 172 | A logger may be considered request-scooped if it contains metadata about who sent the request. 173 | 174 | Context.Value should inform, not control application flow. For example if your function can’t behave correctly because of a value that may or may not be inside context.Value, then your API is obscuring required inputs too heavily. 175 | 176 | * So what should I do? 177 | 178 | There is no bulletproof rules for what to store in the context! Try, analyze and then make your own decisions about what is good or bad for your particular project. 179 | 180 | 181 | * Logger in the context (1/1) 182 | 183 | You can extend your base logger with request specific fields and provide an interface to create request scoped logger. 184 | 185 | .code examples/logger/logger.go /START1/,/STOP1/ 186 | 187 | 188 | * Logger in the context (1/2) 189 | 190 | Base logger can be created with default/known values. 191 | 192 | .code examples/logger/logger.go /START1A/,/STOP1A/ 193 | 194 | And request scoped logger interface implementation will look like: 195 | .code examples/logger/logger.go /START2/,/STOP2/ 196 | 197 | * Logger in the context (1/3) 198 | 199 | You have to init request scoped logger in the middleware 200 | 201 | .code examples/logger_in_context.go /START1/,/STOP1/ 202 | 203 | * Logger in the context (1/4) 204 | 205 | An example of how to access logger from the context 206 | 207 | .code examples/logger_in_context.go /START2/,/STOP2/ 208 | 209 | 210 | * Database in the context (1/1) 211 | Storing database connection pool in the context is controversial decision. For sure database itself is not request scoped, but database transactions are kind of related to request lifetime data. 212 | 213 | As an example lets see how it looks like to store database in the context and how this approach may simplify sharing database tranasction across functions. 214 | 215 | 216 | * Database in the context (1/2) 217 | To distinguish Tx from Db we need to create separate interfaces for assertions. 218 | 219 | .code examples/database/database.go /START 1A/,/STOP 1A/ 220 | 221 | * Database in the context (1/3) 222 | 223 | MustBeginTransaction starts transaction and creates new database instance 224 | 225 | .code examples/database/database.go /START1/,/STOP1/ 226 | 227 | * Database in the context (1/4) 228 | 229 | To make Commit and Rollback working with *sql.DB we need to do some type assertion 230 | 231 | .code examples/database/database.go /START2/,/STOP2/ 232 | 233 | * Database in the context (1/5) 234 | You have to set DB into context in the middleware. 235 | 236 | .code examples/database_in_the_context.go /START1/,/STOP1/ 237 | 238 | * Database in the context (1/6) 239 | .code examples/database/context.go /START1/,/STOP1/ 240 | 241 | * Database in the context (1/7) 242 | .code examples/database_in_the_context.go /START2/,/STOP2/ 243 | 244 | 245 | * Blocking and long running operations (1/1) 246 | 247 | You should always have ability to cancel long running and blocking operations because function may work longer than we actually need. 248 | 249 | * Why should I care about blocking / long running operations ? (1/2) 250 | 251 | .code examples/go_keep_alive.go /START1/,/STOP1/ 252 | 253 | 254 | * What can I do with this ? (1/3) 255 | 256 | .code examples/go_keep_alive_1.go /START1/,/STOP1/ 257 | 258 | * How can I explicitly cancel context ? 259 | 260 | .code examples/cancel_ctx.go /START1/,/STOP1/ 261 | 262 | * How can I use WithTimeout and WithDeadline ? 263 | 264 | Context with timeout 265 | 266 | ctx, cancel = context.WithTimeout(ctx, time.Duration(7 * time.Second)) 267 | defer cancel() 268 | 269 | Context with deadline 270 | 271 | deadline := time.Now().Add(time.Duration(30 * time.Second)) 272 | dctx, cancel := context.WithDeadline(ctx, deadline) 273 | defer cancel() 274 | 275 | Pattern here is the same, you have to wait for signal from ctx.Done 276 | 277 | select { 278 | case <-ctx.Done(): 279 | log.Print(ctx.Err()) 280 | return 281 | case result := <-longRunningCalculation(): 282 | io.WriteString(w, result) 283 | } 284 | 285 | 286 | 287 | * Context in outgoing client requests (1/1) 288 | 289 | .code examples/ctx_and_http_client.go /START1/,/STOP2/ 290 | 291 | * golang.org/x/net/context package usage 292 | 293 | .iframe https://godoc.org/golang.org/x/net/context?importers 600 1000 294 | 295 | * context package usage 296 | 297 | .iframe https://godoc.org/context?importers 600 1000 --------------------------------------------------------------------------------