Golang:使用 httprouter 构建 API 服务器
https://medium.com/@gauravsingharoy/build-your-first-api-server-with-httprouter-in-golang-732b7b01f6ab
作者:Gaurav Singha Roy
译者:oopsguy.com
10 个月前,我成为了一名 Gopher,没有后悔。像许多其他 gopher 一样,我很快发现简单的语言特性对于快速构建快速、可扩展的软件非常有用。当我刚开始学习 Go 时,我正在捣鼓不同的路由器,它可以当做 API 服务器使用。如果你跟我一样有 Rails 背景,你也可能会在构建 Web 框架提供的所有功能方面遇到困难。回到路由器话题,我发现了 3 个是非常有用的好东西: Gorilla mux、httprouter 和 bone(按性能从低到高)。即便 bone 的性能最高且有更简单的 handler 签名,但对我来说,它仍然不够成熟,无法应用于生产环境中。因此,我最终使用了 httprouter。在本教程中,我将使用 httprouter 构建一个简单的 REST API 服务器。
如果你想偷懒,只想获取源码,则可以在这里[4]直接克隆我的 github 仓库。
让我们开始吧。首先创建一个基本端点:
package main
import (
"fmt"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func main() {
router := httprouter.New()
router.GET("/", Index)
log.Fatal(http.ListenAndServe(":8080", router))
}
在上面的代码段中,Index 是一个 handler 函数,需要传入三个参数。之后,该 handler 将在 main 函数中被注册到 GET / 路径上。现在编译并运行你的程序,转到 http:// localhost:8080 来查看你的 API 服务器。点击这里[1]获取当前代码。
我们可以让 API 变得复杂一点。现在有一个名为 Book 的实体,可以把 ISDN 字段作为唯一标识。让我们创建更多的 handler,即分表代表着 Index 和 Show 动作的 GET /books 和 GET /books/:isdn。main.go 文件此时如下:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
type Book struct {
// The main identifier for the Book. This will be unique.
ISDN string `json:"isdn"`
Title string `json:"title"`
Author string `json:"author"`
Pages int `json:"pages"`
}
type JsonResponse struct {
// Reserved field to add some meta information to the API response
Meta interface{} `json:"meta"`
Data interface{} `json:"data"`
}
type JsonErrorResponse struct {
Error *ApiError `json:"error"`
}
type ApiError struct {
Status int16 `json:"status"`
Title string `json:"title"`
}
// A map to store the books with the ISDN as the key
// This acts as the storage in lieu of an actual database
var bookstore = make(map[string]*Book)
// Handler for the books index action
// GET /books
func BookIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
books := []*Book{}
for _, book := range bookstore {
books = append(books, book)
}
response := &JsonResponse{Data: &books}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
panic(err)
}
}
// Handler for the books Show action
// GET /books/:isdn
func BookShow(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
isdn := params.ByName("isdn")
book, ok := bookstore[isdn]
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
if !ok {
// No book with the isdn in the url has been found
w.WriteHeader(http.StatusNotFound)
response := JsonErrorResponse{Error: &ApiError{Status: 404, Title: "Record Not Found"}}
if err := json.NewEncoder(w).Encode(response); err != nil {
panic(err)
}
}
response := JsonResponse{Data: book}
if err := json.NewEncoder(w).Encode(response); err != nil {
panic(err)
}
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/books", BookIndex)
router.GET("/books/:isdn", BookShow)
// Create a couple of sample Book entries
bookstore["123"] = &Book{
ISDN: "123",
Title: "Silence of the Lambs",
Author: "Thomas Harris",
Pages: 367,
}
bookstore["124"] = &Book{
ISDN: "124",
Title: "To Kill a Mocking Bird",
Author: "Harper Lee",
Pages: 320,
}
log.Fatal(http.ListenAndServe(":8080", router))
}
你如果现在尝试请求 GET https:// localhost:8080/books,将得到以下响应:
{
"meta": null,
"data": [
{
"isdn": "123",
"title": "Silence of the Lambs",
"author": "Thomas Harris",
"pages": 367
},
{
"isdn": "124",
"title": "To Kill a Mocking Bird",
"author": "Harper Lee",
"pages": 320
}
]
}
我们在 main 函数中硬编码了这两个 book 实体。点击这里[2]获取当前代码。
让我们来重构一下代码。到目前为止,所有的代码都放置在同一个文件中:main.go。我们可以把它们划分移到不同的文件中。此时我们有一个目录:
.
├── handlers.go
├── main.go
├── models.go
└── responses.go
我们把所有与 JSON 响应相关的结构体移动到 responses.go,将 handler 函数移动到 Handlers.go,将 Book 结构体移动到 models.go。点击这里[3]查看当前代码。现在,我们跳过来写一些测试。在 Go 中,*_test.go 文件是用作测试用途。因此让我们创建一个 handlers_test.go。
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/julienschmidt/httprouter"
)
func TestBookIndex(t *testing.T) {
// Create an entry of the book to the bookstore map
testBook := &Book{
ISDN: "111",
Title: "test title",
Author: "test author",
Pages: 42,
}
bookstore["111"] = testBook
// A request with an existing isdn
req1, err := http.NewRequest("GET", "/books", nil)
if err != nil {
t.Fatal(err)
}
rr1 := newRequestRecorder(req1, "GET", "/books", BookIndex)
if rr1.Code != 200 {
t.Error("Expected response code to be 200")
}
// expected response
er1 := "{\"meta\":null,\"data\":[{\"isdn\":\"111\",\"title\":\"test title\",\"author\":\"test author\",\"pages\":42}]}\n"
if rr1.Body.String() != er1 {
t.Error("Response body does not match")
}
}
// Mocks a handler and returns a httptest.ResponseRecorder
func newRequestRecorder(req *http.Request, method string, strPath string, fnHandler func(w http.ResponseWriter, r *http.Request, param httprouter.Params)) *httptest.ResponseRecorder {
router := httprouter.New()
router.Handle(method, strPath, fnHandler)
// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
rr := httptest.NewRecorder()
// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
// directly and pass in our Request and ResponseRecorder.
router.ServeHTTP(rr, req)
return rr
}
我们使用 httptest 包的 Recorder 来 mock handler。同样,你也可以为 handler BookShow 编写测试用例。
让我们稍微做些重构。我们仍然把所有路由都定义在了 main 函数中,handler 看起来有点臃肿,需要做点 DRY。我们仍然在终端中输出一些日志消息,并且可以添加一个 BookCreate handler 来创建一个新的 Book。
首先先解决 handlers.go。
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
// Handler for the books Create action
// POST /books
func BookCreate(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
book := &Book{}
if err := populateModelFromHandler(w, r, params, book); err != nil {
writeErrorResponse(w, http.StatusUnprocessableEntity, "Unprocessible Entity")
return
}
bookstore[book.ISDN] = book
writeOKResponse(w, book)
}
// Handler for the books index action
// GET /books
func BookIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
books := []*Book{}
for _, book := range bookstore {
books = append(books, book)
}
writeOKResponse(w, books)
}
// Handler for the books Show action
// GET /books/:isdn
func BookShow(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
isdn := params.ByName("isdn")
book, ok := bookstore[isdn]
if !ok {
// No book with the isdn in the url has been found
writeErrorResponse(w, http.StatusNotFound, "Record Not Found")
return
}
writeOKResponse(w, book)
}
// Writes the response as a standard JSON response with StatusOK
func writeOKResponse(w http.ResponseWriter, m interface{}) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(&JsonResponse{Data: m}); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "Internal Server Error")
}
}
// Writes the error response as a Standard API JSON response with a response code
func writeErrorResponse(w http.ResponseWriter, errorCode int, errorMsg string) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(errorCode)
json.
NewEncoder(w).
Encode(&JsonErrorResponse{Error: &ApiError{Status: errorCode, Title: errorMsg}})
}
//Populates a model from the params in the Handler
func populateModelFromHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params, model interface{}) error {
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
if err != nil {
return err
}
if err := r.Body.Close(); err != nil {
return err
}
if err := json.Unmarshal(body, model); err != nil {
return err
}
return nil
}
我创建了两个函数,writeOKResponse 用于将 StatusOK 写入响应,其返回一个 model 或一个 model slice,writeErrorResponse 将在发生预期或意外错误时将 JSON 错误作为响应。像任何一个优秀的 gopher 一样,我们不应该 panic。我还添加了一个名为 populateModelFromHandler 的函数,它将内容从 body 中解析成所需的 model(struct)。在这种情况下,我们在 BookCreate handler 中使用它来填充一个 Book。
现在来看看日志。我们简单地创建一个 Logger 函数,它包装了 handler 函数,并在执行 handler 函数之前和之后打印日志消息。
package main
import (
"log"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
)
// A Logger function which simply wraps the handler function around some log messages
func Logger(fn func(w http.ResponseWriter, r *http.Request, param httprouter.Params)) func(w http.ResponseWriter, r *http.Request, param httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, param httprouter.Params) {
start := time.Now()
log.Printf("%s %s", r.Method, r.URL.Path)
fn(w, r, param)
log.Printf("Done in %v (%s %s)", time.Since(start), r.Method, r.URL.Path)
}
}
之后来看看路由。首先,在统一一个地方定义所有路由,比如 routes.go。
package main
import "github.com/julienschmidt/httprouter"
/*
Define all the routes here.
A new Route entry passed to the routes slice will be automatically
translated to a handler with the NewRouter() function
*/
type Route struct {
Name string
Method string
Path string
HandlerFunc httprouter.Handle
}
type Routes []Route
func AllRoutes() Routes {
routes := Routes{
Route{"Index", "GET", "/", Index},
Route{"BookIndex", "GET", "/books", BookIndex},
Route{"Bookshow", "GET", "/books/:isdn", BookShow},
Route{"Bookshow", "POST", "/books", BookCreate},
}
return routes
}
让我们创建了一个 NewRouter 函数,它可以在 main 函数中调用,它读取上面定义的所有路由,并返回一个可用的 httprouter.Router。因此创建一个文件 router.go。我们还将使用新创建的 Logger 函数来包装 handler。
package main
import "github.com/julienschmidt/httprouter"
//Reads from the routes slice to translate the values to httprouter.Handle
func NewRouter(routes Routes) *httprouter.Router {
router := httprouter.New()
for _, route := range routes {
var handle httprouter.Handle
handle = route.HandlerFunc
handle = Logger(handle)
router.Handle(route.Method, route.Path, handle)
}
return router
}
你的目录此时应该像这样:
.
├── handlers.go
├── handlers_test.go
├── logger.go
├── main.go
├── models.go
├── responses.go
├── router.go
└── routes.go
在这里[4]查看完整代码。
到这里,这些代码应该可以帮助你开始编写自己的 API 服务器了。当然,你需要把你的功能放在不同的包中,以下目录结构是一个参照:
.
├── LICENSE
├── README.md
├── handlers
│ ├── books_test.go
│ └── books.go
├── models
│ ├── book.go
│ └── *
├── store
│ ├── *
└── lib
| ├── *
├── main.go
├── router.go
├── rotes.go
如果你有一个大的单体服务,你还可以将 handlers、models 和所有路由功能都放在另一个名为 app 的包中。你只要记住,go 不像 Java 或 Scala 那样可以有循环的包引用。因此你必须格外注意你的包结构。
这就是全部内容,希望本教程能帮助到你。
Golang:使用 httprouter 构建 API 服务器的更多相关文章
- webpack构建本地服务器
webpack构建本地服务器 想不想让你的浏览器监测你的代码的修改,并自动刷新修改后的结果,其实Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建, 可以实现你想要的这些 ...
- 使用Swoole 构建API接口服务
网上类似的文章已经很多了,我也是刚入门.从头开始学习.所以如果重复写文章阐释,反而会浪费时间,于是就自己动手构建了一个demo,使用swoole 的TCP 服务器接受TCP客户端的发来的http请求, ...
- 5分钟Serverless实践:构建无服务器的图片分类系统
前言 在过去“5分钟Serverless实践”系列文章中,我们介绍了如何构建无服务器API和Web应用,从本质上来说,它们都属于基于APIG触发器对外提供一个无服务器API的场景.现在本文将介绍一种新 ...
- 5分钟Serverless实践 | 构建无服务器的敏感词过滤后端系统
前言 在上一篇“5分钟Serverless实践”系列文章中,我们介绍了什么是Serverless,以及如何构建一个无服务器的图片鉴黄Web应用,本文将延续这个话题,以敏感词过滤为例,介绍如何构建一个无 ...
- 5分钟Serverless实践 | 构建无服务器图片鉴黄Web应用
Serverless是什么 Serverless中文译为“无服务器”,最早可以追溯到2012年Ken Fromm发表的<Why The Future Of Software And Apps I ...
- 使用Jersey构建图片服务器
使用Jersey构建图片服务器 前台页面代码 <form id="jvForm" action="add.do" method="post&qu ...
- springboot利用swagger构建api文档
前言 Swagger 是一款RESTFUL接口的文档在线自动生成+功能测试功能软件.本文简单介绍了在项目中集成swagger的方法和一些常见问题.如果想深入分析项目源码,了解更多内容,见参考资料. S ...
- 数据库服务概述,构建MYSQL服务器,数据库基本管理,mysql数据类型,表结构的调整
数据库的发展前引 MySQL的起源与发展过程 最为著名.应用最广泛的开源数据库软件 最早 ...
- 构建 API 的7个建议【翻译】
迄今为止,越来越多的企业依靠API来为客户提供服务,以确保竞争的优势和业务可见性.出现这个情况的原因是微服务和无服务器架构正变得越来越普遍,API作为其中的关键节点,继承和承载了更多业务. 在这个前提 ...
随机推荐
- Swing-setOpaque()用法-入门
先看API: public void setOpaque(boolean isOpaque) 如果为 true,则该组件绘制其边界内的所有像素.否则该组件可能不绘制部分或所有像素,从而允许其底层像素透 ...
- 201521123044 《Java程序设计》第5周学习总结
1. 本章学习总结 2. 书面作业 1. 代码阅读:Child压缩包内源代码 1.1 com.parent包中Child.java文件能否编译通过?哪句会出现错误?试改正该错误.并分析输出结果. 答: ...
- 201521123063 《Java程序设计》第三周学习总结
1.本周学习总结 初学面向对象,会学习到很多碎片化的概念与知识.尝试学会使用思维导图将这些碎片化的概念.知识组织起来.请使用纸笔或者下面的工具画出本周学习到的知识点.截图或者拍照上传. 2.书面作业 ...
- 201521123088《Java程序》第二周总结
#1. 本章学习总结 ①java基本数据类型 ②String类对象使用 #2. 书面作业 使用Eclipse关联jdk源代码,并查看String对象的源代码(截图)?分析String使用什么来存储字符 ...
- 201521123108《Java程序设计》第1周学习总结
本周学习总结 刚加入java的学习,我感觉很困难.但我觉得学习并不是一蹴而就.只要我多读,多看,多练,一定会取得进步.有空的时候把书本上的代码敲一敲.相信会有回报. 2.书面作业 1.为什么java程 ...
- PIC单片机状态寄存器中的C(进位/借位位标志)
查阅PIC单片机芯片手册,关于进位/借位位的说明为: C:进位/借位位. 1 = 结果的最高位发生了进位 0 = 结果的最高位未发生进位 同时有一条标注:借位的极性是相反的. 通过以上说明,可以将C的 ...
- Selenium+Python自动化测试实战(2)元素定位
1.Selenium2 的原理 第一篇分享简单介绍了基于Python开发的Selenium2的环境配置,这篇主要讲一下基本用法.首先讲一下Selenium2的基本原理.基本上知道了这个东西是怎么回事, ...
- WPA/WPA2加密破解
WPA/WPA2无线密码破解这里主要介绍两种方法:穷举PIN码.穷举密码 穷举PIN码(共11000种组合)破解速度快,需要一些条件(路由器支持WPS.QSS功能),路由器信号良好.无论密码多复杂,条 ...
- Linux入门之常用命令(12) mount
查看Linux所有设备 cd /dev ls -l sd* //分区 查看Linux磁盘 fdisk -lu 挂载 一般挂载至/mnt /media mount /dev/sda5 /mnt/ ...
- 【转】常用Maven插件
我们都知道Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven- compiler-plugin完成的.进一步说,每个任务对应 ...