kevinyan815 / gocookbook

go cook book
MIT License
789 stars 167 forks source link

常用编码规范 #61

Open kevinyan815 opened 3 years ago

kevinyan815 commented 3 years ago

本来CookBook里记录的都是开发时通用问题的解决方案,不过除了解决功能上的问题,项目的长期迭代的质量保证也是每个开发人员都拥有的基本常识,而编码规范则是保证质量的一个根本,下面列出来一些编码规范,只列出了关键的一些规范也是想避免矫枉过正。当我们所在的组织有自己的规范,不用怀疑去遵守就好,如果没有通过遵守下面这些编码规范能让我们的代码质量有个基础的保证。

命名规范

类型 规则 正确示例 错误示例
表名 使用SnakeCase 命名法多个单词用下划线 _ 分割使用单词的复数形式命名 vip_members vipMembers vipMember vip_member
字段名 使用SnakeCase 命名法多个单词用下划线 _ 分割 user_name userName UserName _user_name
数据表在代码中的Model 名 使用CamelCase命名 单词使用单数形式 VipMember vipMember VipMembers Members vip_member
包名 使用SnakeCase命名法 单词的单数形式, 多个单词之间不要加分割符 member memberclub members memberClub member_club
Go程序文件名 使用SnakeCase 命名法多个单词用下划线 _ 分割(仅针对Go语言) family_relation.go familyRelation.go
路由/url 命名 路由里统一使用小写格式的单词,多个单词用短横线 - 分割。 路由定义需遵守RESTful 设计规范。
RESTful 规范指南:http://www.ruanyifeng.com/blog/2014/05/restful_api.html https://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html
/wechat/set-phone
/check-member-identity
/WeChat/setPhone /wechat/set_phone

Go语言编码规范

函数签名要避免歧义

函数名、参数名、参数类型、返回值类型要表达清楚要做的事情,避免产生歧义

错误案例

func handleSomething(delay int) {
  for {
    // ...
    time.Sleep(time.Duration(delay) * time.Millisecond)
  }
}
poll(10) // delay参数定义成int 每次加的延迟是10毫秒还是10秒,还需要看poll函数的实现才知道

正确案例


func handleSomething(delay time.Duration) {
  for {
    // ...
    time.Sleep(delay)
  }
}
poll(10 * time.Second) //delay参数定义成time.Duration类型, 调用时根据需求传递执行任务时要延迟的时间段

// 或者用参数名,明确告诉调用者,传递要延迟的秒数
func handleSomething(delaySeconds int) {
  for {
    // ...
    time.Sleep(delaySeconds * time.Second)
  }
}

函数体的长度控制在200行以内

函数体过长会严重影响阅读体验和理解函数造成的心智负担。

因此约定将函数体的长度控制在200行以内,如果实现的逻辑超过200行代码就要考虑代码写的是否简练,以及考虑将部分逻辑抽象到单独的函数中去。

避免在包导出的结构体内进行匿名嵌套

Go 允许结构体匿名嵌入另外一个结构体进行组合。 外部类型获取嵌入类型的方法和字段。

如果包导出的结构体内又匿名嵌入结构体,那么在包外部这些嵌入的类型的导出成员会泄漏实现细节,且不利于向后兼容。

错误案例

type AbstractList struct {}
// 添加将实体添加到列表中。
func (l *AbstractList) Add(e Entity) {
  // ...
}
// 移除从列表中移除实体。
func (l *AbstractList) Remove(e Entity) {
  // ...
}

// ConcreteList 是一个实体列表。
type ConcreteList struct {
  *AbstractList
}

cList := new(ConcreteList)
cList.Add(e) // 会默认委托到嵌入类型

正确案例

使用委托减少实现细节泄露


type AbstractList struct {}

func (l *AbstractList) Add(e Entity) {
  // ...
}

func (l *AbstractList) Remove(e Entity) {
  // ...
}

