本文将采用go的原生组件来设计一个REST Server
- Go打造REST Server【前言】:什么是REST
- Go打造REST Server【一】:用标准库来实现
- Go打造REST Server【二】:用路由的三方库来实现
- Go打造REST Server【三】:用Web框架来实现
- Go打造REST Server【四】:Graphql进阶
Page
首先,我们定义Page
概念,在这种情况下,服务器是Page
管理应用程序的简单后端;它向客户端提供以下REST API
:
POST /page/ : create a page, returns ID
GET /page/<pageid> : returns a single page by ID
GET /page/ : returns all pages
DELETE /page/<pageid> : delete a page by ID
PUT /page/<pageid> : update a page by ID
GET /tag/<tagname> : returns list of pages with this tag
GET /due/<yy>/<mm>/<dd> : returns list of pages due by this date
我们的REST Server
支持GET
、POST
、PUT
和DELETE
请求,其中一些具有多个潜在路径。尖括号<...>
之间的部分表示客户端作为请求的一部分提供的参数;例如,GET /page/42
是获取ID
为42
的Page
的请求,等等。Page
由ID唯一标识。
数据编码为JSON
。在POST /page/
中,客户端将发送要创建的任务的JSON
表示。类似地,每当服务器返回某些东西时,返回的数据都会在HTTP
响应的正文中编码为JSON
。
模型
让我们从讨论我们的服务器的模型(或“数据层”)开始,Store
包(项目目录中的 page/store
),这是一个表示任务数据库的简单抽象。这是它的API
:
func New() *Store
// CreatePage creates a new page in the store.
func (s *Store) CreatePage(text string, tags []string, due time.Time) int
// DeletePage deletes the page with the given id. If no such id exists, an error
// is returned.
func (s *Store) DeletePage(id int) error
// UpdatePage updates the page with the given id and new page. If no such id
// exists, an error is returned, else page with given id should be updated.
func (s *Store) UpdatePage(page *Page) (*Page, error)
// GetPage retrieves a page from the store, by id. If no such id exists, an
// error is returned.
func (s *Store) GetPage(id int) (*Page, error)
// GetAllPages returns all the pages in the store, in arbitrary order.
func (s *Store) GetAllPages() []*Page
// DeleteAllPages deletes all pages in the store.
func (s *Store) DeleteAllPages() error
// GetPagesByTag returns all the pages that have the given tag, in arbitrary
// order.
func (s *Store) GetPagesByTag(tag string) []*Page
// GetPagesByDueDate returns all the pages that have the given due date, in
// arbitrary order.
func (s *Store) GetPagesByDueDate(year int, month time.Month, day int) []*Page
Page
结构体的定义:
type Page struct {
Id int `json:"id"`
Text string `json:"text"`
Tags []string `json:"tags"`
Due time.Time `json:"due"`
}
store
包使用简单的map[int]Page
实现了这个API
,但也可以使用数据库例如MySQL
、MongoDB
实现。在实际应用程序中,Store
可能是多个后端可以实现的接口,但对于我们的简单示例,当前的API
就足够了。
设置服务器
服务器的main()
相当简单:
func main() {
mux := http.NewServeMux()
server := NewPageServer()
mux.HandleFunc("/page/", server.pageHandler)
mux.HandleFunc("/tag/", server.tagHandler)
mux.HandleFunc("/due/", server.dueHandler)
log.Fatal(http.ListenAndServe("localhost:8880", mux))
}
NewPageServer
是我们的服务器类型PageServer
的构造函数。服务器包装了一个Store
,它对并发访问是安全的。
type PageServer struct {
store *page.Store
}
func NewPageServer() *PageServer {
return &PageServer{store: page.New()}
}
路由和处理程序
回到路由,这里使用net/http
包中包含的标准HTTP
多路复用器:
mux.HandleFunc("/page/", server.pageHandler)
mux.HandleFunc("/tag/", server.tagHandler)
mux.HandleFunc("/due/", server.dueHandler)
标准多路复用器非常简单;这既是它的强项,也是弱点。优势因为它非常容易理解;弱点是因为它有时会使路径匹配变得相当乏味,并且会在多个地方分裂导致重复。
由于标准多路复用器只支持路径前缀的精确匹配,我们几乎被迫只匹配顶层的根,并将更详细的匹配放到处理程序中。
func (p *PageServer) pageHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("handling page at %s\n", r.URL.Path)
if r.URL.Path == "/page/" {
// response for url "/page/"
if r.Method == http.MethodPost {
p.createPageHandler(w, r)
} else if r.Method == http.MethodPut {
p.updatePageHandler(w, r)
} else if r.Method == http.MethodGet {
p.getAllPagesHandler(w, r)
} else if r.Method == http.MethodDelete {
p.deleteAllPagesHandler(w, r)
} else {
http.Error(w, fmt.Sprintf("expect method GET, DELETE, PUT or POST at /page/, got %v", r.Method), http.StatusMethodNotAllowed)
return
}
}
}
我们从/page/
的路径的精确匹配开始(意味着没有<pageid>
跟随)。在这里,我们必须弄清楚使用哪种HTTP
方法,并调用相应的服务器方法。大多数处理程序都是Store API
的相当简单的包装器,例如:
func (p *PageServer) getAllPagesHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("handling get all pages at %s\n", r.URL.Path)
ret := p.store.GetAllPages()
js, err := json.Marshal(ret)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
}
这个处理程序有两个主要作用:
- 从模型中获取数据(
Store
) - 返回客户端一个
HTTP
响应 两者都很简单,但是如果检查服务器中的其他处理程序,我们会注意到第二个处理程序有点重复,编组JSON
、编写正确的HTTP
响应标头等,我们稍后再讨论。
现在回到pageHandler
,到目前为止,我们已经看到了它如何处理/page/
路径的直接匹配。/page/<pageid>
呢?这就是函数的下一部分出现的地方:
else {
// response for url "/page/<id>"
path := strings.Trim(r.URL.Path, "/")
pathParts := strings.Split(path, "/")
// if the url is otherwise
if len(pathParts) < 2 {
http.Error(w, "expect /page/<id> in page handler", http.StatusBadRequest)
return
}
// parse id from url
id, err := strconv.Atoi(pathParts[1])
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if r.Method == http.MethodDelete {
p.deletePageHandler(w, r, id)
} else if r.Method == http.MethodGet {
p.getPageHandler(w, r, id)
} else {
http.Error(w, fmt.Sprintf("expect method GET or DELETE at /page/<id>, got %v", r.Method), http.StatusMethodNotAllowed)
return
}
}
当路径与/page/
不完全匹配时,我们希望斜杠后面有一个数字ID
。上面的代码解析这个数字ID
并调用适当的处理程序(基于HTTP
方法)。
其余代码基本相同,应该很容易理解。唯一有点特殊的处理程序是createPageHandler
,因为它必须解析客户端在请求正文中发送的JSON
数据。
优化
我们将之前提到的重复处理JSON的部分提取成一个公共函数:
// renderJSON renders 'v' as JSON and writes it as a response into w.
func renderJSON(w http.ResponseWriter, v interface{}) {
js, err := json.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
}
这样就是在每个需要的地方进行调用了。
代码
本节源码见Github