干货: 使用 Golang 快速构建 JSON API 服务

2016-09-29 20:18:11 +08:00
 chrislon

使用 Golang 快速构建 JSON API 服务

代码仅用于介绍 nex 包的用法, 存在多处不严谨

越来越多的 WEB 应用采用前后端完全分离, WEB 前端使用 Angular/Vue 之类的库, 用 JSON 通过 RESTful 与服务器通讯, 同样的服务器接口不但可以用于 WEB 前端, 同时能用于动应用前端.

这个例子通过一个失物招领的信息管理系统来介绍如何使用nex包快速 构建 JSON API 服务, 限于作者水平有限, 如果发现错误, 欢迎指正.

REPO 名字的由来

名字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 最终在releasedevelop版本包含不同级别的错误信息.

总结

nex主要用于将一个符合nex签名的函数转换成符合http.Handler接口的结构, 并在请求到达时, 自动进行依赖注入, 相对于HandleFunc更加便于写单元测试, 并且减少在各个接口中序列化反序列化中的大量冗余代码, 我在使用go-kit 的过程中就存在这个问题.

相关功能逻辑单元如果需要新的依赖, 只需要在函数签名中新加一个参数即可, 在实际使用中还是比较方便, nex的函数必须 包含两个返回值, 一个返回值代表正常返回数据, 另一个返回值代表错误信息


欢迎任何关于nex的建议及意见, e-mail: chris@lonng.org, 欢迎 Star, nex 传送门

4918 次点击
所在节点    程序员
1 条回复
shen100
2017-12-29 11:42:02 +08:00
这里也有篇介绍 json 的文章,内容太性感
https://www.golang123.com/topic/1434

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/309855

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX