Go语言规范:高质量编程及编码规范

一、简介

高质量编程是指以高标准和良好实践来编写可读、可维护、可测试和性能等方面的优秀表现的代码。

  • 各种边界条件是否考虑完备
  • 异常情况处理、稳定性保证
  • 易读易维护

(一)编码原则


从指令的角度考虑,开发中应如何编码,才能减少执行的指令。各种语言特性和语法各不相同,但高质量编程遵循的原则是一致的,如下:

  1. 简洁性:代码应该简洁明了,避免冗余和复杂的逻辑。简洁的代码更易于理解、调试和维护。
  2. 可读性:代码应该易于阅读和理解。使用有意义的变量和函数命名,遵循一致的代码风格,添加适当的注释和文档,以提高代码的可读性。
  3. 一致性:在编写代码时应遵循一致的命名规范、代码风格和代码组织结构。一致的代码风格使得代码更易于理解和维护。
  4. 模块化:将代码划分为模块或函数,每个模块或函数只负责一个明确的任务。模块化的代码更易于测试、重用和维护。
  5. 错误处理:合理处理错误和异常情况。避免忽略错误,而是采取适当的错误处理机制,例如返回错误值或抛出异常。
  6. 依赖管理:使用Go模块管理依赖项,确保代码的可重复性和可维护性。可以使用Go Modules来管理项目的依赖。
  7. 测试:编写测试是保证代码质量的重要手段。编写单元测试和集成测试,覆盖代码的各个功能和边界情况。
  8. 文档:编写清晰、准确的文档,包括代码注释、函数说明和项目文档等。良好的文档可以帮助其他开发人员理解和使用代码。
  9. 并发安全性:在多线程环境中,需要确保代码的并发安全性。可以使用Go语言提供的互斥锁(Mutex)或通道(Channel)等机制来实现。

(二)如何编写高质量的Go代码


1. 代码格式

  • gofmt

gofmt是Go语言官方提供的一个命令行工具,用于格式化Go代码。它会自动调整代码的缩进、空格、括号位置等,以确保代码的一致性和可读性。

在命令行中,可以使用以下命令来运行gofmt工具:

gofmt -w <文件或目录>

其中,-w选项表示将格式化后的代码直接写回源文件,如果不使用-w选项,则gofmt会将格式化后的代码输出到标准输出。

例如,要格式化名为main.go的文件,可以运行以下命令:

gofmt -w main.go

如果要格式化整个项目目录下的所有Go文件,可以运行以下命令:

gofmt -w .

需要注意的是,gofmt工具会直接修改源文件,因此在运行之前,建议先备份代码,以防止意外修改。

此外,还可以使用一些编辑器或IDE中的插件,如GoLand、Visual Studio Code的Go插件等,来自动触发gofmt工具的格式化操作。这样可以在保存文件时自动进行代码格式化,进一步提高开发效率。

  • goimports

goimports也是一个Go语言官方提供的工具,它是在 gofmt的基础上增加了自动导入功能。除了格式化代码外,goimports还会自动检测并添加缺失的导入语句,删除未使用的导入语句,并按照一定的规则对导入语句进行排序并分类。

2. 注释

在Go语言中,注释是用来对代码进行说明和解释的文本。Go语言支持两种类型的注释:单行注释和多行注释。

  • 单行注释:以//开头,用于注释单行代码或单行说明。
// 这是一个单行注释
fmt.Println("Hello, World!") // 打印Hello, World!
  • 多行注释:以/*开头,以*/结尾,用于注释多行代码或多行说明。
/*
这是一个多行注释,
可以跨越多行。
*/
fmt.Println("Hello, World!")
  • 除了用来对代码进行说明,注释还可以用来生成文档。在Go语言中,可以使用特殊格式的注释来生成文档,这种注释被称为文档注释或文档注解。

文档注释以/*开头,以*/结尾,并且在每行注释前添加一个*。文档注释可以包含一些特殊的标记,如@param@return等,用于描述函数的参数和返回值。

