# Golang语言编码规范

go-coding

版本号 编写团队 更新日期 备注
1.0 成都新生泰技术团队 2020.2.10 正式版

# 前言

这是一份关于Go语言开发的参考手册。欲获取更多信息与文档,请访问http://golang.org

# 1 命名规范

命名是代码规范中很重要的一部分,统一的命名规则有利于提高的代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。

Go在命名时以字母a到Z或a到Z或下划线开头,后面跟着零或更多的字母、下划线和数字(0到9)。Go不允许在命名时中使用@、$和%等标点符号。Go是一种区分大小写的编程语言。因此,Manpowermanpower是两个不同的命名。

  • 1 当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public)
  • 2 命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的private)

# 1.1 文件名

尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。

my_test.go
1

# 1.2 包命名

保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写。

package demo

package main

package utils
1
2
3
4
5

# 1.3 结构体命名

  • 采用驼峰命名法,首字母根据访问控制大写或者小写

  • struct申明和初始化格式采用多行,例如下面:

    // 多行申明
    type User struct{
        UserName  string
        Email     string
    }
    
    // 多行初始化
    u := User{
        UserName: "astaxie",
        Email:    "astaxie@gmail.com",
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

# 1.4 接口名

  • 命名规则基本和上面的结构体类型
  • 单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer
type Reader interface {
        Read(p []byte) (n int, err error)
}
1
2
3
  • 两个函数的接口名综合两个函数名

    type WriteFlusher interface {
        Write([]byte) (int, error)
        Flush() error
    }
    
    1
    2
    3
    4
  • 三个以上函数的接口名,类似于结构体名

type Car interface {
    Start([]byte)
    Stop() error
    Recover()
}
1
2
3
4
5

# 1.5 函数命名

若函数或方法为判断类型(返回值主要为 bool 类型),则名称应以 HasIsCan 或 Allow 等判断性动词开头:

func HasPrefix(name string, prefixes []string) bool { ... }
func IsEntry(name string, entries []string) bool { ... }
func CanManage(name string) bool { ... }
func AllowGitHook() bool { ... }
1
2
3
4

# 1.6 常量命名

常量包含(布尔常量、符文常量、整数常量、浮点数常量、复数常量和字符串常量)。字符、整数、浮点数和复数常量统称为数值常量

  • 常量均需使用全部大写字母组成,并使用下划线分词:const APP_VER = “1.0”
  • 如果是枚举类型的常量,需要先创建相应类型:
type Scheme string
const (
    HTTP  Scheme = "http"
    HTTPS Scheme = "https"
)
1
2
3
4
5
  • 如果模块的功能较为复杂、常量名称容易混淆的情况下,为了更好地区分枚举类型,可以使用完整的前缀:
type PullRequestStatus int
const (
    PULL_REQUEST_STATUS_CONFLICT PullRequestStatus = iota
    PULL_REQUEST_STATUS_CHECKING
    PULL_REQUEST_STATUS_MERGEABLE
)
1
2
3
4
5
6

# 1.7 变量命名

驼峰命名式。局部变量用小写字母开头。需要在package外部使用的全局变量用大写字母开头,否则用小写字母开头。

  • 全局变量:采用驼峰命名方式,仅限在包内的全局变量
  var ProjectName string 
  //如多组变量则使用,组和声明或者平行赋值
  var(
      ProjectName string 
   )
1
2
3
4
5
  • 局部变量:采用小驼峰命名方式,注意声明局部变量尽量使用 :=
    projectName := "name"
1

在相对简单的环境(对象数量少、针对性强)中,可以将一些名称由完整单词简写为单个字母,例如:

  • user 可以简写为 u
  • userID 可以简写 uid
  • 若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头:
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
1
2
3
4

# 2 注释规范

Go提供C风格的/* */块注释和C ++风格的//行注释。行注释是常态;块注释主要显示为包注释,但在表达式中很有用或禁用大量代码。

  • 单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释
  • 多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段

# 2.1 包注释

每个包都应该有一个包注释,一个位于package子句之前的块注释或行注释。包如果有多个go文件,只需要出现在一个go文件中(一般是和包同名的文件)即可。 包注释应该包含下面基本信息(请严格按照这个顺序,简介,创建人,创建时间):

  • 包的基本简介(包名,简介)
  • 创建者,格式: 创建人: rtx 名
  • 创建时间,格式:创建时间: yyyyMMdd
// util 包, 该包包含了项目共用的一些常量,封装了项目中一些共用函数。
// 创建人: hanru
// 创建时间: 20190419
1
2
3

# 2.2 结构体 (接口) 注释

每个自定义的结构体或者接口都应该有注释说明,该注释对结构进行简要介绍,放在结构体定义的前一行,格式为: 结构体名, 结构体说明。同时结构体内的每个成员变量都要有说明,该说明放在成员变量的后面(注意对齐),实例如下:

// User , 用户对象,定义了用户的基础信息
type User struct{
    Username  string // 用户名
    Email     string // 邮箱
}
1
2
3
4
5

# 2.3 函数(方法)注释

每个函数,或者方法(结构体或者接口下的函数称为方法)都应该有注释说明。

# 2.3.1 函数注释

// @Title 标题

// @Description 详细信息

// @Auth 创建时间 创建人

// @Param 参数类型 参数介绍

// @Return 返回类型 "错误信息"

// @Title NewtAttrModel 
// @Description 属性数据层操作类的工厂方法
// @Auth 福小林
// @Param  ctx  上下文信息
// @Return 属性操作类指针
func NewAttrModel(ctx *common.Context) *AttrModel {
}
1
2
3
4
5
6
7

# 2.3.2 函数注释

@Title 这个 API 所表达的含义,是一个文本,空格之后的内容全部解析为 title

@Description 这个 API 详细的描述,是一个文本,空格之后的内容全部解析为 Description

@Param 参数,表示需要传递到服务器端的参数,有五列参数,使用空格或者 tab 分割,表示的含义如下

1 参数名

2 参数类型,可以有的值是 formData、query、path、body、header,

3 参数类型

4 是否必须

5 注释

@Success 成功返回给客户端的信息

@Failure 失败返回的信息,包含两个参数,使用空格分隔,第一个表示 status code,第二个表示错误信息

@router 路由信息,包含两个参数,使用空格分隔,第一个是请求的路由地址,支持正则和自定义路由,和之前的路由规则一样,第二个参数是支持的请求方法,放在 [] 之中,如果有多个方法,那么使用 , 分隔。 ``

// @Title Get Product list
// @Description 开发时间 编写人 Get Product list by some info
// @Success 200 {object} models.ZDTProduct.ProductList
// @Param   category_id     query   int false       "category id"
// @Param   brand_id    query   int false       "brand id"
// @Param   query   query   string  false       "query of search"
// @Param   segment query   string  false       "segment"
// @Param   sort    query   string  false       "sort option"
// @Param   dir     query   string  false       "direction asc or desc"
// @Param   offset  query   int     false       "offset"
// @Param   limit   query   int     false       "count limit"
// @Param   price           query   float       false       "price"
// @Param   special_price   query   bool        false       "whether this is special price"
// @Param   size            query   string      false       "size filter"
// @Param   color           query   string      false       "color filter"
// @Param   format          query   bool        false       "choose return format"
// @Failure 400 no enough input
// @Failure 500 get products common error
// @router /products [get]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2.4 代码逻辑注释

对于一些关键位置的代码逻辑,或者局部较为复杂的逻辑,需要有相应的逻辑说明,方便其他开发者阅读该段代码,实例如下:

package main

import (
	"database/sql"
	"fmt"
//执行driver.go文件中的init(),向"database/sql"注册一个mysql的驱动
	_ "github.com/go-sql-driver/mysql" 
)

func main() {
	dsn := "root:admin@tcp(127.0.0.1:3306)/go_test?charset=utf8"
	//Open打开一个driverName指定的数据库,dataSourceName指定数据源
	//不会校验用户名和密码是否正确,只会对dsn的格式进行检测
	db, err := sql.Open("mysql", dsn)
	if err != nil { //dsn格式不正确的时候会报错
		fmt.Printf("打开数据库失败,err:%v\n", err)
		return
	}
	//尝试连接数据库,Ping方法可检查数据源名称是否合法,账号密码是否正确。
	err = db.Ping()
	if err != nil {
		fmt.Printf("连接数据库失败,err:%v\n", err)
		return
	}
	fmt.Println("连接数据库成功!")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 2.5 bug注释

针对代码中出现的bug,可以使用特殊的注释,在godocs可以做到注释高亮:

// BUG(astaxie):This divides by zero. 
var i float = 1/0
1
2

# 2.6 注释风格

统一使用中文注释,对于中英文字符之间严格使用空格分隔, 这个不仅仅是中文和英文之间,英文和中文标点之间也都要使用空格分隔,例如:

// 从 Redis 中批量读取属性,对于没有读取到的 id , 记录到一个数组里面,准备从 DB 中读取
1

上面 Redis 、 id 、 DB 和其他中文字符之间都是用了空格分隔。

  • 建议全部使用单行注释

  • 和代码的规范一样,单行注释不要过长,禁止超过 120 字符。

# 3 代码风格

# 3.1 缩进和折行

  • 缩进直接使用 gofmt工具格式化即可(gofmt是使用 tab缩进的);
  • 折行方面,一行最长不超过120个字符,超过的请使用换行展示,尽量保持格式优雅。

# 3.2 控制结构

# 3.2.1 语句的结尾

  • Go语言中是不需要类似于Java需要冒号结尾,默认一行就是一条数据
  • 如果你打算将多个语句写在同一行,它们则必须使用 ;

# 3.2.2 括号和空格

括号和空格方面,也可以直接使用gofmt工具格式化(go 会强制左大括号不换行,换行会报语法错误),所有的运算符和操作数之间要留空格

// 正确的方式
if a > 0 {

} 

// 错误的方式
if a>0  // a ,0 和 > 之间应该空格
{       // 左大括号不可以换行,会报语法错误

}
1
2
3
4
5
6
7
8
9
10

# 3.2.3 if

  • 条件语句不需要加上圆括号
  • 省略不必要的 else 语句
  • 可以加上合适的初始化语句
result := query()
if err := check(result); err != nil {
return err }
// 不需要 else doSomeThing(result)
1
2
3
4

# 3.2.4 for

Golang 只有 for 一种循环结构。

for i := 0; i < 10; i++ {
    ...
}
1
2
3

# 3.2.5 遍历

//遍历字符串
for pos, str := range "SONY大法好" { 
	fmt.Printf("%q: %d\n", str, pos)
}
//range可以遍历数组,切片,字典,管道和字符串。
for key, value := range oldMap {
    ...
}
1
2
3
4
5
6
7
8

# 3.2.6 switch

  • 表达式不限制为常量或整数
  • case 可以使用逗号来列举多个条件
  • 无需显式break,但使用break 可以提前结束
  func Factory(name string, value interface{}) interface{} {
   var object interface{}
    switch name {
    case "A", "AA":
        object = NewA()
    case "B":
        object = newB()
        if value == nil {
			break
			}
        object.SetValue(value)
    }
    return object
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.2.7 类型选择

对于接口变量,可以使用switch来判断其实际类型,这是一个很有用的技巧:

func ErrorWrap(e interface{}) *TraceableError {
    var message string
    switch e := e.(type) {
    case TraceableError:
        return &e
    case *TraceableError:
        return e
    case error:
        message = e.Error()
    default:
        message = fmt.Sprintf("%v", e)
    }
    return ErrorNew(message, 2)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.2.8 退出循环

因为break关键字在switch块中有特殊含义,因此无法直接用break退出循环,需要借助标签:

package main
import (
    "fmt"
)
func main() {
Loop:
    for index := 1; index < 10; index++ {
        switch index % 5 {
        case 1:
			break 
		case 0:
			break Loop
		default:
		    fmt.Printf("%v\n", index)
		} 
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 3.2.9 select

select用法类似于switch ,专用于轮询多个管道的读取。

# 3.3 结构体和接口

# 3.3.1 结构体初始化

p1 := new (MyStruct) // type *SyncedBuffer
p2 := &MyStruct{} // type *SyncedBuffer
var s1 MyStruct // type SyncedBuffer
s2 := MyStruct{} // type SyncedBuffer
1
2
3
4

初始化时可以指定结构成员的初始值:

type MyStruct1 struct {
    Value int
}
type MyStruct2 struct {
    MyStruct1
ID int }
// 直接初始化
s1 := MyStruct1{
Value: 0, }
// 嵌套结构
s2 := MyStruct2{
    ID: 0,
    MyStruct1: MyStruct1 {
Value: 1, }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.4 接收者

方法的接收者既可以声明成值类型,也可以声明成指针类型。调用时,Golang可以自动进行转换。但是需要注意的是,声明成值类型时,调用方法时传入的是调用者的拷贝, 而不是调用者本身,因此对接收者的修改将不生效。 接收者类型建议如下:

  • map , func , chan :不要使用指针
  • 切片类型:如果不存在对切片的重分配,
  • 则不要使用指针
  • 如果方法会改变接收者,必须使用指针
  • 如果接收者结构有类似 sync.Mutex 等用于同步的成员,必须使用
  • 一般情况,从实用性的角度出发,建议接收者都声明成指针类型

# 3.5 defer

  • 打开文件/连接等后需要 defer 来延时执行关闭
  • 慎用 defer 来处理锁
  • defer 求值是实时的,因此可以在循环中使用
package main
import "fmt"
func main() {
    word := "world"
    defer fmt.Printf("%v\n", word)
    word = "blueking"
    fmt.Printf("hello ") 
}
1
2
3
4
5
6
7
8

# 3.5 chan

Golang的并发模型基于CSP,并发实体(goroutine)通过管道(channel)进行通信。 管道本质上是一个结构体,维护发送和队列两个队列,创建管道时使用make :

ch := make(chan int, 0)
1

不要直接声明,这样可能会导致goroutine死锁:

var ch chan int
1

# 3.5.1 单向管道

管道可以机上只读和只写声明,这种用法一般用在函数声明中:

  • 只读管道: ch <-chan int
  • 只写管道: ch chan<- int
func handle(readCh <-chan int, writeCh chan<- int) {
go func() {
v := <-readCh
writeCh <- 2 * v
}()
}
1
2
3
4
5
6

# 3.6 goroutine

goroutineGlolang提供的一种并发模型,可以通过关键字来启动轻量级线程来执行指定的逻辑。但需要注意的是,goroutine并不是协程,底层实现是个线程池,一个 goroutine 在执行的过程中可能会跑在不同的线程和 CPU 上。

# 3.6.1 线程安全

因为goroutine是在线程池中执行,因此我们在goroutine中访问闭包需要考虑线程安全的问题。

# 3.6.2 Once

sync.Once提供了一个线程安全的单次执行接口,常用于单例模式或者初始化的场景。

package main
import (
    "sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.6.3 WaitGroup

Golang没有提供类似 thread.join等待goroutine结束的接口,我们可以用sync.WaitGroup来实现:

  • 初始化 WaitGroup,加上特定的值
  • 激活goroutine,goroutine结束时记得调用WaitGroup.Done()
  • 主流程执行 WaitGroup.Wait()
package main
import (
    "fmt"
    "sync"
)
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
	go func() {
	        fmt.Printf("hello ")
	        wg.Done()
	    }()
		wg.Wait()
	    fmt.Printf("world\n")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.6.4 Atomic

goroutine可以使用闭包特性访问外部变量,或者多个goroutine共同修改同一个变量, 很容易陷入了变量并发访问的陷阱。这个时候需要借助sync.atomic包提供的一系列底层内存同步原语来进行同步处理。 相比于公共变量,更推荐使用管道。

package main
import (
    "fmt"
"sync"
    "sync/atomic"
)
func main() {
    var value int64
    var wg sync.WaitGroup
	wg.Add(2)
    fun := func(count int) {
        for index := 0; index < count; index++ {
            atomic.AddInt64(&value, 1)  // not value++
        }
	wg.Done() }
    go fun(100)
    go fun(100)
	wg.Wait()
    fmt.Printf("%v\n", value)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3.7 import 规范

import在多行的情况下,goimports会自动帮你格式化,但是我们这里还是规范一下import的一些规范,如果你在一个文件里面引入了一个package,还是建议采用如下格式:

import (
    "fmt"
)
1
2
3

如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:

import (
    "encoding/json"  //标准包
    "strings"

    "myproject/models"
    "myproject/controller"   //内部包
    "myproject/utils"

    "github.com/astaxie/beego"   //第三方包
    "github.com/go-sql-driver/mysql"
)   
1
2
3
4
5
6
7
8
9
10
11

有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是项目包,第三是第三方包。

在项目中不要使用相对路径引入包:

// 这是不好的导入
import../net”

// 这是正确的做法
import “github.com/repo/proj/src/net”
1
2
3
4
5

但是如果是引入本项目中的其他包,最好使用相对路径。

# 3.8 错误处理

  • 错误处理的原则就是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。接收到错误,要么返回err,或者使用log记录下来
  • 尽早return:一旦有错误发生,马上返回
  • 尽量不要使用panic,除非你知道你在做什么
  • 错误描述如果是英文必须为小写,不需要标点结尾
  • 采用独立的错误流进行处理
// 错误写法
if err != nil {
    // error handling
} else {
    // normal code
}

// 正确写法
if err != nil {
    // error handling
    return // or continue, etc.
}
// normal code
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.9 参数传递

  • 对于少量数据,不要传递指针
  • 对于大量数据的struct可以考虑使用指针
  • 传入参数是map,slice,chan不要传递指针,因为map,slice,chan是引用类型,不需要传递指针的指针

# 3.10 单元测试

  • 1.单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件
  • 2.单元测试的函数名必须以Test开头,是可导出公开的函数。备注:函数名最好是Test+要测试的方法函数名
  • 3.测试函数的签名必须接收一个指向testing.T类型的指针作为参数,并且该测试函数不能返回任何值
  • 4.更多参考示例

# 4 数据库规范

# 4.1 总命名规范

  • 1、不得使用数据库保留关键字,以及golang/php/java等常用语言的保留关键字,或者可能成为关键字的单词作为完整命名。
  • 2、如无特殊说明,名称必须用英文字母开头,采用有特征含义的单词或缩写,单词中间用“_”分割,且只能由英文字母、数字和下划线组成,不能用双引号包含。
  • 3、除数据库名称长度为1至8个字符,其余(包括表、字段、索引等)不超过30个字符,Database link名称也不要超过30个字符。(30并不是凭空想象出来的,而是参考了Oracle的限制)

# 4.2 建表规范

# 4.2.1 表名

(建议以2-3字项目名称为前缀开头),紧跟2-5个字符(英文字母或数字,但不得全是数字)的模块名(必须), 最后跟上当前表的含义的单词(1-3个单词,用下划线连接),

例如:SQ_SYS_CAR,SQ是项目名称的缩写,SYS是模块名称的缩写,CAR表示当前表的具体含义。
特别强调:项目名称和模块名用简写(建议长度为2-5个字符),而表含义的名称,可简写、也可以不简写,但是都不能超过3个单词,
例如下面两个反面例子:
ABF_SUPERVISION_USER,问题:模块名称似乎比较长,建议控制在2-5个字符,缩写为 ABF_SUPV_USER; 
ABF_SYS_USER_MANAGE_ORG_ROLE,问题:除去前缀ABF_SYS_,表含义(USER_MANAGE_ORG_ROLE)超过了3个单词。
1
2
3
4
5

# 4.2.2 字段名

  • a) 表的字段数不超过50个。
  • b) 类型:各表之间相同含义的字段,类型定义要完全相同(包括精度、默认值等);
  • c) 命名:
    1. 字段名无单词数的限制,但是名字的字符长度应该符合上面的“总命名规范”。
    2. 字段命名及其注释,要做到清楚、无歧义。
1 表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是unsignedtinyint
2 字段名必须使用小写字母或数字;禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
3 小数类型为decimal,禁止使用float和double。
4 如果存储的字符串长度几乎相等,使用char定长字符串类型。
5 表必备三字段:id, create_time, modified_time
6 修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。
7 字段允许适当冗余,以提高性能,但是必须考虑数据同步的情况。
8 合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
1
2
3
4
5
6
7
8

# 4.3 索引规约

1 【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引
说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但提高查找速度是明显的;
另外,即使在应用层做了非常完善的校验和控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生
2 【强制】超过三个表禁止join。需要join的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引
说明:即使双表join也要注意表索引、SQL性能。
3.【强制】在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用count(distinctleft(列名, 索引长度))/count(*)的区分度来确定。
4 【推荐】如果有orderby的场景,请注意利用索引的有序性。orderby最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort的情况,影响查询性能。
说明: 正例:wherea=? andb=? orderbyc;索引:a_b_c反例:索引中有范围查找,那么索引有序性无法利用,
如:WHEREa>10 ORDERBYb;索引a_b无法排序。
1
2
3
4
5
6
7
8
9
10

# 4.4 SQL规约

1 【强制】不要使用count(列名)或count(常量)来替代count(*),count(*)就是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关。
说明:count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行。
2.【强制】count(distinctcol)计算该列除NULL之外的不重复数量。注意count(distinct col1, col2)如果其中一列全为NULL,那么即使另一列有不同的值,也返回为0。
3.【强制】当某一列的值全是NULL时,count(col)的返回结果为0,但sum(col)的返回结果为NULL,因此使用sum()时需注意NPE问题。
正例:可以使用如下方式来避免sum的NPE问题:SELECT IF(ISNULL(SUM(g)),0,SUM(g)) FROM table;
4.【强制】使用ISNULL()来判断是否为NULL值。注意:NULL与任何值的直接比较都为NULL。
说明:1)NULL<>NULL的返回结果是NULL,而不是false。2)NULL=NULL的返回结果是NULL,而不是true 3)NULL<>1的返回结果是NULL,而不是true
5.【强制】在代码中写分页查询逻辑时,若count为0应直接返回,避免执行后面的分页语句
6.【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
说明:(概念解释)学生表中的student_id是主键,那么成绩表中的student_id则为外键。
如果更新学生表中的student_id,同时触发成绩表中的student_id更新,则为级联更新。
外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
7.【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
8.【强制】数据订正时,删除和修改记录时,要先select,避免出现误删除,确认无误才能执行更新语句。
9.【推荐】in操作能避免则避免,若实在避免不了,需要仔细评估in后边的集合元素数量,控制在1000个之内。
10.【参考】如果有全球化需要,所有的字符存储与表示,均以utf-8编码,那么字符计数方法
说明:SELECTLENGTH("轻松工作");返回为12  SELECTCHARACTER_LENGTH("轻松工作");返回为4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 4.5 ORM规约

1.【强制】在表查询中,一律不要使用* 作为查询的字段列表,需要哪些字段必须明确写明。
说明:1)增加查询分析器解析成本。2)增减字段容易与resultMap配置不一致。
2【强制】xml配置中参数注意使用:#{},#param# 不要使用${} 此种方式容易出现SQL注入。
1
2
3

# 5 项目开发规范

# 5.1 项目目录结构

+--bin 编译后的文件
+--pkg 本项目或其他项目使用的包
项目根目录
  +--api 接口规范目录
  +--cmd
  +--swagger 自动化API文档
  +--test 该目录放的是临时的测试方法
  +--config 所有的配置文件目录
  +--internal 只在本项目使用的包
  +-- doc 说明文档(含go-bindata和mysql文件)
  +-- exec_package 可执行的打包文件(目前只有win 64bit的打包)
  +-- inits 所有需初始化的目录
  |       +-- parse 所有配置文件的初始化目录
  |       +-- init.go 用于初始化系统root用户,并注入所有service
  +-- middleware 包含的中间件目录
  |       +-- casbins 用于rbac权限的中间件的目录
  |       +-- jwts jwt中间件目录
  +-- resources 打包的前端静态资源文件
  |       +--img 静态图片 
  |       +--html  网页资源
  |       +--file 文件资源
  +-- utils 工具包目录
  +--plugin 插件,扩展码的包
  +-- web
  |       +-- db 数据库dao层目录
  |       +-- models  models 存放实体类
  |       +--service 业务逻辑
  |       +-- controller 所有分发出来的路由的目录
  |       +-- supports 提供辅助方法的目录(可以无)
  +-- main.go 入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 5.2 包管理器

统一使用Go Model进行包管理,对标准包,程序内部包,第三方包进行分组。 更多参考

import (
    "encoding/json"         //标准包
    "strings"

    "myproject/models"      //内部包
    "myproject/utils"

    "github.com/go-sql-driver/mysql"    //第三方包
)
1
2
3
4
5
6
7
8
9

常用的go mod命令如下:

go mod命令 描述
go mod download 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit 编辑go.mod文件
go mod graph 打印模块依赖图
go mod init 初始化当前文件夹, 创建go.mod文件
go mod tidy 增加缺少的module,删除无用的module
go mod vendor 将依赖复制到vendor下
go mod verify 校验依赖
go mod why 解释为什么需要依赖

# 5.3 自动格式化

gofmt(goimports)大部分的格式问题可以通过gofmt解决,gofmt自动格式化代码,保证所有的go代码与官方推荐的格式保持一致,于是所有格式有关问题,都以gofmt的结果为准

# 5.4 常用中间件

Middleware 描述 Example
jwt 检查请求的Authorization头,进行JWT检查和解析 jwt_example
cors HTTP 跨域请求。 cors_example
secure 一些快速安全实现的中间件。 secure_example
tollbooth 用于验证 HTTP 请求速率的通用中间件 tollbooth_example
cloudwatch AWS cloudwatch 指标中间件。 cloudwatch_example
new relic 官方 New Relic Go Agent. newrelic_example
prometheus 轻松为 prometheus 检测工具创建指标端点 prometheus_example
casbin 支持各种权限模型的授权库,例如ACL,RBAC,ABAC casbin_example
gorm gorm能够简化操作,提高开发效率。特别是对结构体的应用 gorm
go-redis go操作redis的中间件 go-redis
viper Viper是Go应用程序的完整配置解决方案,包括12-Factor应用程序。 viper
json-iterator json-iterator是一款快且灵活的JSON解析器,同时提供Java和Go两个版本。 json-iterator

# 6 安全规约

# 6.1 权限控制校验

系统所有页面必须进行权限控制校验。 防止没有做水平权限校验就可随意访问、操作别人的数据,比如查看、修改别人的数据。

# 6.2 数据脱敏

用户敏感数据禁止直接展示,必须对展示数据脱敏。 如查看个人手机号码会显示成:158****9119,隐藏中间4位,防止隐私泄露。 身份证脱敏等等

# 6.3 防止SQL注入

用户输入的SQL参数严格使用参数绑定或者METADATA字段值限定,防止SQL注入, 禁止字符串拼接SQL访问数据库。

# 6.4 参数有效性验证

用户请求传入的任何参数必须做有效性验证。 忽略参数校验可能导致如下异常:

  • pagesize过大导致内存溢出
  • 恶意orderby导致数据库慢查询
  • 任意重定向
  • SQL注入
  • 反序列化注入
  • 正则输入源串拒绝服务ReDoS

# 6.5 CSRF安全过滤

表单、AJAX提交必须执行CSRF安全过滤。 说明:CSRF(Cross-siterequestforgery)跨站请求伪造是一类常见编程漏洞。对于存在CSRF漏洞的应用/网站,攻击者可以事先构造好URL,只要受害者用户一访问,后台便在用户不知情情况下对数据库中用户参数进行相应修改。

# 7 参考资料

https://github.com/golang/go/wiki/CodeReviewComments

https://golang.org/doc/effective_go.html

http://lsdcloud.com/go/introduction.html