衡量代码质量的唯一标准是阅读该代码时说脏话的次数
一个优秀的团队应当保持一致的代码风格,我们在实践中发现,如果一个项目的代码风格不一致,会导致项目杂乱、不利于跨模块开发、影响开发效率、想骂人等弊端,
在定义我们的编码风格之前,先推荐一本非常好的书籍《clean code》,有时间的可以去看看,磨刀不误砍柴工。
命名基础规则
- 站在调用者的角度,包不是给你自己用的
- 简洁、且见名知义
- 采用通用、大众熟知的缩写命名。比如
buf
而不是bufio
- 如果缩写的名字会产生歧义,则放弃或换个
包名
- 包名与目录名一致
- 大多数使用命名导入的情况下,不需要重命名
少用调用者去起别名,除非名字太烂
- 包名应当全部小写、没有下划线、没有大写字母
- 单数,不用复数
样例:
Bad:
configs
third_party
thirdParty
...
导入包
(1)如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名
import (
client "example.com/client-go"
trace "example.com/trace/v2"
)
建议路径最后一个元素是版本号的都使用别名,避免歧义。
(2)在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名
import (
"net/http/pprof"
gpprof "github.com/google/pprof"
)
(3)如有重名,请保留标准包命名,别名自定义或者第三方包
(4)禁止使用相对路径导入,所有导入路径必须符合 go get 标准
文件名
- 文件名应当保持简单易懂,见名知意
- 文件名应当全小写,不应出现大写字母,不同单词之间使用下划线分割
- 文件名应当避免使用复数形式
样例:
Bad:
configs.yaml
cloudServer.go
cloud-server.go
函数名/方法名
- 函数名/方法名应当保持动词/动宾形式
- 除非是公认的缩写,否则不应使用缩写
- 避免命名过长,长命名并不会使其更具可读性,一份有用的说明文档或者注释通常比额外的长名更有价值
样例:
Bad:
container.createContainer(in)
Good:
container.create(in)
- Go 并不对获取器(getter)和设置器(setter)提供自动支持。针对某个变量或字段,获取器名字无需携带
Get
,设置器名字以Set
开头
note : 若你有个名为 owner (小写,未导出)的字段,其获取器应当名为 Owner(大写,可导出)而非 GetOwner。
样例:
Good:
func Owner() string {
...
}
func SetOwner(ower string) string {
...
}
Bad:
func GetOwner() string {
...
}
- 命名使用英文,避免单词出错
样例:
Bad:
func (u *userRepo) syncDepartmentFormDatabase() {
...
}
From而非Form。
接口名
- 按照约定,只包含一个方法的接口应当以该方法的名称加上 -er 后缀来命名,如 Reader、Writer、Formatter/CloseNotifier 等
type Reader interface {
Read(p []byte) (n int, err error)
}
- 名词用于接口名,动词用于接口的方法名
- 除非包含的方法名与接口名无直接关系,否则都应当使用单个动词
type ContainerRepo interface {
Create() error
Delete() error
...
}
结构体名
- 结构体名严格使用名词形式命名
- 避免使用复数
- 多个单词的使用驼峰命名法
样例:
Good:
type Container struct {}
type ContainerInfo struct {}
Bad:
type ContainerDelete struct {}
type Containers struct {}
type Containerinfo struct {}
常量名
- 如果是枚举类型的常量,需要先创建相应类型
type Scheme string
const (
Http Scheme = "http"
Https Scheme = "https"
)
- 如果模块的功能较为复杂、常量名称容易混淆的情况下,为了更好地区分枚举类型,可以使用完整的前缀
type Symbol string
const (
SymbolAdd Symbol = "+"
SymbolSub Symbol = "-"
)
变量名
- 如果是全局变量,务必使用全称并务必加上注释
// configPath is the absolute path of config file from the command line.
var configPath string
- 如果是局部变量,在相对简单的环境(对象数量少、针对性强)中,可以将一些名称由完整单词简写为单个字母
user 可以简写为 u
userId 可以简写 uid
- 若变量类型为 bool 类型,则名称应以 Has、Is、Can 或 Allow 开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
Error
Error
类型的命名以Error
结尾
type CodeError struct {
message string
}
Error
类型的变量,以Err
开头
var ErrTokenExpired = errors.New("token expired")
- 返回类型为
Error
的变量缩写采用err
func test() {
res, err := container.create(in)
...
}
- 不同类型的error不要使用同一个err接收
样例:
Good:
func (c *container)Update(ctx context.Context,name string) error {
res, listErr := c.list()
if listErr!=nil {
return listErr
}
updateErr := c.update(name)
if updateErr!=nil {
return updateErr
}
...
}
Bad:
func (c *container)Update(ctx context.Context,name string) error {
res, err := c.list()
if err!=nil {
return err
}
_, err = c.update(name)
if err!=nil {
return err
}
...
}
URL
- URL 命名全部小写
- 用正斜杠
/
表明层级关系 - 使用连字符
-
来提高长路径中名称的可读性,不得在 URL 中使用下划线_
- URL 结尾不应包含正斜杠
/
- 文件扩展名不应包含在 URL 中
- URL 需见名知意,但不可暴露服务器架构
Bad:
/GetUserInfo
/photos_path
/My-Folder/my-doc/
/user/user-list
Good:
/user/list
/user/operator-logs
Proto
- service的rpc应参考函数名/方法名的风格
- 在proto文件中,结构体(proto的message)命名风格如下:
message CreateSecurityGroupRequest {}
message CreateSecurityResponse {}
// 除了request和response外,其他的命名风格需保持名词形式
message SecurityGroupInfo{}
enum Policy{}
其他
- 包内容的名字不可以包名开头,因为无需重复包名
举个例子:标准包http
包提供的 HTTP 服务名为 http.Server
,而非 HTTPServer
。用户代码通过 http.Server
引用该类型,因此没有歧义。
package http
type Server struct {}
- 不同包中的类型名可以相同,因为客户端可通过包名区分它们
举个例子:标准库中含有多个名为 Reader 的类型,包括 jpeg.Reader
、 bufio.Reader
和 csv.Reader
。每个包名搭配 Reader 都是个不错的类型名。
package jpeg
type Reader struct {}
package bufio
type Reader struct {}
格式化
我们没有太多可选的余地,因为 Go 已经规范好了,在 Go 世界没有此类战争。
Json/Bson
- 需要特别指出的是,我们在定义结构体时经常需要
id
这个字段,为了简明易懂无歧义,我们规定,在一个结构体中,凡是涉及到id
字段的,都必须要加上前缀,用以区分 - 所有json/bson字段多个单词的都使用下划线格式,不使用驼峰格式
type SecurityGroup struct {
SgID string `json:"sg_id"`
Rule Rule `json:"rule"`
...
}
type Rule struct {
RuleID string `json:"rule_id"`
...
}
空行风格
- 一段代码逻辑处理完后,应当空一行
- if err后应当空一行
样例:
Good:
func Create() error {
if err:=container.create();err!=nil {
return err
}
if err:=store.update();err!=nil {
return err
}
...
}
func check() error {
isExist, err := contaier.check()
if err!=nil {
return err
}
if isExist {
...
}
if err:=store.update();err!=nil {
return err
}
...
}
- 函数内第一行代码不空行
样例:
Bad:
func test() error {
if err:=container.create();err!=nil {
return err
}
...
}
- 建议函数内最后一行的return语句前一行是空行
func test() error {
fmt.Println("test")
return nil
}
import风格
- 遵循我们自定义的import顺序:标准包、第三方包、该项目包,三者之间用空行隔开
import (
"flag"
"os"
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/config"
"github.com/go-kratos/kratos/v2/config/file"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/tracing"
"github.com/go-kratos/kratos/v2/transport/grpc"
"github.com/go-kratos/kratos/v2/transport/http"
"backend/internal/conf"
"backend/internal/data"
)