REST(Representational State Transfer),表述性状态传递,是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。
REST是一种软件架构风格,指的是一组架构约束条件和原则,而REST API则是满足这种软件架构风格的API架构。
设计一个专用于REST API高性能路由库
route是我参照母所设计的一个用于REST API的路由库,参照了mux路由库。
该路由库支持Http方法的四种基本操作:Get、Post、Put和Delete,支持自定义正则表达式;同时路由库也可以为静态文件创建路由,为文件提供/static
;该库还实现了过滤器功能,可以过滤所有请求或仅在需要的REST URL参数存在时应用过滤器。
此路由库仅有routes.go一个主要文件,设计的所有函数都在此文件中。主要内容如下:
route
结构体:此结构体是路由的基本内容,包括方法名字,正则表达式内容,参数和handler对象;Get
函数:创建一个用于GET请求的路由;Put
函数:创建一个用于PUT请求的路由;Post
函数:创建一个用于POST请求的路由;Del
函数:创建一个用于DELETE请求的路由;Static
函数:为静态http请求创建一个新的路由,可以提供指定目录中的静态文件;Filter
函数:添加一个过滤器强制过滤所有请求;FilterParam
函数:先判断是否有URL参数,如果有URL参数,则添加一个过滤器过滤请求;AddRoute
函数:为handler对象添加路由,并解析url的内容,支持正则表达式。ServeHTTP
函数:提供http服务,对所有路由进行处理。Write
, Header
, WriteHeader
函数:设置responseWriter
结构体的具体参数,用于跟踪http响应是否被写入。本来想要对Get,Post,Put和Delete这四个函数都设置对应的测试的,但是写不来,mux库中也没有找到可以借鉴的,最后没有对这四个基本函数进行测试,而是测试了路由的连接情况以及过滤器的过滤功能。
路由正常连接的代码为200.
var HandlerOk = func(w http.ResponseWriter, r *http.Request) {
//fmt.Fprintf(w, "hello world")
w.WriteHeader(http.StatusOK)
}
func TestRouteOk(t *testing.T) {
r, _ := http.NewRequest("GET", "/student/three/LiMing?learn=Chinese", nil)
w := httptest.NewRecorder()
handler := new(RouteMux)
handler.Get("/student/:grade/:name", HandlerOk)
handler.ServeHTTP(w, r)
params := r.URL.Query()
gradeParam := params.Get(":grade")
nameParam := params.Get(":name")
learnParam := params.Get("learn")
if gradeParam != "three" {
t.Errorf("url param set to [%s]; want [%s]", gradeParam, "three")
}
if nameParam != "LiMing" {
t.Errorf("url param set to [%s]; want [%s]", nameParam, "LiMing")
}
if learnParam != "Chinese" {
t.Errorf("url param set to [%s]; want [%s]", learnParam, "Chinese")
}
}
如果没有与请求的url匹配的路由,则会返回代码404.
func TestNotFound(t *testing.T) {
r, _ := http.NewRequest("GET", "/teacher/two/LiuYang?teach=English", nil)
w := httptest.NewRecorder()
handler := new(RouteMux)
handler.Get("/student/:grade/:name", HandlerOk)
handler.ServeHTTP(w, r)
if w.Code != http.StatusNotFound {
t.Errorf("Code set to [%v]; want [%v]", w.Code, http.StatusNotFound)
}
}
此测试能测试route路由库是否能提供静态路由连接,如果得到的路径结果错误的话,则说明静态路由连接失败,无法提供文件。
func TestStatic(t *testing.T) {
r, _ := http.NewRequest("GET", "/routes_test.go", nil)
w := httptest.NewRecorder()
pwd, _ := os.Getwd()
handler := new(RouteMux)
handler.Static("/", pwd)
handler.ServeHTTP(w, r)
testFile, _ := ioutil.ReadFile(pwd + "/routes_test.go")
if w.Body.String() != string(testFile) {
t.Errorf("handler.Static failed to serve file")
}
}
此单元测试目的在于测试函数Filter()是否能够正常运行,若能够正常运行的话,会强制过滤掉所有路由。
var Filter = func(w http.ResponseWriter, r *http.Request) {
if r.URL.User == nil || r.URL.User.Username() != "admin" {
http.Error(w, "", http.StatusUnauthorized)
}
}
func TestFilter(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler := new(RouteMux)
handler.Get("/", HandlerOk)
handler.Filter(FilterUser)
handler.ServeHTTP(w, r)
if w.Code != http.StatusUnauthorized {
t.Errorf("Did not apply Filter. Code set to [%v]; want [%v]", w.Code, http.StatusUnauthorized)
}
r, _ = http.NewRequest("GET", "/", nil)
r.URL.User = url.User("admin")
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Code set to [%v]; want [%v]", w.Code, http.StatusOK)
}
}
此单元测试旨在测试函数FilterParam()是否能够过滤掉url中指定参数的所有路由。
var FilterUser = func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get(":user")
if id == "admin" {
http.Error(w, "", http.StatusUnauthorized)
}
}
func TestFilterParam(t *testing.T){
r, _ := http.NewRequest("GET", "/:user", nil)
w := httptest.NewRecorder()
handler := new(RouteMux)
handler.Get("/:user", HandlerOk)
handler.FilterParam(":user", FilterUser)
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Did not apply Filter. Code set to [%v]; want [%v]", w.Code, http.StatusOK)
}
r, _ = http.NewRequest("GET", "/admin", nil)
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusUnauthorized {
t.Errorf("Did not apply Param Filter. Code set to [%v]; want [%v]", w.Code, http.StatusUnauthorized)
}
}
基准测试包括两个:BenchmarkRosteredHandler
和BenchmarkServeMux
,分别测试路由库中的RoutedMux
以及http/serve.go
文件中的ServeMux
的性能并加以比较。
func BenchmarkRoutedHandler(b *testing.B) {
handler := new(RouteMux)
handler.Get("/", HandlerOk)
for i := 0; i < b.N; i++ {
r, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
}
}
func BenchmarkServeMux(b *testing.B) {
r, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
mux := http.NewServeMux()
mux.HandleFunc("/", HandlerOk)
for i := 0; i < b.N; i++ {
mux.ServeHTTP(w, r)
}
}
测试结果:
可以发现,此route库对性能的影响还是很大的,相比http本身要慢了十多倍,每个操作平均需要一千多纳秒。 (不知道还算不算得上高性能路由库?)
覆盖率测试主要测试的是测试代码的代码覆盖率。
可以看到代码覆盖率有84.1%。
然后在命令行依次输入命令:
go test -coverprofile=cover.out
go tool cover -html=cover.out -o coverage.html
可以生成cover.out文件并根据cover.out文件生成coverage.html文件。查看html格式的代码覆盖率报告。
打开coverage.html文件,可以查看测试代码具体覆盖了哪些代码,主要就是Put、Post和Del这三个函数没有覆盖到。
由于在单元测试时没有涉及到Post, Put和Del这几个函数,所以我专门为这几个函数进行了功能测试,具体内容在main.go文件中。
package main
import (
"fmt"
"gitee.com/li-haowei/route"
"net/http"
)
func getStudent(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
grade := params.Get(":grade")
name := params.Get(":name")
fmt.Fprintf(w, "You get the Grade %s student's message, his name is %s.\n", grade, name)
}
func modifyStudent(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
grade := params.Get(":grade")
name := params.Get(":name")
fmt.Fprintf(w, "You post the Grade %s student's message, his name is %s.\n", grade, name)
}
func addStudent(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
grade := params.Get(":grade")
name := params.Get(":name")
fmt.Fprintf(w, "You put a new student message. The student named %s is in Grade %s\n", name, grade)
}
func deleteStudent(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
grade := params.Get(":grade")
name := params.Get(":name")
fmt.Fprintf(w, "You delete the Grade %s student's message, his name is %s.\n", grade, name)
}
func main() {
mux := route.New()
mux.Get("/:grade/:name", getStudent)
mux.Post("/:grade/:name", modifyStudent)
mux.Put("/:grade/:name", addStudent)
mux.Del("/:grade/:name", deleteStudent)
http.Handle("/", mux)
http.ListenAndServe(":8088", nil)
}
在main.go所在目录下运行go run main.go
,可以开启http的监听和服务,然后另开一个终端进行测试。
命令:
curl -X GET http://localhost:8088/three/LiMing
命令:
curl -X POST http://localhost:8088/four/LiuYang
命令:
curl -X PUT http://localhost:8088/five/WangWei
命令:
curl -X DELETE http://localhost:8088/six/ZhangFang
结果:
【传送门】