How to pass context.Context to different packages so values can be retrieved using keys?

165 Views Asked by At

I am trying to put database transaction in context so I can commit or rollback right before sending a response. I have trouble passing the context between packages. Even thought I set the value for given key in service package, in server package for the same key, values is nil. Does anyone know how to solve this issue ?

package server

import (
    "e-commerce/models"
    "encoding/json"
    "net/http"
)

func (s *Server) CreateCategory(w http.ResponseWriter, req *http.Request) {
    ctx := req.Context()
    var category models.Category

    // decoding json message to user model
    err := json.NewDecoder(req.Body).Decode(&category)
    if err != nil {
        returnResponse(ctx, w, http.StatusBadRequest, err)
        return
    }

    _, err = s.service.CreateCategory(ctx, &category)
    if err != nil {
        returnResponse(ctx, w, http.StatusBadRequest, err)
        return
    }

    returnResponse(ctx, w, http.StatusOK, err)
    return

}
package services

import (
    "context"
    "e-commerce/models"
)

func (s Service) CreateCategory(ctx context.Context, category *models.Category) (*models.Category, error) {
    err := category.Validate()
    if err != nil {
        return nil, err
    }

    tx := s.userRepository.Db().Begin()

    context.WithValue(ctx, "transaction", tx)

    result := s.userRepository.Db().Create(&category)
    if result.Error != nil {
        return nil, result.Error
    }

    return category, nil
}
package server

import (
    "context"
    "e-commerce/config"
    "e-commerce/services"
    "github.com/gorilla/mux"
    "gorm.io/gorm"
    "net/http"
)

func returnResponse(ctx context.Context, w http.ResponseWriter, status int, err error) {

    tx := ctx.Value("transaction")
    if status != 200 {
        tx.(*gorm.DB).Rollback()
    } else {
        tx.(*gorm.DB).Commit()
    }

    w.WriteHeader(status)
    if err != nil {
        w.Write([]byte(err.Error()))
    }
}
1

There are 1 best solutions below

0
Burak Serdar On

Adding values to a context works like the layers of an onion: Each new added value creates a new context wrapping the old one. The new context has the added value, but the old one doesn't. If you cancel a context, all contexts wrapping it will be canceled.

So, if you want to call a function that adds things to a context, you can:

func (s Server) Handler(w http.ResponseWriter, req *http.Request) {
   ...
   newCtx:= s.Service.CreateCategory(req.Context(), ...)
   // pass newCtx to other functions
   ...
}

func (svc Service) CreateCategory(ctx context.Context,...) (context.Context,error) {
   ...
   newCtx:=context.WithValue(ctx,"transaction",tx)
   ...
   return newCtx,nil
}

Alternatively, you can prepare the context in the handler, so other functions can modify it:

type ContextData struct {
   Tx *sql.Transaction
   ...
}


func (s Server) Handler(w http.ResponseWriter, req *http.Request) {
 
 
 newReq:=req.WithContext(context.WithValue(req.Context(),"data",&ContextData{}))
  s.Svc.CreateCategory(newReq.Context(),...)
...
}

func (svc Service) CreateCategory(ctx context.Context,...) error {
   data:=ctx.Value("data").(*ContextData)
   data.Tx= ... // Create new transaction
   ...
}