gin.Context is not a interface, how to extend it?
What's your scenario? I think context is the core part of gin that coordinates other components, like controls the middlewares execution, and model binding...etc. It'll make the system more complicated if you change the default behavior of such core components.
do you mean you want to share service for all request handlers globally?
@ycavatars Interface makes it possible to extend context, for example, you can add UserID field to context, and call context.UserID directly instead of using context.GetString("UserID").
any news? :)
Here's some input about supporting custom Context in Gin. I'm not very experienced so take it with a grain of salt.
type MyContext struct {
*gin.Context
// Value set in my custom middleware
User *AuthenticatedUser
}
func indexHandler (c *MyContext) {
fmt.Print(c.User)
}
Advantages:
โ
c.User is typed
โ
no typecasting from interface{}
โ
no map lookup
โ
wont compile if dev mistypes c.User
โ
is available to all handlers, per request, without extra work
โ
editor autocompletion at all times
Instead Gin, and most libs, force me to do something along the lines of:
func indexHandler (c *Context) {
user, ok =: c.Get("user").(*AuthenticatedUser)
if ok {
fmt.Print(user)
} else {
// handle case when "user" key was not found somehow
}
}
โ bloat
โ typecast from interface{}
โ map lookup
โ "user" key prone to mistyping*
โ editors wont autocomplete until you finished casting it
*Note that we are not supposed to use strings as keys in stuff like that, which alleviates some of the problems. Although there's code on Gin's frontpage using string as key (https://i.imgur.com/Kdbr9rg.png), perhaps for simplicity sake. This part of https://golang.org/pkg/context/#WithValue provides some reasoning on why not using string is good practice:
The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys. To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables' static type should be a pointer or interface.
There are ways to mitigate some of the bloat. One is encapsulating the typecasting logic inside helper functions (aka shove the bloat somewhere only we know), typically contained inside an App struct so we don't pollute global scope:
func (app *App) User(c *gin.Context) *AuthenticatedUser {
if user, ok =: c.Get("user").(*AuthenticatedUser); ok {
return user
}
return &AuthenticatedUser{} // Or perhaps nil
}
So now our handlers look something along the lines of:
func (app *App) indexHandler (c *gin.Context) {
fmt.Print(app.User(c))
}
To me as a lib user, Context's map[interface{}]interface{} provided by standard library, Gin and most other libs is a bit disappointing. From my brief evaluation of alternatives so far, gorilla mux, echo and gocraft/web seems to tackle this problem in different ways:
echo - Allows for declaring a custom Context struct but requires one to typecast from default Context to it in EVERY handler. I don't see much gain here.
Extending Gorilla Mux Tutorial (Pt. 1) - Tutorial on how to use Custom context on Gorilla mux. But as far as I understood, it doesn't support request scoped values. So it's not much different than declaring handlers as methods of a struct and putting data like Db connection in that struct to be accessible from all handlers. I could be wrong, but that's what it looks.
HTTP Request Contexts & Go (2004) - Suggests some interesting alternatives but in the end settles with helper() methods to encapsulate typecasting.
I'm going to study the problem further.
After some careful consideration I decided to go with the helper() solution described in Mitigating Context map problems. Some considerations:
Injecting dependencies (like Db connection) in handlers is done via App struct fields which is not as ideal as the more functional approach of passing them via parameters when building each handler.
I should think about making the App struct and their handlers testable. Perhaps not all routes but I'm sure I'll want to test some routes and in those tests I'll want to pass fake dependencies such as a fake (or lightweight SQLite) database connection.
Accessing request scoped data like authenticated user will still require writing a helper function that executes a map lookup and typecast from interface{} every time it is accessed. But since this bloat is encapsulated, I can live with that. For now.
One bad thing of gin.Context is that it's hard to cooperate with standard Context.
For example, to add user's KV to gin.Context:
func UserHandler(gc *gin.Context) {
ctx := context.WithValue(gc, "user_defined_key", "user_value")
//or context.WithTimeout context.WithDeadline etc
UserFunc1(ctx)
}
func UserFunc1(ctx context.Context) {
//do something with user_defined_key
v, ok := ctx.Value("user_defined_key").(string)
//.....
//we can never get gin.Context back anyway...
}
so, I modify the gin.Context, to make it more compatible with standard context:
https://github.com/unlikezy/gin
Above example could be done by this:
func UserHandler(gc *gin.Context) {
ctx := context.WithValue(gc, "user_defined_key", "user_value")
UserFunc1(ctx)
}
func UserFunc1(ctx context.Context) {
//do something with user_defined_key
v, ok := ctx.Value("user_defined_key").(string)
//.....
gc := gin.MustGinContext(ctx)
//to do things with gin.Context as need
}
I encounter same problem ,I need to get gin.Context work with OpenTracing client packet which work with context.Context,but cannot get gin.Context back after process...hope I can simply get gin.Context from context.Context,at least provide some method can merge value data from context.Context to gin.Context.
span, ctx := opentracing.StartSpanFromContext(c, opName)
// c.with
defer span.Finish()
span.LogFields(
log.String("event", "soft error"),
log.String("type", "cache timeout"),
log.Int("waited.millis", 1500))
h(ctx.(*gin.Context))//can get the context back to gin.context since it is actually valueCtx struct type
well fine,it looks like almost impossible to copy all value from standard context since valueCtx is private recursive structure...especially the key is private type in opentracing...I can't even use the key manually outside that package. I hope there is any way to pass regular context's Values though gin.context.
ok I found that I can pass context though Request's ctx ,not pretty but it should work
Most helpful comment
@ycavatars Interface makes it possible to extend context, for example, you can add UserID field to context, and call context.UserID directly instead of using context.GetString("UserID").