// ConcreteList 是一个实体列表。
type ConcreteList struct {
  list *AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

禁止使用硬编码的魔术数字或字符串进行逻辑判断

在逻辑判断里使用类似判断属性值是否等于某个硬编码的值时会使得代码晦涩难懂,应该使用更能从字面上看明白含义的常量来代替这些逻辑判断里硬编码的值。

错误案例

if prize.Type != 1 && prize.Type != 2{
    ......
}

正确案例

const (
  PRIZE_TYPE_COUPON = 1
  PRIZE_TYPE_MONEY = 2
  PRIZE_TYPE_VIPSCORE = 3
)

if prize.Type != PRIZE_TYPE_COUPON && prize.Type != PRIZE_TYPE_MONEY {
    ......
}

避免在init中修改已初始化好的数据

注意程序的完全确定性,不要依赖init执行的顺序实现功能,比如在后执行的init函数中对前面已初始化后的全局变量进行更改。

slice、map、chan、struct指针使用前必须先初始化


   var aMap map[string]string

aMap["foo"] = "bar" // panic
type Person struct {
    Friends []string
}

func main() {
    var f1 []string
    f2 := make([]string, 0)

    json1, _ := json.Marshal(Person{f1})
    json2, _ := json.Marshal(Person{f2})
    fmt.Printf("%s\n", json1)

    fmt.Printf("%s\n", json2)
}

{"Friends":null}

{"Friends":[]}
func main() {
   //fmt.Println("Called heapAnalysis", heapAnalysis())
   var achan chan struct{}
   achan <- struct{}{} // fatal error: all goroutines are asleep - deadlock!

}
func QueryData(a int) (data *Data, err error) {
    // data 返回值直接使用时,默认是nil
    // 确保安全应该先对data 进行初始化 data = new(Data)
    data, err := querySomeData()
    if errors.IsNotFoundErr(err) {
        return;
    }
}

func main() {
    dataP, err := QueryData()
    if err != nil {
        return err
    }

    if dataP.State == STATE_ACTIVE { // 此处有可能尝试对nil pointer进行解引用,会造成空指针问题程序崩溃。
        // active logic

    }
}

相似的声明要放在一组

错误案例

import "a"
import "b"

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV" // 不相关的定义不要放在一组
)

正确案例

import (
  "a"
  "b"
)

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

代码逻辑要尽量减少嵌套

代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码的代码量。

错误案例

for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}

正确案例

for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

减少不必要的else代码块

注意下面两种写法的直观感受

var a int
if b {
  a = 100
} else {
  a = 10
}

// 减少了不必要的else块
// 如果在 if 和 else 两个分支中都设置了变量,则可以将其替换为单个 if。
a := 10
if b {
  a = 100
}

初始化结构体时要指定字段名

在代码里做初始化结构体时,应该指定字段名称。

错误案例

k := User{"John", "Doe", true}

正确案例

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

尽量避免使用map[string]interface{} 类型的参数

在函数的参数中尽量不使用map[string]interface{}, map[string][string]这种类型的参数,IDE没法帮助提示这些参数的内部结构,这让其他人使用这个代码时就会很苦恼,还需要先看看函数实现里具体用到了字典的哪些键。

针对比较复杂的代表一类事物的参数,应该先定义结构体,然后使用结构体指针或者结构体指针切片作为参数。

错误案例

func AuthenticateUser(input map[string]interface{}) error {
    name, _ := input[name].(string)
    password, _ := input[name].(string)
    findUser(input["name"], input["password"])
    ...
}

正确案例

type UserAuth struct{
  Name     string
  Age      int32
  Password string
}
func AuthenticateUser(input *UserAuth) error {
    findUser(input.Name, input.Password)
    ...
}

底层代码只返回Error不对Error进行类似日志记录的处理

错误案例

package logic

func AddUserMoney(userId int64, money int64) {
    ......
    user, err := dao.GetUserById(userId)
    if err != nil {
        log.Error(...)
    }

}

package dao

func GetUserById(userId int64) (user userModel err error) {
    ......
    err = db.Where("user_id = ?", userId).Find(&userModel).Error
    if err != nil {
        log.Error("error msg", error)
    }
    return err
}

正确案例