Go语言如何开发RESTful API接口服务

此前一直写java,最近转go了,总结一下如何用Go语言开发RESTful API接口服务,希望对Go新手有所帮助,同时也希望Go大神不吝赐教!

Golang.png

Frameworks and Libraries

Gin

网络框架,采用的Gin。Gin 是用 Go 编写的一个 Web 应用框架,对比其它主流的同类框架,他有更好的性能和更快的路由。由于其本身只是在官方 net/http 包的基础上做的完善,所以理解和上手很平滑。

Xorm

对数据库操作,我们可以直接写SQl,也可以使用ORM工具,因为ORM工具使用起来方便简洁。我们项目中使用的是xorm库。特点是提供简单但丰富实用的 API 来完成对数据库的各类操作。该库支持包括 MySQL、PostgreSQL、SQLite3 和 MsSQL 在内的主流数据库,其在支持链式操作的基础上,还允许结合SQL语句进行混合处理。另外,该库还支持session事务和回滚以及乐观锁等。

Project and Package Structure

本项目是一个服务的QUERY端,因为我们采用的CQRS。因为query端简单,因此采用三层结构。分别为Controller层,Apllication层,以及Gateway层。接下来详细了解各层职责。

Controller层

Controller层,主要提供Restful接口,Restful接口一定要符合规范,这样,别人才容易理解。

本接口是要获取我的crm系统中,某个商户的某个需求的详细信息。因此Restful接口设计如下:

func RequirementRouter(r *gin.Engine) {
  r.GET("crm/merchants/:merchantId/requirements/:requirementId", handler.QueryRequirementById)
}

Application层

Application层

Application层是主要逻辑层,需要完成权限校验和数据查询以及格式转换的功能。其中分为了三个Package,分别为Auth,handler以及view。

auth

Auth package中提供身份认证以及权限校验接口。

// Get params from request and to authenticate
func HandleAuthenticateRequest() gin.HandlerFunc {
    return func(c *gin.Context) {
        userId := c.GetHeader(configuration.UIN)
        log.Debug("HandleAuthenticateRequest,user id is ", userId)
        identity, err := GetCasBinAuthInstance().HandleAuthenticate(userId, nil)
        if identity == "" || err != nil {
            c.Status(http.StatusUnauthorized)
            c.Abort()
        }
        c.Next()
    }
}

// Get params from request and to  authorize
func HandleAuthorizeRequest(ctx *gin.Context, objectType string, operation string) error {
    log.Debug("HandleAuthorizeRequest, object and operation is ", objectType, operation)
    userId := getUserIdFromRequest(ctx)
    if userId == "" {
        return errs.New(errs.UNAUTHORIZED, "user authorize failed,can not get user id")
    }
    merchantId := getMerchantIdFromRequest(ctx)
    if merchantId == "" {
        return errs.New(errs.UNAUTHORIZED, "user authorize failed,can not get merchant id")
    }
    permission := getPermission(objectType, operation, merchantId)
    success, err := GetCasBinAuthInstance().HandleAuthorize(userId, permission)
    if err != nil || !success {
        log.Warn("user authorize failed, userId=", userId, ", err=", err)
        return errs.New(errs.UNAUTHORIZED, "user authorize failed")
    }
    return nil
}

因为每个接口都需要身份认证,其实身份认证可以放到请求入口处做,而权限校验不是每个接口都需要,同时,权限包含一定的业务逻辑,因此权限校验在Applciation这一层做是比较合适的。

handler

handler pankage中是controller层和application的一个中间层,每一个Restful请求,对应一个Gin 的HandlerFunc,看一个Gin中的请求接口定义:

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle("POST", relativePath, handlers)
}

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle("GET", relativePath, handlers)
}

// DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle("DELETE", relativePath, handlers)
}

在每个handler我们首先调用auth的权限校验接口进行权限校验,然后对必须的请求参数进行检查,如果参数无效或缺少,返回错误,如果权限以及参数校验均通过,则调用view中的真正逻辑接口,进行查询数据。

func QueryRequirementById(ctx *gin.Context) {
    err := auth.HandleAuthorizeRequest(ctx, "P", "VIEW_REQUIREMENT_DETAIL")
    if err != nil {
        responseBodyHandler(ctx, nil, err)
        return
    }
    requirementId, err := strconv.Atoi(ctx.Param("requirementId"))
    if err != nil {
        resp := CreateResp("", errs.INVALID_PARAMETER, "请提供正确的需求ID")
        ctx.JSON(http.StatusBadRequest, &resp)
        return
    }
    doQueryRequirementById(ctx, requirementId)
}
func doQueryRequirementById(context *gin.Context, requirementId int) {
    defer func() {
        if err := recover(); err != nil {
            resp := CreateResp("", errs.UNKNOWN_ERROR, "server internal error")
            context.JSON(http.StatusInternalServerError, &resp)
            log.Error("QueryRequirementById errors=", err)
        }
    }()
    requirement, err := requirementView.QueryRequirementById(context, requirementId)
    responseBodyHandler(context, requirement, err)
}

一般函数不宜函数过长,如太长,建议封装为子函数。

