为形成统一的Go 开发的代码风格,以保障项目代码的易维护性和编码安全性,特制定本规范。本规范在 Google Golang 代码规范 的基础上,可根据自身的情况提出建议和issue。
代码规范 包含注包/变量/常量命名、基本语法、函数、错误处理
每项规范内容,给出了要求等级,其定义为:
- 必须(Mandatory):用户必须采用;
- 推荐(Preferable):用户理应采用,但如有特殊情况,可以不采用;
- 可选(Optional):用户可参考,自行决定是否采用;
-
代码都必须用
gofmt
格式化。可使用goland 配置 -
编写的代码每行应该不超过 160 个字符,超出部分用换行解决。
-
单个文件最大行数最大不超过 1000 行.
-
单个函数最大行数不超过 120 行。
-
例外场景:
- import 模块语句
- 工具生成代码
- struct tag
- SQL
-
运算符和操作数之间要留空格。
-
作为输入参数或者数组下标时,运算符和运算数之间不需要空格,紧凑展示。
- 使用
goimports
自动格式化引入的包名,import 规范原则上以goimports
规则为准。 goimports
会自动把依赖包按首字母排序,并对包进行分组管理,通过空行隔开,默认分为本地包(标准库、内部包)、第三方包。- 内部包是指不能被外部 import 的包,如 GoPath 模式下的包名或者非域名开头的当前项目的 GoModules 包名。
- 带域名的包名都属于第三方包,如 github.com/xxx/xxx,不用区分是否是当前项目内部的包。
goimports
默认最少分成本地包和第三方包两大类,这两类包必须分开不能放在一起。本地包或者第三方包内部可以继续按实际情况细分不同子类。- 不要使用相对路径引入包:
// 不要采用这种方式
import (
"../net"
)
- 应该使用完整的路径引入包:
import (
"xxxx.com/proj/net"
)
- 包名和 git 路径名不一致时,或者多个相同包名冲突时,使用别名代替,别名命名规范和包命名规范保持一致:
// 合理用法:包名和 git 路径名不一致,使用别名
import (
opentracing "github.com/opentracing/opentracing-go"
)
// 合理用法:多个相同包名冲突,使用别名
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
// 不合理用法:包名和路径名一致,也不存在多包名冲突,并且原包名符合规范,则不应该使用别名
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
- 【可选】第三方包的包名不符合规范可使用别名修正:
import (
xyz "github.com/xxxx/X_Y_Z"
)
- 【可选】匿名包的引用建议使用一个新的分组引入,并在匿名包上写上注释说明。
完整示例如下:
import (
// standard package & inner package
"encoding/json"
"myproject/models"
"myproject/controller"
"strings"
// third-party package
"github.com/obc/utils"
"github.com/dep/beego"
"github.com/dep/mysql"
opentracing "github.com/opentracing/opentracing-go"
// anonymous import package
// import filesystem storage driver
_ "github.com/org/repo/pkg/storage/filesystem
)
- 例外包
- embed 当使用embed内嵌外部数据时不需要注释
error
作为函数的值返回,必须对error
进行处理, 或将返回值赋值给明确忽略。对于defer xx.Close()
可以不用显式处理。error
作为函数的值返回且有多个返回值的时候,error
必须是最后一个参数。
// 不要采用这种方式
func test() (error, int) {
}
// 要采用下面的方式
func test() (int, error) {
}
- 错误描述不需要标点结尾。
- 采用独立的错误流进行处理。
// 不要采用这种方式
if err != nil {
// error handling
} else {
// normal code
}
// 而要采用下面的方式
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
- 如果返回值需要初始化,则采用下面的方式:
x, err := f()
if err != nil {
// error handling
return // or continue, etc.
}
// use x
- 错误返回的判断独立处理,不与其他变量组合逻辑判断。
// 不要采用这种方式:
x, y, err := f()
if err != nil || y == nil {
return err // 当y与err都为空时,函数的调用者会出现错误的调用逻辑
}
// 应当使用如下方式:
x, y, err := f()
if err != nil {
return err
}
if y == nil {
return errors.New("some error")
}
- 【推荐】对于不需要格式化的错误,生成方式为:
errors.New("xxxx")
。 - 【推荐】推荐使用
github.com/pkg/errors
包
- 在业务逻辑处理中禁止使用
panic
。 - 在
main
包中只有当完全不可运行的情况可使用panic
,例如:文件无法打开,数据库无法连接导致程序无法正常运行。 - 对于其它的包,可导出的接口一定不能有
panic
;在包内传递错误时,不推荐使用panic
来传递error
。
// 不推荐为传递error而在包内使用panic,以下为示例
// PError 包内定义的错误类型
type PError string
// Error error接口方法
func (e PError) Error() string {
return string(e)
}
func do(str string) {
// ...
// 此处的panic用于传递error
panic(PError("错误信息"))
// ...
}
// Do 包级访问入口
func Do(str string) error {
var err error
defer func() {
if e := recover(); e != nil {
err = e.(PError)
}
}()
do(str)
return err
}
- 建议在
main
包中使用log.Fatal
来记录错误,这样就可以由log
来结束程序,或者将panic
抛出的异常记录到日志文件中,方便排查问题。 panic
捕获只能到goroutine
最顶层,每个自行启动的goroutine
,必须在入口处捕获panic
,并打印详细堆栈信息或进行其它处理。
recover
用于捕获runtime
的异常,禁止滥用recover
。- 必须在
defer
中使用,一般用来捕获程序运行期间发生异常抛出的panic
或程序主动抛出的panic
。
package main
import (
"log"
)
func main() {
defer func() {
if err := recover(); err != nil {
// do something or record log
log.Println("exec panic error: ", err)
// log.Println(debug.Stack())
}
}()
getOne()
panic(11) // 手动抛出panic
}
// getOne 模拟slice越界 runtime运行时抛出的panic
func getOne() {
defer func() {
if err := recover(); err != nil {
// do something or record log
log.Println("exec panic error: ", err)
// log.Println(debug.Stack())
}
}()
var arr = []string{"a", "b", "c"}
log.Println("hello,", arr[4])
}
// 执行结果:
// 2020/01/02 17:18:53 exec panic error: runtime error: index out of range
// 2020/01/02 17:18:53 exec panic error: 11
- 单元测试文件名命名规范为
example_test.go
。 - 测试用例的函数名称必须以
Test
开头,例如TestExample
。 - 如果存在
func Foo
,单测函数可以带下划线,为func Test_Foo
。如果存在func (b *Bar) Foo
,单测函数可以为func TestBar_Foo
。下划线不能出现在前面描述情况以外的位置。 - 单测文件行数限制是普通文件的2倍,即
1600行
。单测函数行数限制也是普通函数的2倍,即为160行
。圈复杂度、列数限制、 import 分组等其他规范细节和普通文件保持一致。 - 由于单测文件内的函数都是不对外的,所有可导出函数可以没有注释,但是结构体定义时尽量不要导出。
- 每个重要的可导出函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。
type assertion
的单个返回值形式针对不正确的类型将产生panic
。因此,请始终使用“comma ok”
的惯用法。
// 不要采用这种方式
t := i.(string)
// 而要采用下面的方式
t, ok := i.(string)
if !ok {
// 优雅地处理错误
}
- 在编码阶段同步写好变量、函数、包注释,注释可以通过
godoc
导出生成文档。 - 程序中每一个被导出的(大写的)名字,都应该有一个文档注释。
- 所有注释掉的代码在提交 code review 前都应该被删除,除非添加注释讲解为什么不删除, 并且标明后续处理建议(比如删除计划)。
- 使用goanno setting插件
// GetModelList
// @Description: 查询模板列表
// @return []entity.ModelDetailResult
// @return error
- 每个包都应该有一个包注释。
- 包如果有多个 go 文件,只需要出现在一个 go 文件中(一般是和包同名的文件)即可,格式为:“// Package 包名 包信息描述”。
// Package math provides basic constants and mathematical functions.
package math
// 或者
/*
Package template implements data-driven templates for generating textual
output such as HTML.
....
*/
package template
- 每个需要导出的自定义结构体或者接口都必须有注释说明。
- 注释对结构进行简要介绍,放在结构体定义的前一行。
- 格式为:"// 结构体名 结构体信息描述"。
- 结构体内的可导出成员变量名,如果是个生僻词,或者意义不明确的词,就必须要给出注释,放在成员变量的前一行或同一行的末尾。
// User 用户结构定义了用户基础信息
type User struct {
Name string
Email string
// Demographic 族群
Demographic string
}
- 每个需要导出的函数或者方法(结构体或者接口下的函数称为方法)都必须有注释。注意,如果方法的接收器为不可导出类型,可以不注释,但需要质疑该方法可导出的必要性。
- 注释描述函数或方法功能、调用方等信息。
- 格式为:"// 函数名 函数信息描述"。
// NewtAttrModel 是属性数据层操作类的工厂方法
func NewAttrModel(ctx *common.Context) *AttrModel {
// TODO
}
- 例外方法:
- Write Read 用于常见IO
- ServeHTTP 用于HTTP服务
- String 用于打印
- Unwrap Error 用于错误处理
- Len Less Swap 用于排序
- 每个需要导出的常量和变量都必须有注释说明。
- 该注释对常量或变量进行简要介绍,放在常量或者变量定义的前一行。
- 大块常量或变量定义时,可在前面注释一个总的说明,然后每一行常量的末尾详细注释该常量的定义。
- 格式为:"// 变量名 变量信息描述",斜线后面紧跟一个空格。
// FlagConfigFile 配置文件的命令行参数名
const FlagConfigFile = "--config"
// 命令行参数
const (
FlagConfigFile1 = "--config" // 配置文件的命令行参数名1
FlagConfigFile2 = "--config" // 配置文件的命令行参数名2
FlagConfigFile3 = "--config" // 配置文件的命令行参数名3
FlagConfigFile4 = "--config" // 配置文件的命令行参数名4
)
// FullName 返回指定用户名的完整名称
var FullName = func(username string) string {
return fmt.Sprintf("fake-%s", username)
}
- 每个需要导出的类型定义(type definition)和类型别名(type aliases)都必须有注释说明。
- 该注释对类型进行简要介绍,放在定义的前一行。
- 格式为:"// 类型名 类型信息描述"。
// StorageClass 存储类型
type StorageClass string
// FakeTime 标准库时间的类型别名
type FakeTime = time.Time
命名是代码规范中很重要的一部分,统一的命名规范有利于提高代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。
-
保持
package
的名字和目录一致。 -
尽量采取有意义、简短的包名,尽量不要和标准库冲突。
-
包名应该为小写单词,不要使用下划线或者混合大小写,使用多级目录来划分层级。
-
包名可谨慎地使用缩写。当缩写是程序员广泛熟知的词时,可以使用缩写。例如:
- strconv (string conversion)
- syscall (system call)
- fmt (formatted I/O)
如果缩写有歧义或不清晰,不用缩写。
-
项目名可以通过中划线来连接多个单词。
-
简单明了的包命名,如:
time
、list
、http
。 -
不要使用无意义的包名,如:
util
、common
、misc
、global
。
package
名字应该追求清晰且越来越收敛,符合‘单一职责’原则。而不是像common
一样,什么都能往里面放,越来越膨胀,让依赖关系变得复杂,不利于阅读、复用、重构。注意,xx/util/encryption
这样的包名是允许的。
- 采用有意义,简短的文件名。
- 文件名应该采用小写,并且使用下划线分割各个单词。
- 采用驼峰命名方式,首字母根据访问控制采用大写或者小写。
- 结构体名应该是名词或名词短语,如
Customer
、WikiPage
、Account
、AddressParser
,它不应是动词。 - 避免使用
Data
、Info
这类意义太宽泛的结构体名。 - 结构体的声明和初始化格式采用多行,例如:
// User 多行声明
type User struct {
Name string
Email string
}
// 多行初始化
u := User{
Name: "john",
Email: "[email protected]",
}
- 命名规则基本保持和结构体命名规则一致。
- 单个函数的接口名以
er
作为后缀,例如Reader
,Writer
。
// Reader 字节数组读取接口
type Reader interface {
// Read 读取整个给定的字节数据并返回读取的长度
Read(p []byte) (n int, err error)
}
- 两个函数的接口名综合两个函数名。
- 三个以上函数的接口名,类似于结构体名。
// Car 小汽车结构申明
type Car interface {
// Start ...
Start([]byte)
// Stop ...
Stop() error
// Recover ...
Recover()
}
- 变量名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。
- 特有名词时,需要遵循以下规则:
- 如果变量为私有,且特有名词为首个单词,则使用小写,如
apiClient
; - 其他情况都应该使用该名词原有的写法,如
APIClient
、repoID
、UserID
; - 错误示例:
UrlArray
,应该写成urlArray
或者URLArray
; - 详细的专有名词列表可参考这里。
- 如果变量为私有,且特有名词为首个单词,则使用小写,如
- 私有全局变量和局部变量规范一致,均以小写字母开头。
- 代码生成工具自动生成的代码可排除此规则(如 xxx.pb.go 里面的 Id)。
- 变量名更倾向于选择短命名。特别是对于局部变量。
c
比lineCount
要好,i
比sliceIndex
要好。基本原则是:变量的使用和声明的位置越远,变量名就需要具备越强的描述性。
- 常量均需遵循驼峰式。
// AppVersion 应用程序版本号定义
const AppVersion = "1.0.0"
- 如果是枚举类型的常量,需要先创建相应类型:
// Scheme 传输协议
type Scheme string
const (
// HTTP 表示HTTP明文传输协议
HTTP Scheme = "http"
// HTTPS 表示HTTPS加密传输协议
HTTPS Scheme = "https"
)
- 私有全局常量和局部变量规范一致,均以小写字母开头。
const appVersion = "1.0.0"
- 函数名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。
- 代码生成工具自动生成的代码可排除此规则(如协议生成文件 xxx.pb.go , gotests 自动生成文件 xxx_test.go 里面的下划线)。
if
接受初始化语句,约定如下方式建立局部变量:
if err := file.Chmod(0664); err != nil {
return err
}
if
对两个值进行判断时,约定如下顺序:变量在左,常量在右:
// 不要采用这种方式
if nil != err {
// error handling
}
// 不要采用这种方式
if 0 == errorCode {
// do something
}
// 而要采用下面的方式
if err != nil {
// error handling
}
// 而要采用下面的方式
if errorCode == 0 {
// do something
}
if
对于bool类型的变量,应直接进行真假判断:
go
复制代码var allowUserLogin bool
// 不要采用这种方式
if allowUserLogin == true {
// do something
}
// 不要采用这种方式
if allowUserLogin == false {
// do something
}
// 而要采用下面的方式
if allowUserLogin {
// do something
}
// 而要采用下面的方式
if !allowUserLogin {
// do something
}
- 采用短声明建立局部变量:
sum := 0
for i := 0; i < 10; i++ {
sum += 1
}
- 如果只需要第一项(key),就丢弃第二个:
for key := range m {
if key.expired() {
delete(m, key)
}
}
- 如果只需要第二项,则把第一项置为下划线:
sum := 0
for _, value := range array {
sum += value
}
- 要求必须有
default
:
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
- 尽早
return
,一旦有错误发生,马上返回:
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
d, err := f.Stat()
if err != nil {
return err
}
codeUsing(f, d)
- 业务代码禁止使用
goto
,其他框架或底层源码推荐尽量不用。
- 函数返回相同类型的两个或三个参数,或者如果从上下文中不清楚结果的含义,使用命名返回,其它情况不建议使用命名返回。
// Parent1 ...
func (n *Node) Parent1() *Node
// Parent2 ...
func (n *Node) Parent2() (*Node, error)
// Location ...
func (f *Foo) Location() (lat, long float64, err error)
- 传入变量和返回变量以小写字母开头。
- 参数数量均不能超过
5个
。 - 尽量用值传递,非指针传递。
- 传入参数是
map
,slice
,chan
,interface
不要传递指针。
- 当存在资源管理时,应紧跟
defer
函数进行资源的释放。 - 判断是否有错误发生之后,再
defer
释放资源。
resp, err := http.Get(url)
if err != nil {
return err
}
// 如果操作成功,再defer Close()
defer resp.Body.Close()
- 禁止在循环中使用
defer
,举例如下:
// 不要这样使用
func filterSomething(values []string) {
for _, v := range values {
fields, err := db.Query(v) // 示例,实际不要这么查询,防止sql注入
if err != nil {
// xxx
}
defer fields.Close()
// 继续使用fields
}
}
// 应当使用如下的方式:
func filterSomething(values []string) {
for _, v := range values {
func() {
fields, err := db.Query(v) // 示例,实际不要这么查询,防止sql注入
if err != nil {
...
}
defer fields.Close()
// 继续使用fields
}()
}
}
- 【推荐】推荐以类名第一个英文首字母的小写作为接收器的命名。
- 【推荐】接收器的命名在函数超过
20行
的时候不要用单字符。 - 【必须】命名不能采用
me
,this
,self
这类易混淆名称。
- 【必须】文件长度不能超过
1000行
。 - 【推荐】函数长度不能超过
100行
(函数长度为函数签名左括号下一行开始到右括号上一行结束部分的行数,包括代码行,注释行,空行)。
- 嵌套深度不能超过
4层
:
// AddArea 添加成功或出错
func (s *BookingService) AddArea(areas ...string) error {
s.Lock()
defer s.Unlock()
for _, area := range areas {
for _, has := range s.areas {
if area == has {
return srverr.ErrAreaConflict
}
}
s.areas = append(s.areas, area)
s.areaOrders[area] = new(order.AreaOrder)
}
return nil
}
// 建议调整为这样:
// AddArea 添加成功或出错
func (s *BookingService) AddArea(areas ...string) error {
s.Lock()
defer s.Unlock()
for _, area := range areas {
if s.HasArea(area) {
return srverr.ErrAreaConflict
}
s.areas = append(s.areas, area)
s.areaOrders[area] = new(order.AreaOrder)
}
return nil
}
// HasArea ...
func (s *BookingService) HasArea(area string) bool {
for _, has := range s.areas {
if area == has {
return true
}
}
return false
}
- 变量声明尽量放在变量第一次使用前面,就近原则。
- 如果魔法数字出现超过
2次
,则禁止使用。
func getArea(r float64) float64 {
return 3.14 * r * r
}
func getLength(r float64) float64 {
return 3.14 * 2 * r
}
- 用一个常量代替:
// PI ...
const PI = 3.14
func getArea(r float64) float64 {
return PI * r * r
}
func getLength(r float64) float64 {
return PI * 2 * r
}
go mod init github.com/group/myrepo
- 建议所有不对外开源的工程的
module name
使用github.com/group/repo
,方便他人直接引用。 - 建议使用
go modules
作为依赖管理的项目不提交vendor
目录。 - 建议使用
go modules
管理依赖的项目,go.sum
文件必须提交,不要添加到 .gitignore 规则中。 - 建议使用
go work
管理模块的项目,go.work.sum
文件必须提交,不要添加到 .gitignore 规则中。
- 其中建议包括服务基本描述、使用方法、部署时的限制与要求、基础环境依赖(例如最低 go 版本、最低外部通用包版本)等。
- 在对slice进行操作时,必须判断长度是否合法,防止程序panic
// bad: 未判断data的长度,可导致 index out of range
func decode(data []byte) bool {
if data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' && data[3] == 'Z' && data[4] == 'E' && data[5] == 'R' {
fmt.Println("Bad")
return true
}
return false
}
// bad: slice bounds out of range
func foo() {
var slice = []int{0, 1, 2, 3, 4, 5, 6}
fmt.Println(slice[:10])
}
// good: 使用data前应判断长度是否合法
func decode(data []byte) bool {
if len(data) == 6 {
if data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' && data[3] == 'Z' && data[4] == 'E' && data[5] == 'R' {
fmt.Println("Good")
return true
}
}
return false
}
- 进行指针操作时,必须判断该指针是否为nil,防止程序panic,尤其在进行结构体Unmarshal时
type Packet struct {
PackeyType uint8
PackeyVersion uint8
Data *Data
}
type Data struct {
Stat uint8
Len uint8
Buf [8]byte
}
func (p *Packet) UnmarshalBinary(b []byte) error {
if len(b) < 2 {
return io.EOF
}
p.PackeyType = b[0]
p.PackeyVersion = b[1]
// 若长度等于2,那么不会new Data
if len(b) > 2 {
p.Data = new(Data)
}
return nil
}
// bad: 未判断指针是否为nil
func main() {
packet := new(Packet)
data := make([]byte, 2)
if err := packet.UnmarshalBinary(data); err != nil {
fmt.Println("Failed to unmarshal packet")
return
}
fmt.Printf("Stat: %v\n", packet.Data.Stat)
}
// good: 判断Data指针是否为nil
func main() {
packet := new(Packet)
data := make([]byte, 2)
if err := packet.UnmarshalBinary(data); err != nil {
fmt.Println("Failed to unmarshal packet")
return
}
if packet.Data == nil {
return
}
fmt.Printf("Stat: %v\n", packet.Data.Stat)
}
- 在进行数字运算操作时,需要做好长度限制,防止外部输入运算导致异常:
- 确保无符号整数运算时不会反转
- 确保有符号整数运算时不会出现溢出
- 确保整型转换时不会出现截断错误
- 确保整型转换时不会出现符号错误
- 以下场景必须严格进行长度限制:
- 作为数组索引
- 作为对象的长度或者大小
- 作为数组的边界(如作为循环计数器)
// bad: 未限制长度,导致整数溢出
func overflow(numControlByUser int32) {
var numInt int32 = 0
numInt = numControlByUser + 1
// 对长度限制不当,导致整数溢出
fmt.Printf("%d\n", numInt)
// 使用numInt,可能导致其他错误
}
func main() {
overflow(2147483647)
}
// good
func overflow(numControlByUser int32) {
var numInt int32 = 0
numInt = numControlByUser + 1
if numInt < 0 {
fmt.Println("integer overflow")
return
}
fmt.Println("integer ok")
}
func main() {
overflow(2147483647)
}
- 在进行make分配内存时,需要对外部可控的长度进行校验,防止程序panic。
// bad
func parse(lenControlByUser int, data []byte) {
size := lenControlByUser
// 对外部传入的size,进行长度判断以免导致panic
buffer := make([]byte, size)
copy(buffer, data)
}
// good
func parse(lenControlByUser int, data []byte) ([]byte, error) {
size := lenControlByUser
// 限制外部可控的长度大小范围
if size > 64*1024*1024 {
return nil, errors.New("value too large")
}
buffer := make([]byte, size)
copy(buffer, data)
return buffer, nil
}
- 当一个对象从被GC选中到移除内存之前,runtime.SetFinalizer()都不会执行,即使程序正常结束或者发生错误。由指针构成的“循环引用”虽然能被GC正确处理,但由于无法确定Finalizer依赖顺序,从而无法调用runtime.SetFinalizer(),导致目标对象无法变成可达状态,从而造成内存无法被回收。
// bad
func foo() {
var a, b Data
a.o = &b
b.o = &a
// 指针循环引用,SetFinalizer()无法正常调用
runtime.SetFinalizer(&a, func(d *Data) {
fmt.Printf("a %p final.\n", d)
})
runtime.SetFinalizer(&b, func(d *Data) {
fmt.Printf("b %p final.\n", d)
})
}
func main() {
for {
foo()
time.Sleep(time.Millisecond)
}
}
- 重复释放一般存在于异常流程判断中,如果恶意攻击者构造出异常条件使程序重复释放channel,则会触发运行时panic,从而造成DoS攻击。
// bad
func foo(c chan int) {
defer close(c)
err := processBusiness()
if err != nil {
c <- 0
close(c) // 重复释放channel
return
}
c <- 1
}
// good
func foo(c chan int) {
defer close(c) // 使用defer延迟关闭channel
err := processBusiness()
if err != nil {
c <- 0
return
}
c <- 1
}
- 启动一个协程就会做一个入栈操作,在系统不退出的情况下,协程也没有设置退出条件,则相当于协程失去了控制,它占用的资源无法回收,可能会导致内存泄露。
// bad: 协程没有设置退出条件
func doWaiter(name string, second int) {
for {
time.Sleep(time.Duration(second) * time.Second)
fmt.Println(name, " is ready!")
}
}
- 由于unsafe包绕过了 Golang 的内存安全原则,一般来说使用该库是不安全的,可导致内存破坏,尽量避免使用该包。若必须要使用unsafe操作指针,必须做好安全校验。
// bad: 通过unsafe操作原始指针
func unsafePointer() {
b := make([]byte, 1)
foo := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(0xfffffffe)))
fmt.Print(*foo + 1)
}
// [signal SIGSEGV: segmentation violation code=0x1 addr=0xc100068f55 pc=0x49142b]
- slice在作为函数入参时,函数内对slice的修改可能会影响原始数据
// bad
// slice作为函数入参时包含原始数组指针
func modify(array []int) {
array[0] = 10 // 对入参slice的元素修改会影响原始数据
}
func main() {
array := []int{1, 2, 3, 4, 5}
modify(array)
fmt.Println(array) // output:[10 2 3 4 5]
}
// good
// 数组作为函数入参,而不是slice
func modify(array [5]int) {
array[0] = 10
}
func main() {
// 传入数组,注意数组与slice的区别
array := [5]int{1, 2, 3, 4, 5}
modify(array)
fmt.Println(array)
}
- 在进行文件操作时,如果对外部传入的文件名未做限制,可能导致任意文件读取或者任意文件写入,严重可能导致代码执行。
// bad: 任意文件读取
func handler(w http.ResponseWriter, r *http.Request) {
path := r.URL.Query()["path"][0]
// 未过滤文件路径,可能导致任意文件读取
data, _ := ioutil.ReadFile(path)
w.Write(data)
// 对外部传入的文件名变量,还需要验证是否存在../等路径穿越的文件名
data, _ = ioutil.ReadFile(filepath.Join("/home/user/", path))
w.Write(data)
}
// bad: 任意文件写入
func unzip(f string) {
r, _ := zip.OpenReader(f)
for _, f := range r.File {
p, _ := filepath.Abs(f.Name)
// 未验证压缩文件名,可能导致../等路径穿越,任意文件路径写入
ioutil.WriteFile(p, []byte("present"), 0640)
}
}
// good: 检查压缩的文件名是否包含..路径穿越特征字符,防止任意写入
func unzipGood(f string) bool {
r, err := zip.OpenReader(f)
if err != nil {
fmt.Println("read zip file fail")
return false
}
for _, f := range r.File {
if !strings.Contains(f.Name, "..") {
p, _ := filepath.Abs(f.Name)
ioutil.WriteFile(p, []byte("present"), 0640)
} else {
return false
}
}
return true
}
- 根据创建文件的敏感性设置不同级别的访问权限,以防止敏感数据被任意权限用户读取。例如,设置文件权限为:
-rw-r-----
ioutil.WriteFile(p, []byte("present"), 0640)
- 使用
exec.Command
、exec.CommandContext
、syscall.StartProcess
、os.StartProcess
等函数时,第一个参数(path)直接取外部输入值时,应使用白名单限定可执行的命令范围,不允许传入bash
、cmd
、sh
等命令; - 使用
exec.Command
、exec.CommandContext
等函数时,通过bash
、cmd
、sh
等创建shell,-c后的参数(arg)拼接外部输入,应过滤\n $ & ; | ' " ( ) `等潜在恶意字符;
// bad
func foo() {
userInputedVal := "&& echo 'hello'" // 假设外部传入该变量值
cmdName := "ping " + userInputedVal
// 未判断外部输入是否存在命令注入字符,结合sh可造成命令注入
cmd := exec.Command("sh", "-c", cmdName)
output, _ := cmd.CombinedOutput()
fmt.Println(string(output))
cmdName := "ls"
// 未判断外部输入是否是预期命令
cmd := exec.Command(cmdName)
output, _ := cmd.CombinedOutput()
fmt.Println(string(output))
}
// good
func checkIllegal(cmdName string) bool {
if strings.Contains(cmdName, "&") || strings.Contains(cmdName, "|") || strings.Contains(cmdName, ";") ||
strings.Contains(cmdName, "$") || strings.Contains(cmdName, "'") || strings.Contains(cmdName, "`") ||
strings.Contains(cmdName, "(") || strings.Contains(cmdName, ")") || strings.Contains(cmdName, "\"") {
return true
}
return false
}
func main() {
userInputedVal := "&& echo 'hello'"
cmdName := "ping " + userInputedVal
if checkIllegal(cmdName) { // 检查传给sh的命令是否有特殊字符
return // 存在特殊字符直接return
}
cmd := exec.Command("sh", "-c", cmdName)
output, _ := cmd.CombinedOutput()
fmt.Println(string(output))
}
- 明文传输的通信协议目前已被验证存在较大安全风险,被中间人劫持后可能导致许多安全风险,因此必须采用至少TLS的安全通信方式保证通信安全,例如gRPC/Websocket都使用TLS1.3。
// good
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
w.Write([]byte("This is an example server.\n"))
})
// 服务器配置证书与私钥
log.Fatal(http.ListenAndServeTLS(":443", "yourCert.pem", "yourKey.pem", nil))
}
- TLS证书应当是有效的、未过期的,且配置正确的域名,生产环境的服务端应启用证书验证。
// bad
import (
"crypto/tls"
"net/http"
)
func doAuthReq(authReq *http.Request) *http.Response {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
res, _ := client.Do(authReq)
return res
}
// good
import (
"crypto/tls"
"net/http"
)
func doAuthReq(authReq *http.Request) *http.Response {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
}
client := &http.Client{Transport: tr}
res, _ := client.Do(authReq)
return res
}
- 禁止将敏感信息硬编码在程序中,既可能会将敏感信息暴露给攻击者,也会增加代码管理和维护的难度
- 使用配置中心系统统一托管密钥等敏感信息
- 只输出必要的最小数据集,避免多余字段暴露引起敏感信息泄露
- 不能在日志保存密码(包括明文密码和密文密码)、密钥和其它敏感信息
- 对于必须输出的敏感信息,必须进行合理脱敏展示
- 避免通过GET方法、代码注释、自动填充、缓存等方式泄露敏感信息
// bad
func serve() {
http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
user := r.Form.Get("user")
pw := r.Form.Get("password")
log.Printf("Registering new user %s with password %s.\n", user, pw)
})
http.ListenAndServe(":80", nil)
}
// good
func serve1() {
http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
user := r.Form.Get("user")
pw := r.Form.Get("password")
log.Printf("Registering new user %s.\n", user)
// ...
use(pw)
})
http.ListenAndServe(":80", nil)
}
- 敏感数据应使用SHA2、RSA等算法进行加密存储
- 敏感数据应使用独立的存储层,并在访问层开启访问控制
- 包含敏感信息的临时文件或缓存一旦不再需要应立刻删除
- 应合理使用panic、recover、defer处理系统异常,避免出错信息输出到前端
defer func () {
if r := recover(); r != nil {
fmt.Println("Recovered in start()")
}
}()
- 对外环境禁止开启debug模式,或将程序运行日志输出到前端
// bad
dlv --listen=:2345 --headless=true --api-version=2 debug test.go
// good
dlv debug test.go
- 在进行用户登陆,加解密算法等操作时,不得在代码里硬编码密钥或密码,可通过变换算法或者配置等方式设置密码或者密钥。
// bad
const (
user = "dbuser"
password = "s3cretp4ssword"
)
func connect() *sql.DB {
connStr := fmt.Sprintf("postgres://%s:%s@localhost/pqgotest", user, password)
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil
}
return db
}
// bad
var (
commonkey = []byte("0123456789abcdef")
)
func AesEncrypt(plaintext string) (string, error) {
block, err := aes.NewCipher(commonkey)
if err != nil {
return "", err
}
}
- 在使用对称密码算法时,需要保护好加密密钥。当算法涉及敏感、业务数据时,可通过非对称算法协商加密密钥。其他较为不敏感的数据加密,可以通过变换算法等方式保护密钥。
- 在使用加密算法时,不建议使用加密强度较弱的算法。
// bad
crypto/des,crypto/md5,crypto/sha1,crypto/rc4等。
// good
crypto/rsa,crypto/aes等。
- 正则表达式编写不恰当可被用于DoS攻击,造成服务不可用,推荐使用regexp包进行正则表达式匹配。regexp保证了线性时间性能和优雅的失败:对解析器、编译器和执行引擎都进行了内存限制。但regexp不支持以下正则表达式特性,如业务依赖这些特性,则regexp不适合使用。
- 回溯引用Backreferences
- 查看Lookaround
// good
matched, err := regexp.MatchString(`a.b`, "aaxbb")
fmt.Println(matched) // true
fmt.Println(err) // nil
- 所有外部输入的参数,应使用
validator
进行白名单校验,校验内容包括但不限于数据长度、数据范围、数据类型与格式,校验不通过的应当拒绝
// good
import (
"fmt"
"github.com/go-playground/validator/v10"
)
var validate *validator.Validate
func validateVariable() {
myEmail := "[email protected]"
errs := validate.Var(myEmail, "required,email")
if errs != nil {
fmt.Println(errs)
return
//停止执行
}
// 验证通过,继续执行
...
}
func main() {
validate = validator.New()
validateVariable()
}
- 无法通过白名单校验的应使用
html.EscapeString
、text/template
或bluemonday
对<, >, &, ',"
等字符进行过滤或编码
import (
"text/template"
)
// TestHTMLEscapeString HTML特殊字符转义
func main(inputValue string) string {
escapedResult := template.HTMLEscapeString(inputValue)
return escapedResult
}
- 使用
database/sql
的prepare、Query或使用GORM等ORM执行SQL操作
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
type Product struct {
gorm.Model
Code string
Price uint
}
...
var product Product
...
db.First(&product, 1)
- 使用参数化查询,禁止拼接SQL语句,另外对于传入参数用于order by或表名的需要通过校验
// bad
import (
"database/sql"
"fmt"
"net/http"
)
func handler(db *sql.DB, req *http.Request) {
q := fmt.Sprintf("SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='%s' ORDER BY PRICE",
req.URL.Query()["category"])
db.Query(q)
}
// good
func handlerGood(db *sql.DB, req *http.Request) {
// 使用?占位符
q := "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='?' ORDER BY PRICE"
db.Query(q, req.URL.Query()["category"])
}
-
使用
"net/http"
下的方法http.Get(url)
、http.Post(url, contentType, body)
、http.Head(url)
、http.PostForm(url, data)
、http.Do(req)
时,如变量值外部可控(指从参数中动态获取),应对请求目标进行严格的安全校验。 -
如请求资源域名归属固定的范围,如只允许
a.qq.com
和b.qq.com
,应做白名单限制。如不适用白名单,则推荐的校验逻辑步骤是:-
第 1 步、只允许HTTP或HTTPS协议
-
第 2 步、解析目标URL,获取其HOST
-
第 3 步、解析HOST,获取HOST指向的IP地址转换成Long型
-
第 4 步、检查IP地址是否为内网IP,网段有:
// 以RFC定义的专有网络为例,如有自定义私有网段亦应加入禁止访问列表。 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8
-
第 5 步、请求URL
-
第 6 步、如有跳转,跳转后执行1,否则绑定经校验的ip和域名,对URL发起请求
-
-
官方库
encoding/xml
不支持外部实体引用,使用该库可避免xxe漏洞
import (
"encoding/xml"
"fmt"
"os"
)
func main() {
type Person struct {
XMLName xml.Name `xml:"person"`
Id int `xml:"id,attr"`
UserName string `xml:"name>first"`
Comment string `xml:",comment"`
}
v := &Person{Id: 13, UserName: "John"}
v.Comment = " Need more details. "
enc := xml.NewEncoder(os.Stdout)
enc.Indent(" ", " ")
if err := enc.Encode(v); err != nil {
fmt.Printf("error: %v\n", err)
}
}
- 使用
text/template
或者html/template
渲染模板时禁止将外部输入参数引入模板,或仅允许引入白名单内字符。
// bad
func handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
x := r.Form.Get("name")
var tmpl = `<!DOCTYPE html><html><body>
<form action="/" method="post">
First name:<br>
<input type="text" name="name" value="">
<input type="submit" value="Submit">
</form><p>` + x + ` </p></body></html>`
t := template.New("main")
t, _ = t.Parse(tmpl)
t.Execute(w, "Hello")
}
// good
import (
"fmt"
"github.com/go-playground/validator/v10"
)
var validate *validator.Validate
validate = validator.New()
func validateVariable(val) {
errs := validate.Var(val, "gte=1,lte=100") // 限制必须是1-100的正整数
if errs != nil {
fmt.Println(errs)
return false
}
return true
}
func handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
x := r.Form.Get("name")
if validateVariable(x) {
var tmpl = `<!DOCTYPE html><html><body>
<form action="/" method="post">
First name:<br>
<input type="text" name="name" value="">
<input type="submit" value="Submit">
</form><p>` + x + ` </p></body></html>`
t := template.New("main")
t, _ = t.Parse(tmpl)
t.Execute(w, "Hello")
} else {
// ...
}
}
- CORS请求保护不当可导致敏感信息泄漏,因此应当严格设置Access-Control-Allow-Origin使用同源策略进行保护。
// good
c := cors.New(cors.Options{
AllowedOrigins: []string{"http://qq.com", "https://qq.com"},
AllowCredentials: true,
Debug: false,
})
// 引入中间件
handler = c.Handler(handler)
- 响应头Content-Type与实际响应内容,应保持一致。如:API响应数据类型是json,则响应头使用
application/json
;若为xml,则设置为text/xml
。
- 所有接口、页面,添加响应头
X-Content-Type-Options: nosniff
。 - 所有接口、页面,添加响应头
X-Frame-Options
。按需合理设置其允许范围,包括:DENY
、SAMEORIGIN
、ALLOW-FROM origin
。用法参考:MDN文档
- 应尽量避免外部可控参数拼接到HTTP响应头中,如业务需要则需要过滤掉
\r
、\n
等换行符,或者拒绝携带换行符号的外部输入。
- 直出html页面或使用模板生成html页面的,推荐使用
text/template
自动编码,或者使用html.EscapeString
或text/template
对<, >, &, ',"
等字符进行编码。
import (
"html/template"
)
func outtemplate(w http.ResponseWriter, r *http.Request) {
param1 := r.URL.Query().Get("param1")
tmpl := template.New("hello")
tmpl, _ = tmpl.Parse(`{{define "T"}}{{.}}{{end}}`)
tmpl.ExecuteTemplate(w, "T", param1)
}
- 用户登录时应重新生成session,退出登录后应清理session。
import (
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"net/http"
)
// 创建cookie
func setToken(res http.ResponseWriter, req *http.Request) {
expireToken := time.Now().Add(time.Minute * 30).Unix()
expireCookie := time.Now().Add(time.Minute * 30)
//...
cookie := http.Cookie{
Name: "Auth",
Value: signedToken,
Expires: expireCookie, // 过期失效
HttpOnly: true,
Path: "/",
Domain: "127.0.0.1",
Secure: true,
}
http.SetCookie(res, &cookie)
http.Redirect(res, req, "/profile", 307)
}
// 删除cookie
func logout(res http.ResponseWriter, req *http.Request) {
deleteCookie := http.Cookie{
Name: "Auth",
Value: "none",
Expires: time.Now(),
}
http.SetCookie(res, &deleteCookie)
return
}
- 涉及系统敏感操作或可读取敏感信息的接口应校验
Referer
或添加csrf_token
。
// good
import (
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"net/http"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/signup", ShowSignupForm)
r.HandleFunc("/signup/post", SubmitSignupForm)
// 使用csrf_token验证
http.ListenAndServe(":8000",
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
}
-
除非资源完全可对外开放,否则系统默认进行身份认证,使用白名单的方式放开不需要认证的接口或页面。
-
根据资源的机密程度和用户角色,以最小权限原则,设置不同级别的权限,如完全公开、登录可读、登录可写、特定用户可读、特定用户可写等
-
涉及用户自身相关的数据的读写必须验证登录态用户身份及其权限,避免越权操作
-- 伪代码 select id from table where id=:id and userid=session.userid
-
没有独立账号体系的外网服务使用
QQ
或微信
登录,内网服务使用统一登录服务
登录,其他使用账号密码登录的服务需要增加验证码等二次验证
- 在循环中启动协程,当协程中使用到了循环的索引值,由于多个协程同时使用同一个变量会产生数据竞争,造成执行结果异常。
// bad
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var group sync.WaitGroup
for i := 0; i < 5; i++ {
group.Add(1)
go func() {
defer group.Done()
fmt.Printf("%-2d", i) // 这里打印的i不是所期望的
}()
}
group.Wait()
}
// good
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var group sync.WaitGroup
for i := 0; i < 5; i++ {
group.Add(1)
go func(j int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in start()")
}
group.Done()
}()
fmt.Printf("%-2d", j) // 闭包内部使用局部变量
}(i) // 把循环变量显式地传给协程
}
group.Wait()
}
- 并发写map容易造成程序崩溃并异常退出,建议加锁保护
// bad
func main() {
m := make(map[int]int)
// 并发读写
go func() {
for {
_ = m[1]
}
}()
go func() {
for {
m[2] = 1
}
}()
select {}
}
敏感操作如果未作并发安全限制,可导致数据读写异常,造成业务逻辑限制被绕过。可通过同步锁或者原子操作进行防护。
通过同步锁共享内存
// good
var count int
func Count(lock *sync.Mutex) {
lock.Lock() // 加写锁
count++
fmt.Println(count)
lock.Unlock() // 解写锁,任何一个Lock()或RLock()均需要保证对应有Unlock()或RUnlock()
}
func main() {
lock := &sync.Mutex{}
for i := 0; i < 10; i++ {
go Count(lock) // 传递指针是为了防止函数内的锁和调用锁不一致
}
for {
lock.Lock()
c := count
lock.Unlock()
runtime.Gosched() // 交出时间片给协程
if c > 10 {
break
}
}
}
- 使用
sync/atomic
执行原子操作
// good
import (
"sync"
"sync/atomic"
)
func main() {
type Map map[string]string
var m atomic.Value
m.Store(make(Map))
var mu sync.Mutex // used only by writers
read := func(key string) (val string) {
m1 := m.Load().(Map)
return m1[key]
}
insert := func(key, val string) {
mu.Lock() // 与潜在写入同步
defer mu.Unlock()
m1 := m.Load().(Map) // 导入struct当前数据
m2 := make(Map) // 创建新值
for k, v := range m1 {
m2[k] = v
}
m2[key] = val
m.Store(m2) // 用新的替代当前对象
}
_, _ = read, insert
}
go 语言本身在代码规范性这方面也做了很多努力,很多限制都是强制语法要求,例如左大括号不换行,引用的包或者定义的变量不使用会报错,此外 go 还是提供了很多好用的工具帮助我们进行代码的规范。
gofmt
,大部分的格式问题可以通过gofmt
解决,gofmt
自动格式化代码,保证所有的 go 代码与官方推荐的格式保持一致,于是所有格式有关问题,都以gofmt
的结果为准。goimports
,此工具在gofmt
的基础上增加了自动删除和引入包。go vet
,vet
工具可以帮我们静态分析我们的源码存在的各种问题,例如多余的代码,提前return
的逻辑,struct
的tag
是否符合标准等。编译前先执行代码静态分析。golint
,类似javascript
中的jslint
的工具,主要功能就是检测代码中不规范的地方。