V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
chrislon
V2EX  ›  程序员

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

  •  
  •   chrislon · 2016-09-29 20:18:11 +08:00 · 4709 次点击
    这是一个创建于 2771 天前的主题,其中的信息可能已经有所发展或是发生改变。

    使用 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: [email protected], 欢迎 Star, nex 传送门

    1 条回复    2017-12-29 11:42:02 +08:00
    shen100
        1
    shen100  
       2017-12-29 11:42:02 +08:00
    这里也有篇介绍 json 的文章,内容太性感
    https://www.golang123.com/topic/1434
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1765 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 00:41 · PVG 08:41 · LAX 17:41 · JFK 20:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.