暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Go语言开发规范(更新)

拖地先生 2019-11-15
425

代码风格是规范代码的共同约定,本规范旨在描述编写Go代码时的注意事项,通过这些规范的定义以提高代码的可维护性及工程师的心智负担,也可高效的使用Go的特性。

规范第一版参见《Go语言开发规范》,第二版补充了大量代码风格规范,并加入了项目工程结构建议及说明。

规范基于Go的通用准则,并加入了大型开源项目如k8s等的规范。依赖的外部准则:

  • Effecitve Go(https://golang.org/doc/effective_go.html)

  • The Go Common mistakes Guide (https://github.com/golang/go/wiki/CodeReviewComments)

  • Uber Go Style(https://github.com/uber-go/guide/blob/master/style.md)

所有代码都应该通过 golint
  go vet
 检查,建议在IDE中设置:

  • 保存时自动运行goimports

  • 自动运行 golint
     和 go vet
     来检查错误

风格规范中 【强制】
 是必须遵循的规范,【建议】
则是建议遵循的规范

代码风格

基本格式

  • 【强制】代码最大行宽为120列,超过换行

  • 【强制】代码提交必须经过 goimports
    格式化

声明&命名

  • 【强制】变量、函数名命名必须统一采用规范的英文命名;禁止采用拼音等方式;禁止以特殊字符作为命名的开始或结束

  • 【强制】变量、函数、包内非导出的结构体、方法使用小驼峰
    的命名方法

  • 【强制】常量、包内导出的结构体、方法、Interface使用大驼峰
    的命名方法

  • 【强制】包名、包引用别名使用全小写
    的命名方法,尽量使用短命名,不能使用下划线、中划线等字符;文件夹必须与包名保持一致;请注意以下事项:

    • 在大多数情况下引入包不需要去重命名

    • 简单明了,命名需要能够在被导入的地方准确识别

    • 不要使用复数。例如,net/url
      , 而不是 net/urls

    • 不要使用common
      util
      shared
      lib
      之类的,这些都是不好的、表达信息不明的名称

      参考:PackageNames(https://blog.golang.org/package-names)Style guideline for Go Packages ( https://rakyll.org/style-packages/ )

  • 【强制】如果包的名称与导入路径的最后一个元素不匹配,那必须使用导入别名;在其他情况下,除非导入的包名之间有直接冲突,否则避免使用导入别名

  • 【强制】在结构体中,专有名词使用全大写
    的命名方法,不能使用其他命名方法;常见的专有名词如:API、HTTP、URL、ID、CPU、CSS、EOF、IP、JSON、TTL、UID、URI、UTF8、XML等

  • 【建议】声明局部变量时需要明确设值,应使用短变量声明形式:=

  • 【强制】array、map、channel 声明时使用make

  • 【强制】将相似的声明进行分组;不相关的声明不要放在同一组内

    // Bad
    import "a"
    import "b"
    // Good
    import (
    "a"
    "b"
    )

    // Bad
    const A1 = "A"
    const B1 = "B"
    // Good
    const (
    A1 = "A"
    B1 = "B"
    )

    // Bad
    var a = 1
    var b = 2
    // Good
    var (
    a = 1
    b = 2
    )

    // Bad
    const (
    Add Operation = iota + 1
    Subtract
    Multiply
    ENV = "MY_ENV"
    )
    // Good
    const (
    Add Operation = iota + 1
    Subtract
    Multiply
    )

    const ENV = "MY_ENV"

  • 【强制】枚举、自定义类型的定义及声明

    type OrderStatusInt int32

    var OrderStatus = struct {
    WaitPayment OrderStatusInt
    Success OrderStatusInt
    Failed OrderStatusInt
    } {
    WaitPayment: 1,
    Success: 2,
    Failed: 3,
    }

函数

分组及排布

  • 【强制】导出的函数必须排布在文件首,并在 struct、const、var定义之后

  • 【强制】newX()、NewX() 之类的函数必须排布在声明类型之后,具有接收器的其余方法之前

  • 【强制】普通工具函数必须排布在文件末尾

    // Bad
    func (s *something) Cost() {
    return calcCost(s.weights)
    }

    type something struct{...}

    func calcCost(n int) int {...}

    func (s *something) Stop() {...}

    func newSomething() *something {
    return &something{}
    }

    // Good
    type something struct{...}

    func newSomething() *something {
    return &something{}
    }

    func (s *something) Cost() {
    return calcCost(s.weights)
    }

    func (s *something) Stop() {...}

    func calcCost(n int) int {...}

循环

  • 【建议】代码应通过尽可能地先处理错误情况、特殊情况,并及早返回或继续下一循环来减少嵌套,尽量减少嵌套与多个级别的代码数量

    // bad
    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)
    }
    }

    // good
    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()
    }

  • 【建议】如果一个变量在if的两个分支都设置了,那应该使用单个if

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

    // good
    a := 10
    if b {
    a = 100
    }

  • 【强制】for循环时采用简短声明建立局部变量

    // bad
    var i int
    for i = 0; i < 5; i++ {
    codeUsing(i)
    }

    // good
    for i := 0; i < 5; i++ {
    codeUsing(i)
    }

  • 【强制】对于遍历数据的场景,如果只使用第一项,可以直接丢弃第二项

    // bad
    for k, _ := range mapper {
    codeUsing(k)
    }

    // good
    for k := range mapper {
    codeUsing(k)
    }

  • 【强制】switch 必须添加 default
     处理;使用多个case
    时,多个case写在一起

