一、简介
高质量编程是指以高标准和良好实践来编写可读、可维护、可测试和性能等方面的优秀表现的代码。
- 各种边界条件是否考虑完备
- 异常情况处理、稳定性保证
- 易读易维护
(一)编码原则
从指令的角度考虑,开发中应如何编码,才能减少执行的指令。各种语言特性和语法各不相同,但高质量编程遵循的原则是一致的,如下:
- 简洁性:代码应该简洁明了,避免冗余和复杂的逻辑。简洁的代码更易于理解、调试和维护。
- 可读性:代码应该易于阅读和理解。使用有意义的变量和函数命名,遵循一致的代码风格,添加适当的注释和文档,以提高代码的可读性。
- 一致性:在编写代码时应遵循一致的命名规范、代码风格和代码组织结构。一致的代码风格使得代码更易于理解和维护。
- 模块化:将代码划分为模块或函数,每个模块或函数只负责一个明确的任务。模块化的代码更易于测试、重用和维护。
- 错误处理:合理处理错误和异常情况。避免忽略错误,而是采取适当的错误处理机制,例如返回错误值或抛出异常。
- 依赖管理:使用Go模块管理依赖项,确保代码的可重复性和可维护性。可以使用Go Modules来管理项目的依赖。
- 测试:编写测试是保证代码质量的重要手段。编写单元测试和集成测试,覆盖代码的各个功能和边界情况。
- 文档:编写清晰、准确的文档,包括代码注释、函数说明和项目文档等。良好的文档可以帮助其他开发人员理解和使用代码。
- 并发安全性:在多线程环境中,需要确保代码的并发安全性。可以使用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 demopackage 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包提供了Wrap
和Unwrap
函数,是指错误处理机制中的用于错误的包装和解包。
- 错误包装是指在处理错误时,将原始错误包装在新的错误中,以提供更多的上下文信息。这样做可以保留原始错误的堆栈信息,并将新的错误与之关联。
- 错误解包是指从包装的错误中提取出原始错误。这样做可以在需要时获取原始错误的详细信息。
目前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化后,每个错误单独占一行。
错误判定
errors.Is(err, target)
:判断err
是否是target
类型的错误,返回布尔值。这个函数用于判断错误类型是否匹配。if errors.Is(err, io.EOF) {fmt.Println("遇到了文件末尾") }
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捕获这个异常,然后正常处理。