Go语言:高质量编程规范编码 | 青训营

87 阅读3分钟

Golang代码规范

1、各种边界条件是否考虑完备异常情况处理

2、稳定性保证

3、易读易维护

原则

简单性

消除“多余的复杂性”,以简单清晰的逻辑编写代码

不理解的代码无法修复改进

可读性

代码是写给人看的,而不是机器

编写可维护代码的第一步是确保代码可读

生产力

团队整体工作效率非常重要

代码格式

gofmt

Go 语言官方提供的工具,能自动格式化 Go语言代码为官方统一风格常见DDE都支持方便的配置

注释

1、注释应该解释代码作用

  • 适合注释公共符号

2、注释应该解释代码如何做的

  • 适合注释实现过程

3、注释应该解释代码实现的原因

  • 适合解释代码的外部因素

  • 提供额外上下文

4、注释应该解释代码什么情况会出错

  • 适合解释代码的限制条件

5、公共符号始终要注释

  • 包中声明的每个公共的符号变量、常量、函数以及结构都需要添加注释

  • 任何既不明显也不简短的公共功能必须予以注释

  • 无论长度或复杂程度如何对库中的任何函数都必须进行注释

  • 对于公共符号都有注释说明

  • 尽管 函数 本身没有注释,但它紧跟 函数 结构的声明,明确它的作用

命名规范

variable

1、简洁胜于冗长

2、缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写

  • 例如使用 ServeHTTP 而不是 ServeHttp

  • 使用 XMLHTTPRequest 或者 xmlHTTPRequest

3、变量距离其被使用的地方越远,则需要携带越多的上下文信息

  • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
//i 和 index 的作用域范围仅限于 for 循环内部时index 的额外元长几乎没有增加对于程序的理解 
// Bad 
for index := 0; index < len(s); index++ { 
    // do something 
} 
// Goodfor 
i := 0;i < len(s); i++ { 
    // do something 
}
//将 deadline 替换成 t 降低了变量名的信息量 
//t 常代指任意时间 
//deadline 指截止时间,有特定的含义 
// Good 
func (c *Client) send( reg *Request, deadline time.Time) 
// Bad 
func (c *Client) send( reg *Request, t time.Time)

function

1、函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的

2、函数名尽量简短

3、当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义

4、当名为 foo 的包某个函数返回类型 T 时(T 并不是 Fo),可以在函数名中加入类型信息

package

1、只由小写字母组成。不包含大写字母和下划线等字符

2、简短并包含一定的上下文信息。例如 schema、 task 等

3、不要与标准库同名。例如不要使用 sync 或者 strings

4、以下规则尽量满足,以标准库包名为例

  • 不使用常用变量名作为包名。例如使用 bufio 而不是 buf

  • 使用单数而不是复数。例如使用 encoding 而不是 encodings

  • 谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短

控制流程

线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支

正常流程代码沿着屏幕向下移动

提升代码可维护性和可读性

故障问题大多出现在复杂的条件语句和循环语句中

1、避免嵌套,保持正常流程清晰(减少内容嵌套)

//如果两个分支中都包含return语句,则可以去除几余的else 
//Bad 
if foo { 
    return x 
}else { 
    return nil 
} 
//Good 
if foo { 
    return x 
} 
return nil

2、尽量保持正常代码路径为最小缩进(减少判断嵌套)

//最常见的正常流程的路径被嵌套在两个 if 条件内 
//成功的退出条件是 return nil,必须仔细匹配大括号来发现 
//函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误 
//如果后续正常流程需要增加一步操作,调用新的函数,则又会增加一层嵌套 
//Bad 
func OneFunc() error { 
    err := doSomething() 
    if err == nil { 
        err := doAnotherThing() 
        if err == nil { 
            return nil 
            // normal case 
        } 
        return err 
    } 
    return err 
} 
// Good 
func OneFunc() error { 
    if err := doSomething(); err != nil { 
        return err 
    } 
    if err := doAnotherThing(); err != nil { 
        return err 
    } 
    return nil 
    // normal case 
}

错误和异常处理

1、尽可能提供简明的上下文信息链,方便定位问题error

2、panic 用于真正异常的情况

3、recover 生效范围,在当前 goroutine 的被 defer 的函数中生效

简单错误

1、简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误

2、优先使用 errors.New 来创建匿名变量来直接表示简单错误

3、如果有格式化的需求,使用 fmt.Errorf

func defaultCheckRedirect(reg xRequest, via []*Request) error { 
    if len(via) >= 10 { 
        return errors.New("stopped after 10 redirects") 
    } 
    return nil 
}

错误的 Wrap 和 Unwrap

1、实际上是提供了一个error 嵌套另一个错误的 Wraperror 的能力,从而生成一个 error 的跟踪链

2、在 fmt.Errorf 中使用: % w 关键字来将一个错误关联至错误链中

list, _, err := c.GetBytes(cache.Subkey(a.actionID,"srcfiles" )) 
if err != nil { 
    return fmt.Errorf("reading srcfiles list: %w", err) 
}

错误判定

判定一个错误是否为特定错误,使用 errors.Is

不同于使用 ==,使用该方法可以判定错误链上的所有错误是否含有特定的错误

data, err = lockedfile.Read(targ) 
if errors.Is(err,fs.ErrNotExist) { 
    // Treat non-existent as empty, to bootstrap othe "latest" file 
    // the first time we connect to a given database 
    return []byte{}, nil 
} 
return data,err

在错误链上获取特定种类的错误,使用errors.As

if _,err := os.Open("non-existing"); err != nil { 
    var pathError *fs.PathError 
    if errors.As(err, &pathError) { 
        fmt.Println("Failed at path:", pathError.Path) 
    } else { 
    fmt.PrintIn(err) 
    } 
}

panic

1、不建议在业务代码中使用 panic

2、调用函数不包含 recover 会造成程序崩溃

3、若问题可以被屏蔽或解决,建议使用error 代替 panic

4、当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic

func main() { 
    //.... 
    ctx, cancel := context.WithCancel( context.Background()) 
    client, err := sarama,NewConsumerGroup(strings.Split(brokers,","), group, config) 
    if err != nil { 
        log.Panicf("Error creating consumer group client: %v", err) 
    } 
    //.... 
} 
// Panicf is equivalent to Printf() followed by a call to panic() 
func Panicf(format string, v ...interfacet}) { 
    s := fmt.Sprintf( format, v...) 
    std.Output(2, s) 
    panic(s) 
}

recover

1、recover 只能在被 defer 的函数中使用

2、嵌套无法生效

3、只在当前 goroutine 生效

4、defer 的语句是后进先出

func (s *ss) Token( skipSpace bool, f func(rune) bool) (tok []byte, err error) { 
    defer func() { 
        if e := recover(); e != nil { 
            if se, ok := e.(scanError); ok { 
                err = se.err 
            }else { 
                panic(e) 
            } 
        } 
    }() 
    //.... 
}