高质量编程简介及编码规范
如何编写简洁清晰的代码
高质量
1. 正确可靠: 对于一些异常我们能够有相对应的处理,保证程序的稳定运行
2. 简洁清晰: 他人阅读自己的代码的时候能够简单易懂
原则
1. 简单性: 消除`复杂性的代码`、对于代码出现问题,可能我们就无法更好地定位问题所在 (增加了问题解决的周期) 以及对于自己的改动所造成的影响
2. 可读性: 如果写出了那种自己日后也不能理解的代码这对于别人或是自己都是无益处的
Note: 过于复杂可能会导致别人无法正确理解代码的含义,不能进行维护
总之就是: 防止别人到时候不能更好地接你的锅(误
益处
无论是对自己还是对于团队而言,日后我们都能更好地维护代码、增加自己或是团队的工作效率
编码规范
代码格式
1. 我们可以使用go自带的格式化工具对我们的代码进行格式, 在 Visual Studio Code 中设定保存后自动对代码进行格式化
2. Go 还提供了goimports工具实现 gofmt + 依赖管理 的功能,包括包引用的增删和依赖排序
我们可以使用Go的帮助命令查看关于gofmt的具体细节
go help fmt
查看更多的具体使用细节
注释
公共符号一定要进行注释
1. Package 中公开的公共符号(即以大写字母开头), 如 常量 变量 函数 以及 结构体这些
2. 任何不明显和简短的功能
3. 任何函数需要进行一个注释
除了实现接口中的方法除外
注释需要做到的内容
1. 解释代码作用
2. 解释代码的实现方式
3. 解释该代码这样实现的原因(解释代码的外部因素,提供上下文)
4. 可能会出现错误的情况(代码的限制条件)
Note: 注释应该可以表达出上下文的相关信息
命名规范
variable
1. 简洁
2. 缩略词进行大写(前提是我们需要对外公开\暴露的时候)
3. 我们使用的变量距离声明较远时,需要添加额外的上下文信息(全局变量等)
//Bad
for index:=0; index < len(s);index++ {
}
//Good
for i:=0; i < len(s);i++ {
}
// t 会使得原本的信息量减少, 而 deadline 可以让我们更好地理解为该时间为一个截至时间
//Good
func (c *Client) send(req *Request, deadling time.Time)
//Bad
func (c *Client) send(req *Request, t time.Time)
function
1. 函数名不需要携带package的上下文信息(我们使用function一般于package同时使用)
2. 简短
3. 当某一个 package foo 返回的类型为 Foo 是我们可以对类型信息进行忽略(防止歧义的出现),如果为其它类型则需要进行类型信息的添加
for example
当我们为http包中的一个创建 server 的函数进行命名是我采用的是 1 因为实际使用中采用的是 http.Server() 这样的一种形式,其中已经包含了http信息,我们就无需采用 http.ServerHTTP()这种形式,造成冗余
//1
func Server(l net.Listener, handler Handler) error
//2
func ServerHTTP(l net.Listener, handler Handler) error
package
1. 小写字母组成,不包含大写字母以及下划线
2. 包含简短的上下文信息(schema task etc..)
3. 尽量避免与标准库产生冲突
4. 避免使用常量名作为 package name(usage bufio > buf)
5. 使用单数
6. 缩写谨慎使用(在不失去含义的前提下)
减少阅读理解代码的成本,设计简洁的的名称,在不失去上下文信息的前提下
控制流程
避免嵌套,保证流程的清晰
当两个流程中都存在 return 语句时我们可以省略 else
// Bad
if foo {
return x
}else {
return nil
}
// Good
if foo {
return x
}
return nil
保持正常代码路径为最小缩进
优先处理特殊/错误情况,这样使得我们的函数可能尽早返回,避免接下来的的一些循环以及嵌套的情况
// Bad
func OneFunc() error {
err := doSomeThing()
if err == nil {
err := doAnotherThing() // 总感觉有错,这一句,前面不是已经声明了嘛!!
if err == nil {
return nil
}
return err
}
return err
}
// Good
func OneFunc() error {
if err := doSomeThing();err == nil {
return err
}
if err:= doAnotherThing(); err == nil {
return err
}
return nil
}
尽量走直线,避免嵌套,提升可读性
错误和异常处理
简单错误
1. 出现一次的错误 & 其它地方无需捕获该错误
2. 尽量使用标准库中的 errors.New() 进行处理
3. 需要对错误进行格式化时使用 fmt.Errorf()
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(vis) >= 10 {
return errors.New("stoped after 10 redirects")
}
return nil
}
错误的 Wrap & UnWrap
1. 错误的 Wrap 实际上提供了一个 error 嵌套另一个error, 产生一个错误的链
2. 在 fmt package 中的Errorf 我们可以使用 %w 来产生一个错误链
list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
if err != nil {
return fmt.Errorf("reading srcfiles list: %w",err)
}
错误判定
1. 判定一个错误是否为特定的错误,可以使用errors.ls
2. 可以判断错误链上的是否存在特定的错误(这是 == 无法达到)
data, err := lockeedfile.Read(targ)
if erors.Is(err, fs.ErrNotExist) {
return []byte{},nil
}
return data,nil
// usage errors.As 获取(取出)信息
if _,err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed path:", pathError.Path)
}else {
fmt.Println(err)
}
}
panic
1. 不建议在义务代码中使用panic
2. 函数调用不包含 recover 会造成程序崩溃
3. 问题可以屏蔽或者解决,使用error
4. 在程序启动阶段,产生不可逆转错误时,使用panic(在 main 或者 init 中)
recover
1. 在被 defer 的函数中使用
2. 不能嵌套
3. 只在当前的协程有效
4. defer 是以栈的形式组织(先进后出)
当我们需要更多的上下文信息时,可以在 recover 后使用 log 进行记录当前栈
func (t *treeFS) Open(name string) (f fs.File, err error){
defer func() {
if e:= recover(); e != nil {
f = nil
err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
}
}()
}
常用的Go程序优化方法
1. 性能优化的前提是保证程序的可靠性,正确性以及简洁清晰等因素
2. 性能优化采用的综合评估,所以在一些方面可能是时间效率较高或是空间效率较高,这两者存在对立的情况,在某些时候
性能表现需要通过实际数据进行衡量 我们可以使用Go提供的 benchmark 进行基准测试
go test -bench=. -benchmem
slice 预分配内存
尽可能使用make() 初始化slice 并提供 Size
func NoPreAlloc(size int){
date := make([]int, 0)
for k:=0; k<size; k++ {
data = append(data,k)
}
}
func PreAlloc(size int){
date := make([]int, size)
for k:=0; k<size; k++ {
data = append(data,k)
}
}
slice 是对数组片段的描述 它的大致结构是
type slice struct {
array unsafe.Pointer
len int
cap int
}
以下情况中在已有的slice上创建新的slice不会对底层的 array 进行重新的创建
1. 原 slice 较大, 新创建的 slice 较小
2. 存在其它的引用,不可释放
3. 但我们可以采用 copy 对原 slice 复制
fucn GetLasBySlice(origin []int) []int {
return origin[len(origin) - 2]
}
fucn GetLasBySlice(origin []int) []int {
result := make(origin []int, 2)
copy(result, origin[len(origin)-2])
return result
}
Map 预分配内存
func NoPreAlloc(size int){
data:=make(map[int]int)
for i:=0;i<size; i++{
data[i] = 1
}
}
func PreAlloc(size int){
data:=make(map[int]int, size)
for i:=0;i<size; i++{
data[i] = 1
}
}
使用append时如果原有的容量不够,添加元素时会不断进行扩容,增加了 copy 和 Rehash 时间
使用 strings.Builder
func Plus(n int, str string){
s := ""
for i:= 0;i < n; i++{
s += str
}
return str
}
func StrBuiler(n int, str string){
var builder strings.Builder
for i:= 0;i < n; i++{
builder.WriteString(str)
}
return builder.String()
}