Go标准化

前言

最近将公司的一个项目代码进行初步的标准化,在过程中也有一些感悟和收获,进行简要的记录

项目代码注重标准化可以帮助我们写出高质量的代码

Go写法标准化

  • 不要将interface做为万能的传递类型,不明确传递类型会降低代码的清晰度,interface类型可以代表任意类型,编译器不知道参数会是什么类型,只有运行时才知道,因此**只能分配到堆上(增加GC压力)**;并且如果接收方也是interface,则随意传递值会导致不可预估的后果(在twice中db层就用interface接收control层传的model是完全错误的)

  • 变量和函数命名规范,要有意义,看名知意且尽量简介,驼峰命名,动作+对象,不要在函数里创建大写开头的变量

  • error返回

    • 关于error的原则是遇到err就进行判定err!=nil而不是err==nil,因为后者就会将正确包在括号里,一层一层如套娃一样,不美观也不优雅
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    if err==nil{
    if err==nil{

    }else{}
    }else{}
    //修改为
    if err!=nil{

    }
    if err!=nil{

    }
    • 遇到err就要进行处理,而不是用匿名变量,唯一的对err都不关心,除非对返回结果也不关心

    • 很多说go语言语法丑陋的,就是因为错误的返回方式,但是在代码风格上也可以进行优化避免大量的 if err!=nil

    1
    2
    3
    4
    5
    6
    7
    8
    err:=xxxFunction()
    if err!=nil{

    }
    //修改为
    if err:=xxxFunction();err!=nil{

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func DBxxxOperate(xx model)error{
    err:=DB.Create(&xx).Error
    if err!=nil{
    return err
    }
    return nil
    }
    //修改为
    func DBxxxOperate(xx model)error{
    return DB.Create(&xx).Error
    }
  • 对野生goroutine进行兜底保护

    因为gin框架已经对每一个接口进行了兜底保护,即使发送panic也不会宕机(panic),但是如果在接口中使用了野生goroutine并且在里面发升了panic,那么整个程序都会崩溃!(包括主程序)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    go func() { 
    defer func(){ //兜底保护
    if err:=recover();err!=nil{
    logger("happen panic!")
    ...
    }
    }
    xxx()
    }()

DB层标准化

  • 返回时返回error而不是自定义的rsp结构体(从而到control层后直接作为数据返回给客户端),db层只是完成数据库相关操作,如果在其他也需要该db层返回的数据并且不需要rsp这样重量级的返回,则需要返回到control会做进一步处理,并且会增加db层的耦合度

  • 不要在db层做业务逻辑的操作,业务逻辑操作就该丢到control层db层只用来做好数据库信息的操作即可,做到功能内聚

    比如对查询到的数据数组进行进一步的操作时,需要将返回至control层再操作,而不是在db层操作后再返回给control层

  • 在db层的操作只有对于Find操作时需要传递指针的(因为只返回error),其他关于Create、Update、Delete操作只需要传递值即可(如果不需要数据段Create和Update的时间信息),项目中尽量减少指针传递的操作,避免频繁的内存逃逸增加Go的GC压力

    最开始以为传入指针就会发生内存逃逸,后面发现并不是,具体请查看 Go逃逸分析详解

  • 只有对于Find操作时需要传递指针的(因为要获取数据),其他关于Create、Update、Delete操作只需要传递值即可(如果不需要数据段Create和Update的时间信息),但是对于传值还是传指针,需要根据具体实际看(对于多字段的传值会有更多的copy操作)

Control层标准化

  • 函数传递的值能少也要尽量少(前提是满足需求)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func FindXxx(){
var article model
...
if err:=DBxxxOperate(article.ID,article.Tag,&article);err!=nil{

}
}

func DBxxxFind(id int,tag string,article *model)error{
return DB.Where("id=? AND type=?",id,tag).Find(&article).Error
}
//修改为
func FindXxx(){
var article model
...
if err:=DBxxxOperate(&article);err!=nil{

}
}

func DBxxxFind(article *model)error{
return DB.Where("id=? AND type=?",atricle.ID,article,Tag).Find(&article).Error
}
  • 要相信一个原则:永远不要相信前端给你传来的值,则对于前端传来的值都需要进行验证,比如delete时需要验证delete_id是否存在,因为就算不存在delete空也不会报错,而对于update更要验证update_id的存在,因为不存在的话update会在数据库创建一个update_id的对象

Error返回标准化

对于API接口的调用返回也是非常重要的,对于返回最要使用同一的结构体进行封装(这样也是为了前端姿势同一和在进行测试时便利)

采用CodeData的组合,没有加入消息Message和错误Error因为对于发生错误时是并不需要前端和客户端知道具体是什么错误,只需要知道错误码,而后端则可以通过错误码来查看具体api逻辑值错误的地方,而消息Message则可以写在日志中,需要时则进行查看

更新:现在需要加入消息msg,因为这样前端在测试时才知道是自己出现了问题还是后端的问题

因为只要是有相应,则说明客户端和服务器相应成功,都应该是返回200,所以设置新的错误码才能有利于去区分不同的错误类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Response struct {
Code ErrCode `json:"code,omitempty"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}

type ErrCode int

const Success ErrCode = 20000

const (
ErrService ErrCode = 1000
ErrBindJson = 1001
...
)

对返回进行进一步封装,可分为成功有返回、成功无返回、错误(是为了在control层写时清晰,知道是哪一种返回类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func ResSuccessData(c *gin.Context, data interface{}) {
ResJson(c, Response{Code: SuccessCode, Data: data})
}

func ResSuccess(c *gin.Context) {
ResJson(c, Response{Code: SuccessCode})
}

func ResErr(c *gin.Context, code ErrCode) {
ResJson(c, Response{Code: code})
}

func ResJson(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, data)
c.Abort()
}

总结

在平时或工作中需要对自己代码进行严格的把控,对需要标准化的地方要仔细,这样才能写出高质量的代码

但是并不是在任何时候都要对代码标准化严格执行,在实际生产中,代码是需要在规定的时间进行产出,如果在任何时候任何地点严格把控代码就会大大拖慢项目进度

例如在error返回标准化时在项目最初是不需要对错误码进行规范等

但是在err优化操作、大小写字母命名这样的习惯则需要在任何时候养成