41

Is there a native way for inplace url parameters in native Go?

For Example, if I have a URL: http://localhost:8080/blob/123/test I want to use this URL as /blob/{id}/test.

This is not a question about finding go libraries. I am starting with the basic question, does go itself provide a basic facility to do this natively.

Jay
  • 19,649
  • 38
  • 121
  • 184
Somesh
  • 1,235
  • 2
  • 13
  • 29

9 Answers9

31

There is no built in simple way to do this, however, it is not hard to do.

This is how I do it, without adding a particular library. It is placed in a function so that you can invoke a simple getCode() function within your request handler.

Basically you just split the r.URL.Path into parts, and then analyse the parts.

// Extract a code from a URL. Return the default code if code
// is missing or code is not a valid number.
func getCode(r *http.Request, defaultCode int) (int, string) {
        p := strings.Split(r.URL.Path, "/")
        if len(p) == 1 {
                return defaultCode, p[0]
        } else if len(p) > 1 {
                code, err := strconv.Atoi(p[0])
                if err == nil {
                        return code, p[1]
                } else {
                        return defaultCode, p[1]
                }
        } else {
                return defaultCode, ""
        }
}
Jay
  • 19,649
  • 38
  • 121
  • 184
16

Well, without external libraries you can't, but may I recommend two excellent ones:

  1. httprouter - https://github.com/julienschmidt/httprouter - is extremely fast and very lightweight. It's faster than the standard library's router, and it creates 0 allocations per call, which is great in a GCed language.

  2. Gorilla Mux - http://www.gorillatoolkit.org/pkg/mux - Very popular, nice interface, nice community.

Example usage of httprouter:

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/hello/:name", Hello)

    log.Fatal(http.ListenAndServe(":8080", router))
}
Not_a_Golfer
  • 47,012
  • 14
  • 126
  • 92
  • 36
    If you can't without external libraries, how are those libraries implemented? –  Mar 23 '15 at 15:30
  • 8
    @райтфолд they are implemented at the lowest level, bypassing the default http router. I understood OP's question as "is there a way to do this using the default http library". Well - just to implement your own router. And there really isn't a need for that. – Not_a_Golfer Mar 23 '15 at 15:31
  • I'd also recommend chi - https://github.com/go-chi/chi – Steven Eckhoff Aug 05 '20 at 10:47
4

What about trying using regex, and find a named group in your url, like playground:

package main

import (
    "fmt"
    "net/url"
    "regexp"
)

var myExp = regexp.MustCompile(`/blob/(?P<id>\d+)/test`) // use (?P<id>[a-zA-Z]+) if the id is alphapatic

func main() {

    s := "http://localhost:8080/blob/123/test"

    u, err := url.Parse(s)
    if err != nil {
        panic(err)
    }

    fmt.Println(u.Path)

    match := myExp.FindStringSubmatch(s) // or match := myExp.FindStringSubmatch(u.Path)
    result := make(map[string]string)
    for i, name := range myExp.SubexpNames() {
        if i != 0 && name != "" {
            result[name] = match[i]
        }
    }
    fmt.Printf("id: %s\n", result["id"])

}

output

/blob/123/test
id: 123

Below full code to use it with url, that is receiving http://localhost:8000/hello/John/58 and returning http://localhost:8000/hello/John/58:

package main

import (
    "fmt"
    "net/http"
    "regexp"
    "strconv"
)

var helloExp = regexp.MustCompile(`/hello/(?P<name>[a-zA-Z]+)/(?P<age>\d+)`)

func hello(w http.ResponseWriter, req *http.Request) {
    match := helloExp.FindStringSubmatch(req.URL.Path)
    if len(match) > 0 {
        result := make(map[string]string)
        for i, name := range helloExp.SubexpNames() {
            if i != 0 && name != "" {
                result[name] = match[i]
            }
        }
        if _, err := strconv.Atoi(result["age"]); err == nil {
            fmt.Fprintf(w, "Hello, %v year old named %s!", result["age"], result["name"])
        } else {
            fmt.Fprintf(w, "Sorry, not accepted age!")
        }
    } else {
        fmt.Fprintf(w, "Wrong url\n")
    }
}

func main() {

    http.HandleFunc("/hello/", hello)

    http.ListenAndServe(":8090", nil)
}
Hasan A Yousef
  • 22,789
  • 24
  • 132
  • 203
  • While `regexp` would work, it is a lot slower than the accepted answer. Also there are enough routers written nowadays that do this and we really do not need to reimplement this again and again. – TehSphinX Jan 10 '21 at 19:35
2

How about writing your own url generator (extend net/url a little bit) as below.

// --- This is how does it work like --- //
url, _ := rest.NewURLGen("http", "stack.over.flow", "1234").
    Pattern(foo/:foo_id/bar/:bar_id).
    ParamQuery("foo_id", "abc").
    ParamQuery("bar_id", "xyz").
    ParamQuery("page", "1").
    ParamQuery("offset", "5").
    Do()

log.Printf("url: %s", url) 
// url: http://stack.over.flow:1234/foo/abc/bar/xyz?page=1&offset=5

// --- Your own url generator would be like below --- //
package rest

import (
    "log"
    "net/url"
    "strings"

    "straas.io/base/errors"

    "github.com/jinzhu/copier"
)

// URLGen generates request URL
type URLGen struct {
    url.URL

    pattern    string
    paramPath  map[string]string
    paramQuery map[string]string
}

// NewURLGen new a URLGen
func NewURLGen(scheme, host, port string) *URLGen {
    h := host
    if port != "" {
        h += ":" + port
    }

    ug := URLGen{}
    ug.Scheme = scheme
    ug.Host = h
    ug.paramPath = make(map[string]string)
    ug.paramQuery = make(map[string]string)

    return &ug
}

