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?
2 Answers 2
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)
}
}
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:
- 3.6 ns/op
- 279 ns/op
- 332 ns/op
- 212 ns/op
old solution:
- 3.33 ns/op
- 16237 ns/op
- 16479 ns/op
- 16543 ns/op