view

view pankage中,是真正的查询逻辑service层,从数据库中查询出数据,然后,进行协议转换,返回给Controller。

// Query requirement by requirement id
func (v *RequirementView) QueryRequirementById(ctx context.Context, requirementId int) (*dto.RequirementDetail, error) {
    requirementPo, err := v.requirementDao.QueryRequirement(requirementId)
    if err != nil {
        return nil, nil
    }
    requirement :=getRequirementFromPo(requirementPo)
    return requirementDetail, nil
}

func (v *RequirementView) getRequirementFromPo(po *po.RequirementPo) *dto.RequirementDetail {
    var requirementDetai = dto.RequirementDetail{
        Id:                   po.Id,
        Name:                 po.Name,
        Category:             categoryName,
        Amount:               po.Amount,
        AmountUnit:           po.AmountUnit,
        ExpectedDeliveryDate: util.ToTimestamp(po.ExpectedDeliveryDate),
        Description:          po.Description,
        UnitPrice:            po.PricePerItem,
        DeliveryRhythm:       po.DeliveryRhythm,
        DeliveryStandard:     po.DeliveryStandard,
        CheckPeriod:          po.CheckPeriod,
    }
    return &requirementDetai
}

Gateway层

Gateway层

Gateway主要就是提供外部存储的增删改查能力,我们存储采用的mysql,因此,gateway主要就是操作mysql数据库。那么主要就包括Po以及Dao。

Xorm提供了很方便的工具,XORM工具,可以根据数据库表结构,生成相应的Po。这个工具的使用文档也可以参考这篇:使用xorm工具,根据数据库自动生成go代码

本项目生成的的RequirementPo如下:

type RequirementPo struct {
    Id                   int       `xorm:"not null pk comment('主键、自增') INT(11)"`
    Name                 string    `xorm:"not null default '' comment('需求名称, 最大长度: 50') VARCHAR(100)"`
    Description          string    `xorm:"not null comment('需求描述, 最大长度: 10000') LONGTEXT"`
    Status               int       `xorm:"not null comment('0: 新需求 1:已启动 2:生产中 3:已完成 4:结算中 5:已结算 6:已关闭') INT(11)"`
    CategoryId           int       `xorm:"not null comment('t_horizon_requirement_category 表的对应Id') INT(11)"`
    MerchantId           string    `xorm:"comment('CRM商户ID') index VARCHAR(100)"`
    Creator              string    `xorm:"not null default '' comment('需求创建人') VARCHAR(50)"`
    PricePerItem         float64   `xorm:"not null comment('需求单价') DOUBLE(16,2)"`
    Amount               float64   `xorm:"not null comment('需求数据量') DOUBLE(16,2)"`
    AmountUnit           string    `xorm:"not null default '" "' comment('用途:需求数据量单位,取值范围:无') VARCHAR(50)"`
    ExpectedDeliveryDate time.Time `xorm:"not null default '1970-01-01 08:00:01' comment('期望交付日期') TIMESTAMP"`
    DeliveryRule         string    `xorm:"not null comment('交付规则') VARCHAR(255)"`
    DeliveryStandard     string    `xorm:"not null comment('交付标准, 最大长度: 10000') TEXT"`
    DeliveryRhythm       string    `xorm:"not null default '" "' comment('用途:交付节奏;取值范围:无') VARCHAR(500)"`
    CheckPeriod          string    `xorm:"not null default '"   "' comment('用途:验收周期;取值范围:无') VARCHAR(255)"`
    DataVersion          int       `xorm:"default 0 comment('数据版本, 用于乐观锁') INT(11)"`
    Channel              string    `xorm:"not null default '' comment('创建需求的渠道') VARCHAR(255)"`
    CreateTime           time.Time `xorm:"not null default '1970-01-01 08:00:01' comment('创建时间') TIMESTAMP"`
}

通过XORM可以很方方便的对数据Po进行增删改查,我的查询接口如下:

func (d *RequirementDao) QueryRequirement(requirementId int) (*po.RequirementPo, error) {
    requirement := new(po.RequirementPo)
    session := Requirement.Engine().NewSession()
    _, err := session.Table("t_requirement").Where("id=?", requirementId).Get(requirement)
    if err != nil {
        log.Warn("query requirement error", err)
        return nil, errs.New(errs.DB_OPERATION_FAILED, "query requirement info fail")
    }
    return requirement, nil
}

想要了解更多Xorm使用信息,可以查看其文档:xorm Gobook 以及 xorm Godoc

main

最后看一下main package中的类文件职责。

func main() {
    r := gin.New()
    r.Use(gin.Recovery())
    //添加监控
    r.Use(MonitorHandler())
    //添加身份认证校验
    r.use(authenticateHandler())
    //restul接口路由
    controller.RequirementRouter(r)

    r.Run(":" + configuration.Base.Server.Port)
}

ok,这样即一个go的http服务的主要模块,当然还有配置文件类等,这个按照自己喜欢的方法,自行开发加载配置逻辑以及使用网上开源的包均可。

后记

刚接触GO语言不久,还有很长的路要走,加油!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339