代码仅用于介绍 nex 包的用法, 存在多处不严谨
越来越多的 WEB 应用采用前后端完全分离, WEB 前端使用 Angular/Vue 之类的库, 用 JSON 通过 RESTful 与服务器通讯, 同样的服务器接口不但可以用于 WEB 前端, 同时能用于动应用前端.
这个例子通过一个失物招领的信息管理系统来介绍如何使用nex包快速 构建 JSON API 服务, 限于作者水平有限, 如果发现错误, 欢迎指正.
名字Yue
来源于典故拾金不昧
中的主人公, 穷秀才何岳
两次将捡到的金子物归原主, 正好和这里例子失物招领
系统名字吻合. 这个系统就是个基本的 CRUD 系统, 主要操作Clue(线索)
, 对Clue
的增删改查, 希望可以通过这个
简单的例子, 能让读者明白nex
如何使用, 为了保存示例简单, 减少依赖, 数据没使用数据库, 直接使用一个数组来
存储所有的Clue
信息.
完整代码: https://github.com/chrislonng/yue
需要下载依赖
go get github.com/gorilla/mux
go get github.com/chrislonng/nex
这个代码不包括 WEB 前端, 只有后端的 JSON API 服务, 读者可以使用 PostMan 测试, Repo 中包含 PostMan 的配置 可以导入 PostMan. 如果之前没有使用过 PostMan 的同学, 可以从 https://www.getpostman.com/获取.
提供 JSON API 服务的 RESTful 接口, 使用了mux.Router
进行多路复用
r.Handle("/clues", nex.Handler(createClue)).Methods("POST") //创建
r.Handle("/clues", nex.Handler(clueList)).Methods("GET") //获取列表
r.Handle("/clues/{id}", nex.Handler(clueInfo)).Methods("GET") //获取 Clue 信息
r.Handle("/clues/{id}", nex.Handler(updateClue)).Methods("PUT") //更新
r.Handle("/clues/{id}", nex.Handler(deleteClue)).Methods("DELETE") //删除
r.Handle("/blob", nex.Handler(uploadFile)).Methods("POST") //上传
func createClue(c *ClueInfo) (*StringMessage, error) {
title := strings.TrimSpace(c.Title)
number := strings.TrimSpace(c.Number)
if title == "" || number == "" {
return nil, errors.New("title and number can not empty")
}
db.clues = append(db.clues, *c)
return SuccessResponse, nil
}
上面的代码片段中, 返回参数必须是两个, 一个是正常逻辑返回到客户端的数据, 另一个是发生错误时, 返回给客户端的
数据, nex
提供了一个对error
的默认的 Encode 函数, 后面会介绍如何自定义编码函数, c *ClueInfo
是将客
户端数据反序列化后的结构体
func clueList(query nex.Form) (*ClueListResponse, error) {
s := query.Get("start")
c := query.Get("count")
var start, count int
var err error
if s == "" {
start = 0
} else {
start, err = strconv.Atoi(s)
if err != nil {
return nil, err
}
}
if c == "" {
count = len(db.clues)
} else {
count, err = strconv.Atoi(c)
if err != nil {
return nil, err
}
}
return &ClueListResponse{Data: db.clues[start : start+count]}, nil
}
在参数列表中使用nex.Form
或者*nex.Form
, 可以自动获取查询参数, 具体用法和原生http.Request
中的Form
一样, 同时也可以使用nex.PostForm
或者*nex.PostForm
, 获取Post
参数, 是对http.Request
中的PostForm
的封装
Request
func clueInfo(r *http.Request) (*ClueInfoResponse, error) {
id, err := parseID(r)
if err != nil {
return nil, err
}
return &ClueInfoResponse{Data:&db.clues[id-1]}, nil
}
可以在函数签名中直接使用*http.Request
获取原始的 Request
func updateClue(r *http.Request, c *ClueInfo) (*StringMessage, error) {
id, err := parseID(r)
if err != nil {
return nil, err
}
title := strings.TrimSpace(c.Title)
number := strings.TrimSpace(c.Number)
if title == "" || number == "" {
return nil, errors.New("title and number can not empty")
}
db.clues[id] = *c
return SuccessResponse, nil
}
所有nex
支持的类型, 都可以在函数签名中使用, 没有顺序要求, nex
支持的类型, 详见nex
http.Header
func deleteClue(h http.Header, r *http.Request) (*StringMessage, error) {
t := h.Get("Authorization")
if t != token {
return nil, errors.New("permission denied")
}
id, err := parseID(r)
if err != nil {
return nil, err
}
db.clues = append(db.clues[:id], db.clues[id:]...)
return SuccessResponse, nil
}
这里为了演示如何在nex
的函数中使用http.Header
, 在删除Clue
是需要客户端在Header
中加入Authorization
字段
值等于服务器的token
时, 才能删除, 这里仅用于严实才这样写的
func uploadFile(form *multipart.Form) (*BlobResponse, error) {
uploaded, ok := form.File["uploadfile"]
if !ok {
return nil, errors.New("can not found `uploadfile` field")
}
localName := func(filename string) string {
ext := filepath.Ext(filename)
id := time.Now().Format("20060102150405.999999999")
return id + ext
}
var fds []io.Closer
defer func() {
for _, fd := range fds {
fd.Close()
}
}()
files := make(map[string]string)
for _, fh := range uploaded {
fileName := localName(fh.Filename)
files[fh.Filename] = fileName
// upload file
uf, err := fh.Open()
fds = append(fds, uf)
if err != nil {
return nil, err
}
// local file
lf, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, 0660)
fds = append(fds, lf)
if err != nil {
return nil, err
}
_, err = io.Copy(lf, uf)
if err != nil {
return nil, err
}
}
return &BlobResponse{Data: files}, nil
}
使用*multipart.Form
类型, 可以获取http.Request
中的MultipartForm
字段, 这里上传的文件, 简单的存在本地
并返回文件在服务器的文件名
nex.SetErrorEncoder(func(err error) interface{} {
return &ErrorMessage{
Code: -1000,
Error: err.Error(),
}
})
上面的代码通过自定义错误编码函数, 将所有的错误信息Code
设为-1000, 实际开发中可能会根据不同的错误生成不同的错误码,
以及返回相应的错误信息, 包括过滤一部分服务器的敏感信息, 通常可以在开发过程中, 通过 golang 的+build
来设置不同的tags
最终在release
和develop
版本包含不同级别的错误信息.
nex
主要用于将一个符合nex
签名的函数转换成符合http.Handler
接口的结构, 并在请求到达时, 自动进行依赖注入,
相对于HandleFunc
更加便于写单元测试, 并且减少在各个接口中序列化反序列化中的大量冗余代码, 我在使用go-kit
的过程中就存在这个问题.
相关功能逻辑单元如果需要新的依赖, 只需要在函数签名中新加一个参数即可, 在实际使用中还是比较方便, nex
的函数必须
包含两个返回值, 一个返回值代表正常返回数据, 另一个返回值代表错误信息
欢迎任何关于nex
的建议及意见, e-mail: chris@lonng.org, 欢迎 Star, nex 传送门
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.