参数

  • 【建议】函数调用时尽量避免裸参数;裸参数会降低代码可读性,所以当参数名称含义不明显时,请为参数添加/*.*/
    注释

    func printInfo(name string, isLocal, done bool) {
    ...
    }

    // bad
    printInfo("foo", true, true)

    // good
    printInfo("foo", true /* isLocal */, true /* done */)

    这个例子更好的做法是将bool类型替换为自定义类型,从而使代码更易读且类型安全。

  • 【强制】对于大量的(大于4个时)入参,使用结构体进行封装,并通过指针传递

  • 【强制】对于map、slice、channel、interface的入参,不要使用指针进行传递

  • 【强制】context.Context
     参数不能放在结构体中,且必须作为函数的第一个参数进行传递

  • 【建议】尽量避免使用函数命名返回值

    // bad
    func foo(a, b int) (c string, ok bool) {
    c = "hello world"
    ok = true
    return c, ok
    }

    // good
    func foo(a, b int) (string, bool) {
    c := "hello world"
    ok := true
    return c, ok
    }

其他

  • 【建议】尽量缩小变量作用范围,除非这样与减少嵌套的规则冲突;如果需要在if之外使用函数调用的结构,则不应该尝试缩小范围

    // bad
    err := ioutil.WriteFile(name, data, 0644)
    if err != nil {
    return err
    }

    // good
    if err := ioutil.WriteFile(name, data, 0644); err != nil {
    return err
    }

  • 【强制】引用包时不能使用省略包名的方式

    // bad
    import . "fmt"
    Println("Hello world")

    // good
    import "fmt"
    fmt.Println("Hello world")

结构体

  • 【强制】嵌入式类型应该放置在结构体字段列表的顶部,并且必须以空行与常规字段隔开

    // bad
    type Client struct {
    version int
    http.Client
    }

    // good
    type Client struct {
    http.Client

    version int
    }

  • 【强制】初始化结构体时,必须指定字段名称

    // bad
    u := User{"John", "Doe", true}

    // good
    u := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
    }

  • 【强制】在初始化结构引用时,使用 &T{}
     而非new(T)
    ,以使其与结构体初始化方式保持一致

  • 【建议】接收者名称统一采用1-3个字母,不宜太长

