julienschmidt / httprouter

A high performance HTTP request router that scales well
https://pkg.go.dev/github.com/julienschmidt/httprouter
BSD 3-Clause "New" or "Revised" License
16.64k stars 1.47k forks source link

How to pass the variable to 'handler'? #198

Closed sttyru closed 5 years ago

sttyru commented 7 years ago

Hello!

My issue were not necessarily linked with julienschmidt/httprouter but I sure you can to help me. I try to write a little application at Go for simulate a behaviour of some legacy HTTP-backends. My choice fell on julienschmidt/httprouter because this tool have an impressive performance and I found any pretty examples. But I have a trouble by the one part of code. I need to pass the variable to 'handler'. I have an huge JSON-like structure filled by a data from the database. I need to use it in whole or in part. Of cource, I can to call the function for re-filling a structure at the everyone HTTP-request but it's too slow. I found some mentions about 'wrappers' for function but I don't know how to apply this. Besides, I'm guess that not everything is what it appears on the surface. Could you kindly share a code for newbies like a me, please? :)

Thank you!

sttyru commented 7 years ago

I want to get something like that (it's a plot only):

func main() {
...
        s := query();
        r := httprouter.New();
        r.GET("/", GetAnIndex, s );
}
...
func GetAnIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params, s *Settings) {
....
    fmt.Println(s.Path);
...
}
falmar commented 7 years ago

Hello, are you using Go 1.7+ ? I could give you and example using the context package. And no, you cannot change the the interface type httprouter.Handle func(http.ResponseWriter, *http.Request, Params)

sttyru commented 7 years ago

Hello, @falmar! Thank you! Can you share an example?

Yes. I'm using go 1.7.4.

go version go1.7.4 linux/amd64
falmar commented 7 years ago

I encourage you to use the standard http.Handler func (w http.ResponseWriter, r *http.Request) instead of httprouter.Handle which may change in favor of the context package.

This would be a standard http.Handler

func getIndex(w http.ResponseWriter, r *http.Request) {
  //...

  // ugh... but here we don't have access to s
  fmt.Println(s.Path);

  //...
}

Well s does't exist in the scope and besides where the hell is ps httprouter.Params? lets fix that!

First you would need to wrap julien's httprouter.Handle to take the Params, put them in the context and then call your http.Handler.

The wrapper receives a http.Handler and returns a httprouter.Handle to satisfy httprouter

func wrapHandler(h http.Handler) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        // Take the context out from the request
        ctx := r.Context()

        // Get new context with key-value "params" -> "httprouter.Params"
        ctx = context.WithValue(ctx, "params", ps)

        // Get new http.Request with the new context
        r = r.WithContext(ctx)

        // Call your original http.Handler
        h.ServeHTTP(w, r)
    }
}

You now can take the params out in your GetIndex handler

func getIndex(w http.ResponseWriter, r *http.Request) {
    //...

    // You can take the params this way
    ps, ok := r.Context().Value("params").(httprouter.Params)

    if !ok {
        log.Fatal("ps is not type httprouter.Params")
    }

    //...
}

I ignore where your Settings are coming from, but there could be two approaches, use a middleware to pass the settings down the context or use a wrapper similar to the func wrapHandler

1) Use a wrapper

func getIndexWithSettings(s Settings) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        //

        // Settings is in the scope
        fmt.Println(s.Path)

        //
    })
}

func getIndexWithSettings2(s Settings) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        //

        // Settings is in the scope and ps httprouter.params
        fmt.Println(s.Path)

        //
    }
}

2) Use a middleware

func settingsMiddleware(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Take the context out from the request
        ctx := r.Context()

        // Get the settings
        s := somewhere()

        // Get new context with key-value "settings"
        ctx = context.WithValue(ctx, "settings", s)

        // Get new http.Request with the new context
        r = r.WithContext(ctx)

        // Call your original http.Handler
        h.ServeHTTP(w, r)
    })
}

This is how you would use any of this approaches

func main() {
    r := httprouter.New()

    // wrapHandler
    r.GET("/index", wrapHandler(http.HandlerFunc(getIndex)))

    // inject settings
    r.GET("/index", getIndexWithSettings(s))

    // middleware
    r.GET("/index", wrapHandler(settingsMiddleware(http.HandlerFunc(getIndex))))

        http.ListenAndServe(addr, r)
}

But chained all this handlers and wrappers will be cumbersome you could use Alice to fix that

sttyru commented 7 years ago

@falmar Thank you for the comprehensive answer! You are guru of Go! But in my sorry I can't to build it :-( Help me, please, again. Sorry, I know it's rude.

package main

import (
         "fmt"         
         "context"
         "net/http"
         "github.com/julienschmidt/httprouter"
)

func main() {
            r := httprouter.New()
            var s = "/var/bin"
            r.GET("/index", getIndexWithSettings(s))          
            http.ListenAndServe(":8080", r)
}

func wrapHandler(h http.Handler) httprouter.Handle {
        return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        // Take the context out from the request
        ctx := r.Context()
        ctx = context.WithValue(ctx, "params", ps)
        r = r.WithContext(ctx)
        h.ServeHTTP(w, r)
    }
}       

func getIndex(w http.ResponseWriter, r *http.Request) {
    ps, ok := r.Context().Value("params").(httprouter.Params)
    if !ok {
        fmt.Println("ps is not type httprouter.Params")
    }
}

func getIndexWithSettings(s string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(s)
    })
}
/*
func settingsMiddleware(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        var s = "/var/bin"
        ctx = context.WithValue(ctx, "params", s)
        r = r.WithContext(ctx)
        h.ServeHTTP(w, r)
    })
}
*/

Message error:

 cannot use getIndexWithSettings(s) (type http.Handler) as type httprouter.Handle in argument to r.GET
falmar commented 7 years ago

Ok, look func getIndexWithSettings is returning http.Handler, it does not satisfy httprouter.Get(string, httprouter.Handle)

change this func

func getIndexWithSettings(s string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(s)
    })
}

for this

func getIndexWithSettings(s string) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        fmt.Println(s)
    }
}

And you will end up with this

package main

import (
    "fmt"
    "net/http"

    "github.com/julienschmidt/httprouter"
)

func main() {
    r := httprouter.New()
    var s = "/var/bin"
    r.GET("/index", getIndexWithSettings(s))
    http.ListenAndServe(":8080", r)
}

func getIndexWithSettings(s string) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        fmt.Println(s)
    }
}

You should take a look at the middleware approach as a better practice when writing future programs

package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/julienschmidt/httprouter"
)

func main() {
    r := httprouter.New()

    index := settingsMiddleware(http.HandlerFunc(getIndex))

    r.GET("/index", wrapHandler(index))

    http.ListenAndServe(":8080", r)
}

func wrapHandler(h http.Handler) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        // Take the context out from the request
        ctx := r.Context()
        ctx = context.WithValue(ctx, "params", ps)
        r = r.WithContext(ctx)
        h.ServeHTTP(w, r)
    }
}

func settingsMiddleware(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        var s = "/var/bin"

        ctx := r.Context()
        ctx = context.WithValue(ctx, "settings", s)
        r = r.WithContext(ctx)
        h.ServeHTTP(w, r)
    })
}

func getIndex(w http.ResponseWriter, r *http.Request) {
    s, ok := r.Context().Value("settings").(string)

    if !ok {
        fmt.Println("s is not type string")
    }

    fmt.Println(s) // "/var/bin"
}
sttyru commented 7 years ago

Thank you very much for your help! I've modified my source code and HTTP router do everything it takes! :) Thank you!

Now I want to force drop a connection, but that's another story.