// Clone return copied self
func (u *URLGen) Clone() *URLGen {
    cloned := &URLGen{}
    cloned.paramPath = make(map[string]string)
    cloned.paramQuery = make(map[string]string)

    err := copier.Copy(cloned, u)
    if err != nil {
        log.Panic(err)
    }

    return cloned
}

// Pattern sets path pattern with placeholder (format `:<holder_name>`)
func (u *URLGen) Pattern(pattern string) *URLGen {
    u.pattern = pattern
    return u
}

// ParamPath builds path part of URL
func (u *URLGen) ParamPath(key, value string) *URLGen {
    u.paramPath[key] = value
    return u
}

// ParamQuery builds query part of URL
func (u *URLGen) ParamQuery(key, value string) *URLGen {
    u.paramQuery[key] = value
    return u
}

// Do returns final URL result.
// The result URL string is possible not escaped correctly.
// This is input for `gorequest`, `gorequest` will handle URL escape.
func (u *URLGen) Do() (string, error) {
    err := u.buildPath()
    if err != nil {
        return "", err
    }
    u.buildQuery()

    return u.String(), nil
}

func (u *URLGen) buildPath() error {
    r := []string{}
    p := strings.Split(u.pattern, "/")

    for i := range p {
        part := p[i]
        if strings.Contains(part, ":") {
            key := strings.TrimPrefix(p[i], ":")

            if val, ok := u.paramPath[key]; ok {
                r = append(r, val)
            } else {
                if i != len(p)-1 {
                    // if placeholder at the end of pattern, it could be not provided
                    return errors.Errorf("placeholder[%s] not provided", key)
                }
            }
            continue
        }
        r = append(r, part)
    }

    u.Path = strings.Join(r, "/")
    return nil
}

func (u *URLGen) buildQuery() {
    q := u.URL.Query()
    for k, v := range u.paramQuery {
        q.Set(k, v)
    }
    u.RawQuery = q.Encode()
}
Browny Lin
  • 2,427
  • 3
  • 28
  • 32
2

With net/http the following would trigger when calling localhost:8080/blob/123/test

http.HandleFunc("/blob/", yourHandlerFunction)

Then inside yourHandlerFunction, manually parse r.URL.Path to find 123.

Note that if you don't add a trailing / it won't work. The following would only trigger when calling localhost:8080/blob:

http.HandleFunc("/blob", yourHandlerFunction)
Stéphane Bruckert
  • 21,706
  • 14
  • 92
  • 130
1

As of 19-Sep-22, with go version 1.19, instance of http.request URL has a method called Query, which will return a map, which is a parsed query string.

func helloHandler(res http.ResponseWriter, req *http.Request) {
    // when request URL is `http://localhost:3000/?first=hello&second=world`
    fmt.Println(req.URL.Query()) // outputs , map[second:[world] first:[hello]] 

    res.Write([]byte("Hello World Web"))
}
Harishm72
  • 11
  • 1
  • 2
-1

No way without standard library. Why you don't want to try some library? I think its not so hard to use it, just go get bla bla bla

I use Beego. Its MVC style.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Arief Hidayatulloh
  • 847
  • 1
  • 7
  • 4
  • Before I go searching for a library, I want to know if it can be done in golang. (Also, Stackoverflow bans questions asking about choosing libraries) – Jay Sep 13 '18 at 04:27
  • Please provide a sample solution if you are suggesting something. Your answer is not helpful to the readers. Provide a sample to question asked using `Beego` – Muhammad Tariq Jan 24 '21 at 17:37
-1

how about a simple utility function ?

func withURLParams(u url.URL, param, val string) url.URL{
    u.Path = strings.ReplaceAll(u.Path, param, val)
    return u
}

you can use it like this:

u, err := url.Parse("http://localhost:8080/blob/:id/test")
if err != nil {
    return nil, err
}
u := withURLParams(u, ":id","123")

// now u.String() is http://localhost:8080/blob/123/test
danfromisrael
  • 2,982
  • 3
  • 30
  • 40
  • 1
    This is the opposite of the question, the question is if the url is: `http://localhost:8080/blob/123/test` the appis required to read it and tell the `:id = 123` – Hasan A Yousef Jan 10 '21 at 18:20
-10

If you need a framework and you think it will be slow because it's 'bigger' than a router or net/http, then you 're wrong.

Iris is the fastest go web framework that you will ever find, so far according to all benchmarks.

Install by

  go get gopkg.in/kataras/iris.v6

Django templates goes easy with iris:

import (
    "gopkg.in/kataras/iris.v6"
    "gopkg.in/kataras/iris.v6/adaptors/httprouter"
    "gopkg.in/kataras/iris.v6/adaptors/view" // <-----

)

func main() {

    app := iris.New()
    app.Adapt(iris.DevLogger())
    app.Adapt(httprouter.New()) // you can choose gorillamux too
    app.Adapt(view.Django("./templates", ".html")) // <-----

    // RESOURCE: http://127.0.0.1:8080/hi
    // METHOD: "GET"
    app.Get("/hi", hi)

    app.Listen(":8080")
}

func hi(ctx *iris.Context){
   ctx.Render("hi.html", iris.Map{"Name": "iris"})
}
kataras
  • 835
  • 9
  • 14
  • 2
    You might want to read about iris before you use it, eg: https://www.reddit.com/r/golang/comments/57w79c/why_you_really_should_stop_using_iris/ – Alexander Köb Jan 28 '19 at 00:05