代码仅用于介绍 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: [email protected], 欢迎 Star, nex 传送门
1 shen100 2017-12-29 11:42:02 +08:00 这里也有篇介绍 json 的文章,内容太性感 https://www.golang123.com/topic/1434 |