3
\$\begingroup\$

I know Gorilla and others have packages that accomplish this, but I'm trying to learn Go by making stuff. In the router below, the first route / is for all http methods, the second route /hello is only for GET, and the third route hello/:name has one parameter for all HTTP methods.

func main() {
    http.HandleFunc("/", route)
    http.ListenAndServe(":8080", nil)
}

func match(re string, route string) bool {
    match, err := regexp.MatchString(re, route)
    if err != nil {
        return false
    }

    return match
}

func route(w http.ResponseWriter, r *http.Request) {
    route := r.URL.Path
    switch {
    case route == "/":
        home(w, r)
    case route == "/hello" && r.Method == "GET":
        hello(w, r)
    case match("^/hello/([^/]+)$", route) == true:
        var re = regexp.MustCompile("^/hello/([^/]+)$")
        match := re.FindStringSubmatch(route)

        helloName(w, r, match[1])
    default:
        notFound(w, r)
    }
}

// func home, hello, helloName, and notFound

How can I make this more efficient? Is it reliable?

\$\endgroup\$
2
\$\begingroup\$

Instead of case x == true you can use boolean expressions directly, and write simply:

case x:

The same regex is written twice here. What's worse is that the pattern matching is also evaluated twice:

case match("^/hello/([^/]+)$", route) == true:
    var re = regexp.MustCompile("^/hello/([^/]+)$")

I'm not sure if there's an elegant solution for this situation in Go. But at the minimum you could move the regex pattern into a variable to avoid typing it twice.

Another alternative is to create a matcher structure, with a match function that will not only match a pattern and return a boolean, but at the same time also store the sub-string you want to extract.

type matcher struct {
    value string
}

func (m *matcher) match(pattern string, route string) bool {
    var re = regexp.MustCompile(pattern)
    matches := re.FindStringSubmatch(route)
    if len(matches) < 2 {
        return false
    }

    m.value = matches[1]

    return true
}

With the help of this, you could rewrite the route function as:

func route(w http.ResponseWriter, r *http.Request) {
    route := r.URL.Path
    m := &matcher{}
    switch {
    case route == "/":
        home(w, r)
    case route == "/hello" && r.Method == "GET":
        hello(w, r)
    case m.match("^/hello/([^/]+)$", route):
        helloName(w, r, m.value)
    default:
        notFound(w, r)
    }
}
| improve this answer | |
\$\endgroup\$
1
\$\begingroup\$

I found that I can put all of the regexp.MustCompile's into the func init, change a few other things, and increase the speed by more than a factor of 50:

var routeRegex map[string]*regexp.Regexp

func init() {
    r := make(map[string]string)
    r["helloName"] = "^/hello/([a-zA-Z]+?)$"
    r["helloNameAge"] = "^/hello/([a-zA-Z]+?)/([0-9]+?)$"

    routeRegex = make(map[string]*regexp.Regexp)
    for k, v := range r {
        routeRegex[k] = regexp.MustCompile(v)
    }
}

func match(x string, r string) (bool, []string) {
    if m := routeRegex[x].FindStringSubmatch(r); m != nil {
        return true, m[1:]
    }

    return false, nil
}

func Route(w http.ResponseWriter, r *http.Request) {
    rt := r.URL.Path

    if rt == "/" {
        home(w, r)
    } else if rt == "/hello" && r.Method == "GET" {
        hello(w, r)
    } else if m, p := match("helloName", rt); m {
        helloName(w, r, p[0])
    } else if m, p := match("helloNameAge", rt); m {
        helloNameAge(w, r, p[0], p[1] )
    } else {
        notFound(w, r)
    }
}

I benchmarked func route with 1) no regex, 2) one regex match, 3) two regex matches, and 4) not matching route / not found.

new solution:

  1. 3.6 ns/op
  2. 279 ns/op
  3. 332 ns/op
  4. 212 ns/op

old solution:

  1. 3.33 ns/op
  2. 16237 ns/op
  3. 16479 ns/op
  4. 16543 ns/op
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.