Go打造REST Server【一】:用标准库来实现

463 阅读5分钟

本文将采用go的原生组件来设计一个REST Server

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支持GETPOSTPUTDELETE请求,其中一些具有多个潜在路径。尖括号<...>之间的部分表示客户端作为请求的一部分提供的参数;例如,GET /page/42是获取ID42Page的请求,等等。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,但也可以使用数据库例如MySQLMongoDB实现。在实际应用程序中,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