/*
calculateSum函数用于计算两个整数的和。

@param a 第一个整数
@param b 第二个整数
@return 两个整数的和
*/
func calculateSum(a, b int) int {
    return a + b
}

可以使用go doc命令来查看代码中的文档注释。

go doc <包名>.<函数名>

例如,要查看calculateSum函数的文档注释,可以运行以下命令:

go doc <包名>.calculateSum

注释是编写清晰、易读的代码的重要组成部分。良好的注释可以帮助其他开发人员理解代码的意图和功能,并且可以用来生成文档以供参考。因此,在编写代码时,建议使用注释来解释和说明代码的逻辑和功能。

3. 命名规范

命名是代码规范中很重要的一部分,统一的命名规则有利于提高的代码的可读性.

Go在命名时以字母a到Z或a到Z或下划线开头,后面跟着零或更多的字母、下划线和数字(0到9)。Go不允许在命名时中使用@、$和%等标点符号。

  • Go是一种区分大小写的编程语言。因此,Manpower和manpower是两个不同的命名。
  • 当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
  • 命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private)
变量命名

和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:

  • 如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient
  • 其它情况都应当使用该名词原有的写法,如 APIClient、repoID、UserID
  • 错误示例:UrlArray,应该写成 urlArray 或者 URLArray
  • 若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
接口命名

命名规则基本和上面的结构体类型。 单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer 。

type Reader interface {
        Read(p []byte) (n int, err error)
}
文件命名

尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。

my_test.go
包命名:package

保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写。

package demo

package main

4. 控制流程

流程控制是每种编程语言控制逻辑走向和执行次序的重要部分。

Go 语言的常用流程控制有 if 和 for,而 switch 和 goto 主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制。

  • Go语言if else(分支结构)

在Go语言中,关键字 if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}括起来的代码块,否则就忽略该代码块继续执行后续的代码。

if condition {    
    // do something
}

如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。

if condition {
    // do something
} else {
    // do something
}

如果存在第三个分支,则可以使用下面这种三个独立分支的形式:

if condition1 {
    // do something
} else if condition2 {
    // do something 
} else {
    // catch-all or default
}

else if 分支的数量是没有限制的,但是为了代码的可读性,还是不要在 if 后面加入太多的 else if 结构,如果必须使用这种形式,则尽可能把先满足的条件放在前面。

  • Go语言switch case语句

表达式不需要为常量,甚至不需要为整数,case 按照从上到下的顺序进行求值,直到找到匹配的项,如果 switch 没有表达式,则对 true 进行匹配。

Go语言改进了 switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行,示例代码如下:

var a = "hello"
switch a {
case "hello":
    fmt.Println(1)
case "world":
    fmt.Println(2)
default:    
    fmt.Println(0)}

//代码输出: 1

上面例子中,每一个 case 均是字符串格式,且使用了 default 分支,Go语言规定每个 switch 只能有一个 default 分支。

  • Go语言for循环结构