代码指南

  • 【强制】不应该明确返回长度为零的切片,应该直接返回 nil

  • 【强制】检查切片是否为空,始终使用len(s) == 0
    ,不要与 nil 比较来检查

  • 【建议】零值切片可直接使用,无需调用make创建

  • 【强制】使用原始字符串字面值,避免使用转义

    // bad
    wantError := "unknown name:\"test\""

    // good
    wantError := `unknown name: "test"`

  • 【建议】格式化字符串放在printf
    外部,并将其设置为const常量

    // bad
    msg := "unexpected values %v, %v\n"
    fmt.Printf(msg, 1, 2)

    // good
    const Msg = "Unexpected Values %v, %v\n"
    fmt.Printf(msg, 1, 2)

  • 【建议】不需要一个指向接口的指针,应该直接将接口作为值传递,因为接口底层数据就是指针;如果你需要接口方法来修改这些底层数据,那必须使用指针

  • 【建议】零值的 sync.Mutex
    sync.RWMutex
    是有效的,所以不需要一个指向Mutex的指针;如果希望通过指针操作结构体,Mutex可以作为其非指针结构体字段,或者做好直接嵌入结构体中

    // bad
    mu := new(sync.Mutex)
    mu.Lock()

    // good
    var mu sync.Mutex
    mu.Lock()

    // good
    type smap struct {
    sync.Mutex

    data map[string]string
    }

    func (m *smap) Get(k string) string {
    m.Lock
    defer m.Unlock()

    return m.data[k]
    }

    // good
    type SMap struct {
    mu sync.Mutex

    data map[string]string
    }

    fun (m *SMap) Get(k string) string {
    m.mu.Lock()
    defer m.mu.Lock()

    return m.data[k]
    }

  • 【强制】使用defer
    来做资源的清理工作,例如文件的关闭、锁的释放等

  • 【建议】枚举变量的值应该从非零值开始

  • 【强制】处理类型断言时,必须使用comma ok
    惯用方法

    // bad
    t := i.(string)

    // good
    t, ok := i.(string)
    if !ok {
    // handle the error
    }

  • 【强制】生产环境代码必须避免panic
    如果发生错误,函数应该返回错误并且允许调用者决定如何处理

  • 【强制】外部通信等重要逻辑的任何异常都必须进行处理,不能使用_
    忽略

    // bad
    actions, _ := rpc.GetActionList(ctx, &proto.ActionListRequest{})

    // good
    actions, err := rpc.GetActionList(ctx, &proto.ActionListRequest{})

代码性能

  • 【建议】strconv 性能优于 fmt

    // bad
    for i := 0; i < b.N; i++ {
    s := fmt.Sprint(rand.Int())
    }

    // good
    for i := 0; i < b.N; i++ {
    s := strconv.Itoa(rand.Int())
    }

  • 【建议】避免 string to byte 的转换;不要反复地从字符串字面量创建byte切片,建议执行一次转换后存储结果供后续使用

    // bad
    for i := 0; i < b.N; i++ {
    w.Write([]byte("Hello world"))
    }

    // good
    data := []byte("Hello world")
    for i := 0; i < b.N; i++ {
    w.Write(data)
    }

代码注释

  • 【强制】大段说明性注释采用 /* ... */
     风格

  • 【强制】单行注释采用 //
     风格

  • 【强制】函数、方法的注释需要以函数或方法的名称作为开头

    // bad
    // 这里直接写注释
    func Foo() {...}

    // good
    // Foo 这里才可以写注释
    func Foo() {...}

  • 【建议】注释的单行列长度最大不能超过120列,超过则必须换行,一般以80列为宜

工程规范

数据库定义

  • 【强制】数据库定义必须使用TableName方法指定表名,不推荐使用engine.Table()

    // bad
    package admin

    type AdminUser struct {
    ID int64 `xorm:"autoincr pk int(11) 'id'"`
    }

    // good
    package admin

    type User struct {
    ID int64 `xorm:"autoincr pk int(11) 'id'"`
    }

    func (User) TableName() string {
    return "admin_user"
    }

  • 【强制】批量执行增删改时必须使用事务进行处理

Prorobuf定义

  • 【强制】.proto
     定义request及response格式必须为XxxRequest
    XxxResponse
    大驼峰
    的命名方式,不能使用其他格式

  • 【强制】pb的定义与生成分别放在不能的文件夹中;pb定义必须放在protobuf
    文件夹中,生成的代码根据不同语言存放在gen/xx
    文件夹中

  • 【强制】pb文件使用proto3
     语法,禁止使用 [json_name='']
    类型的tag

  • 【强制】pb文件头定义必须遵循下面代码的规则,定义项必须都要有

    syntax = "proto3";
    option go_package = "gitlab.xxx.com.cn/h2/svr/gen/golang/proto";
    option php_namespace = "Svr\\Proto";
    option php_metadata_namespace = "Svr\\GpbMetadata";
    package proto;

工程设置

  • 【强制】所有项目必须使用 Go Module
    来管理依赖包,不能选择其他依赖包管理方式,参考《Go Module 在项目中的应用

  • 【强制】所有项目必须使用make
    来进行项目操作,禁止使用shell
    文件进行项目操作

  • 【强制】新工程上线一周前需邮件发送研发中心,说明项目信息,包括但不限于配置详情、是否需要mirror环境、上线时间、数据库等信息,越详细越好




拖地先生,从事互联网技术工作,在这里每周两篇文章,一起聊聊日常的技术点滴和管理心得。

如果对你有帮助,让大家也看看呗~

文章转载自拖地先生,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论