使用循环语句时,需要注意的有以下几点:

  • 左花括号{必须与 for 处于同一行。
  • Go语言中的 for 循环与C语言一样,都允许在循环条件中定义和初始化变量,唯一的区别是,Go语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。
  • Go语言的 for 循环同样支持 continue 和 break 来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环,如下例:
for j := 0; j < 5; j++ {
    for i := 0; i < 10; i++ {
        if i > 5 {
            break JLoop        
        }        
        fmt.Println(i)
    }
}

JLoop:// ...

上述代码中,break 语句终止的是 JLoop 标签处的外层循环。

5. 错误和异常处理

当涉及到错误和异常处理时,Go语言采用了一种不同于其他语言的方法。Go语言中的错误处理是通过返回错误值来完成的,而不是使用异常机制。

简单错误
  • 在Go语言中,errors.New()函数用于创建一个新的错误对象。它接收一个字符串参数作为错误的描述信息,并返回一个错误类型的值。通过判断错误对象实例来确定具体错误类型。
err := errors.New("something error")
  • fmt.Errorf() 创建 error 接口错误对象
err := fmt.Errorf("发生了错误:%s", reason)

通过调用 fmt.Printf 函数,并给定占位符 %s 就可以打印出某个值的字符串表示形式。对于其他类型的值来说,只要我们能为这个类型编写一个 String 方法,就可以自定义它的字符串表示形式。

而对于 error 类型值,它的字符串表示形式则取决于它的 Error 方法。在上述情况下,fmt.Printf 函数如果发现被打印的值是一个 error 类型的值,那么就会去调用它的 Error 方法。fmt 包中的这类打印函数其实都是这么做的。

顺便提一句,当我们想通过模板化的方式生成错误信息,并得到错误值时,可以使用 fmt.Errorf 函数。该函数所做的其实就是先调用 fmt.Sprintf 函数,得到确切的错误信息;再调用 errors.New 函数,得到包含该错误信息的 error 类型值,最后返回该值。

错误的Wrap和Unwrap

在Go语言中,标准库中的errors包提供了WrapUnwrap函数,是指错误处理机制中的用于错误的包装和解包。

  • 错误包装是指在处理错误时,将原始错误包装在新的错误中,以提供更多的上下文信息。这样做可以保留原始错误的堆栈信息,并将新的错误与之关联。
  • 错误解包是指从包装的错误中提取出原始错误。这样做可以在需要时获取原始错误的详细信息。

目前Go标准库中提供的用于wrap error的API有fmt.Errorf和errors.Join。fmt.Errorf最常用,fmt.Errorf也支持通过多个%w一次打包多个error,下面是一个完整的例子:

func main() {
    err1 := errors.New("error1")
    err2 := errors.New("error2")
    err3 := errors.New("error3")

    err := fmt.Errorf("wrap multiple error: %w, %w, %w", err1, err2, err3)
    fmt.Println(err)
    e, ok := err.(interface{ Unwrap() []error })
    if !ok {
        fmt.Println("not imple Unwrap []error")
        return
    }
    fmt.Println(e.Unwrap())
}

示例运行输出如下:

wrap multiple error: error1, error2, error3
[error1 error2 error3]

我们看到,通过fmt.Errorf一次wrap的多个error在String化后,是在一行输出的。

errors.Join用于将一组errors wrap为一个error。 下面是用errors.Join一次打包多个error的示例:

func main() {
    err1 := errors.New("error1")
    err2 := errors.New("error2")
    err3 := errors.New("error3")

    err := errors.Join(err1, err2, err3)
    fmt.Println(err)
    errs, ok := err.(interface{ Unwrap() []error })
    if !ok {
        fmt.Println("not imple Unwrap []error")
        return
    }
    fmt.Println(errs.Unwrap())
}

这个示例输出如下:

$go run demo2.go
error1
error2
error3
[error1 error2 error3]

我们看到,通过errors.Join一次wrap的多个error在String化后,每个错误单独占一行。

错误判定
  1. errors.Is(err, target):判断err是否是target类型的错误,返回布尔值。这个函数用于判断错误类型是否匹配。
    if errors.Is(err, io.EOF) {
        fmt.Println("遇到了文件末尾")
    }
  2. errors.As(err, target):将err转换为target类型的错误,返回布尔值。这个函数用于将错误转换为特定类型的错误,并进行相应的处理。
    var n *net.OpError
    if errors.As(err, &n) {
        fmt.Println("遇到了网络错误:", n)
    }
异常panic和恢复recover用法

panic:

1、内建函数
2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行,这里的defer 有点类似 try-catch-finally 中的 finally
4、直到goroutine整个退出,并报告错误

recover:

1、内建函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议
a). 在defer函数中,通过recever来终止一个gojroutine的panicking过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error

简单来讲:go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

你可能感兴趣的:(Go,golang,开发语言,后端)