高质量编程学习笔记 | 青训营

41 阅读21分钟

高质量编程

01高质量编程

1.1简介:

1.1.1什么是高质量?

编写的的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码。

高质量的代码有这些特征:

  1. 可读性:代码应该易于理解,使用清晰的命名、注释和缩进,遵循一致的编码风格,以便其他开发人员能够轻松阅读和理解代码。
  2. 可维护性:代码应该易于修改和维护。它应该有良好的模块化,遵循设计原则(如单一职责原则和开闭原则),并使用合适的数据结构和算法。
  3. 可扩展性:代码应该易于扩展,以便应对未来的需求变化。它应该具有良好的架构,允许添加新功能或修改现有功能而不会引入太多的依赖关系或影响其他部分的代码。
  4. 可重用性:代码应该是可重用的,以减少重复编写相似功能的工作量。它应该具有独立的模块,可以在不同的项目或场景中重复使用。
  5. 高效性:代码应该是高效的,具有良好的性能和资源利用率。它应该避免不必要的计算和内存消耗,并使用适当的算法和数据结构来提高执行效率。
  6. 健壮性:代码应该具有良好的错误处理和异常处理机制,能够处理各种异常情况,并提供适当的错误提示和日志记录。 高质量编程

01高质量编程

1.1简介:

1.1.1什么是高质量?

编写的的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码。 高质量的代码有这些特征:

  1. 可读性:代码应该易于理解,使用清晰的命名、注释和缩进,遵循一致的编码风格,以便其他开发人员能够轻松阅读和理解代码。
  2. 可维护性:代码应该易于修改和维护。它应该有良好的模块化,遵循设计原则(如单一职责原则和开闭原则),并使用合适的数据结构和算法。
  3. 可扩展性:代码应该易于扩展,以便应对未来的需求变化。它应该具有良好的架构,允许添加新功能或修改现有功能而不会引入太多的依赖关系或影响其他部分的代码。
  4. 可重用性:代码应该是可重用的,以减少重复编写相似功能的工作量。它应该具有独立的模块,可以在不同的项目或场景中重复使用。
  5. 高效性:代码应该是高效的,具有良好的性能和资源利用率。它应该避免不必要的计算和内存消耗,并使用适当的算法和数据结构来提高执行效率。
  6. 健壮性:代码应该具有良好的错误处理和异常处理机制,能够处理各种异常情况,并提供适当的错误提示和日志记录。
  7. 测试性:代码应该易于测试,具有良好的单元测试和集成测试覆盖率。它应该具有可测试的接口和模块,以便进行自动化测试和持续集成。

1.1.2高质量的需求: 各种边界条件是否考虑完备; 异常情况处理,稳定性保证; 易读易维护;

1.1.3编程原则: 实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量的编程遵循的原则是相通的。

1.简单性: 消除“多余的复杂性”,以简单清晰的逻辑编写代码; 不理解的代码无法修复改进;

2.可读性: 代码是写给人看的,而不是机器; 编写可维护代码的第一步是确保代码可读;

3.生产力: 团队整体工作效率非常重要;

1.2编码规范:

1.2.1如何编写高质量的Go代码

编写高质量的Go代码需要注意以下几个方面:

  1. 代码格式:遵循Go语言的官方代码格式规范,使用gofmt或goimports工具自动格式化代码。保持一致的缩进、换行和代码对齐,使代码易于阅读和理解。
  2. 注释:在代码中添加清晰和有用的注释,解释代码的意图、功能和使用方式。注释应该简洁明了,避免过多的冗余和废话。
  3. 命名规范:使用有意义和描述性的命名,遵循Go语言的命名规范。变量、函数和类型的命名应该简洁、清晰,并能准确表达其用途和含义。
  4. 控制流程:使用简洁和清晰的控制流程结构,避免过于复杂的嵌套和冗余的代码。尽量使用简洁的条件语句、循环语句和选择语句,使代码逻辑易于理解和维护。
  5. 错误和异常处理:在代码中合理处理错误和异常情况,避免简单地忽略错误或使用panic抛出异常。使用错误返回值或自定义错误类型来表示错误,并在适当的地方进行错误检查和处理。

1.2.2编码规范-代码格式

推荐使用gofmt自动化式化代码

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

2.goimports: 也是Go语言官方的提供的工具; 实际等于gofmt加上依赖包管理; 自动增删依赖的包引用、将依赖包按字母序排序并分类;

1.2.3编码规范-注释

1.简介:

注释应该做的: 注释应该解释代码作用; 注释应该解释代码如何做的; 注释应该解释代码实现的原因; 注释应该解释代码什么情况会出错; (好的代码有很多注释,坏代码需要很多注释)

1.2.4编码规范-注释

1.解释应该解释代码作用: 适合注释公共符号;

2.注释应该解释代码如何做的: 适合注释实现过程; .注释应该解释代码实现的原因: 适合解释代码的外部因数; 提供额外上下文;

4.注释应该解释代码什么情况会出错: 适合解释代码的限制条件;

5.公共符号始终要解释: 包含声明的每个公共符:变量,常量,函数以及结构都需要添加注释; 任何既不明显也不简短的公共功能必须予以注释; 无论长度或复杂程度如何对库中的任何函数都必须进行注释;

6.小结: 代码是最好的注释; 注释应该提供代码未表达出的上下文信息。

1.2.5编码规范-命名规范

1.variable: 简洁胜于冗长。 缩略词全大写,但当其位于变量开头且不需要导出时,使用小写。 变量距离被使用的地方越远,则需要携带越多的上下文信息。 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义。

2.function: 韩树明不携带包名的上下文信息,因为包名和函数名总是成对出现的。 函数名尽量简短。 当名为foo的包包含某个函数返回类型Foo时,可以省略类型的信息而不导致歧义。 当名为foo的包某个函数返回类型T时,T并不是Foo。可以在函数名中加入类型信息。

3.package: 字由小写字母组成,不包含大写字母和下划线等字符; 简短并包含一定的上下文信息。 不要与标准库同名; 以下规则尽量满足,以标准库报名为例: 不使用常用变量名作为包名; 使用单数,而不是复数; 谨慎的使用缩写;

4.小结: 核心目标是降低阅读理解代码的成本; 重点考虑上下文信息,设计简洁清晰的名称;

1.2.6编码规范-控制流程

1.避免嵌套,保持正常流程清晰;

2.尽量保持正常代码路径为最小缩进;

3.小结: 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支; 正常留存代码沿着屏幕向下移动; 提升代码可维护性和可读性; 故障问题大多出现在复杂的条件语句和循环语句中;

1.2.7编码规范-错误和异常处理

1.简单错误: 简单的错误指的是仅出现一次的错误且在其他地方不需要捕获该错误; 优先使用error.New,来创建匿名变量来直接表示简单错误; 如果有格式化的需求使用fmt.Error;

2.错误的Wrap和Unwrap: 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链; 在fmt.Errorf中使用:%w关键字来将一个错误关联至错误链中;

3.错误判定: 判定一个错误是否为特定错误使用error.ls; 不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误;

4.错误判定: 在错误链上获取特定种类的错误,使用error.As;

5.Panic: 不建议在业务代码中使用panic; 调用函数不包含recover会造成程序崩溃; View问题可以被屏蔽或解决,建议使用error代替panic; 当程序启动阶段发生不可逆逆转的错误时,可以在init或main函数中使用panic;

6.Recover: recover只能在被defer的函数中使用; 嵌套无法生效; 只在当前goroutine生效; defer的雨季是后进先出; 如果需要更多的上下文信息,可以在recovered后在log中记录当前的调用栈。

7.小结: error尽可能提供简明的上下文信息链,方便定位问题; panic用于真正异常的情况; recover生效范围在当前goroutine的被defer还是中生效;

1.3性能优化建议:

1.简介: 性能优化的前提是满足正确,可靠,简洁,清晰的质量因素; 性能优化是综合评估,有时候时间效率和空间效率可能对立; 针对go语言特性介绍go相关的性能优化建议;

1.3.1性能优化建议-benchmark

1.如何使用: 性能表现需要实际数据衡量; Go语言提供了支持基准性能测试的benchmark工具;

1.3.2性能优化建议-Slice

1.slice预分配内存: 尽可能的在使用make()初始化切片时提供容量信息;

2.陷阱: 大内存未释放; 1.3.3性能优化建议-Map map预分配内存: 不断向map中添加元素的操作会触发map的扩容; 提前分配好空间,可以减少内存拷贝和rehash的消耗; 建议根据实际需求提前预估好需要的空间;

1.3.4性能优化建议-字符串处理 使用strings.Bileder: 使用+拼接性能最差,strings.Bulider,bytes.Buffer相近,string.Buffer更快;

1.3.5性能优化建议-空结构体 使用空结构体结构节省内存: 空结构体是指没有任何字段或成员的结构体。 在某些情况下,我们可能只需要一个标识符或者仅仅需要利用结构体类型的特性,而不需要实际存储任何数据。这时,使用空结构体可以避免不必要的内存分配和存储开销。

package main
import "fmt"
type EmptyStruct struct{}
func main() {
var empty EmptyStruct
fmt.Println(empty) // 输出:{}
fmt.Println(sizeof(empty)) // 输出:0
}
// sizeof 函数用于获取变量占用的内存大小
func sizeof(v interface{}) int {
return int(unsafe.Sizeof(v))
}

在上述示例中,EmptyStruct 是一个空结构体,它没有任何字段。我们可以创建一个空结构体变量 empty,并输出它的值,结果是一个空的花括号 {}。然后,我们使用 sizeof 函数获取 empty 变量占用的内存大小,结果为 0。 需要注意的是,尽管空结构体不占用任何内存空间,但在某些情况下,编译器可能会对空结构体进行内存对齐,以确保结构体数组的元素对齐。因此,在实际使用中,还需要考虑编译器的优化行为和对齐规则。 1.3.6性能优化建议-atomic包: 如何使用atomic包: atomic包是Go语言提供的用于原子操作的包。原子操作是指在多个goroutine并发访问共享资源时,保证操作的原子性,避免竞态条件和数据竞争。

使用atomic包的一般步骤如下:

  1. 导入atomic包:在代码中导入atomic包,可以使用import语句导入。
  2. 定义共享资源:定义一个需要进行原子操作的共享资源,例如一个int32类型的变量。
  3. 执行原子操作:使用atomic包提供的函数对共享资源进行原子操作,例如原子读取、写入、加法操作或比较和交换操作。
package main
import (
	"fmt"
	"sync/atomic"
)
func main() {
	var counter int32
	// 原子加法操作
	atomic.AddInt32(&counter, 1)
	// 原子读取操作
	value := atomic.LoadInt32(&counter)
	fmt.Println("Counter:", value)
	// 原子写入操作
	atomic.StoreInt32(&counter, 10)
	// 原子比较和交换操作
	swapped := atomic.CompareAndSwapInt32(&counter, 10, 20)
	fmt.Println("Swapped:", swapped)
}

在上述示例中,首先定义了一个int32类型的counter变量作为共享资源。然后使用atomic包提供的函数进行原子加法操作、原子读取操作、原子写入操作和原子比较和交换操作。最后打印结果。 使用atomic包可以保证对共享资源的原子操作,避免竞态条件和数据竞争,确保并发程序的正确性和稳定性。

1.4 小结: 要实现高质量的Go语言编程,我们可以从许多方面入手

  1. 编写清晰、简洁的代码:
  • 使用有意义的变量和函数命名,避免使用过于简单或者过于复杂的命名。
  • 遵循Go的命名规范,如使用驼峰命名法、使用首字母大写表示公有成员等。
  • 避免冗余的代码和重复的逻辑,尽量保持代码的简洁性。
  • 使用注释来解释代码的用途和实现逻辑,提高代码的可读性。
  1. 引入合适的包和库:
  • 使用标准库提供的功能,避免重复造轮子。
  • 选择合适的第三方库,注意查看其文档和社区支持情况。
  • 注意库的稳定性和性能,避免使用不成熟或者低效的库。
  1. 错误处理和日志记录:
  • 合理处理错误,避免忽略错误或者错误处理不当。
  • 使用Go提供的多返回值来处理函数执行的错误状态。
  • 使用日志记录工具来记录程序的运行状态和错误信息,方便调试和排查问题。
  1. 并发编程的注意事项:
  • 合理使用Goroutine和Channel来实现并发,注意避免竞态条件和资源争用。
  • 使用互斥锁和条件变量来保护共享资源,避免数据竞争。
  • 使用Select语句来处理多个Channel之间的非阻塞选择。
  1. 性能优化:
  • 使用性能分析工具来找出程序的性能瓶颈,进行针对性的优化。
  • 避免不必要的内存分配和拷贝,尽量复用对象和减少内存碎片。
  • 使用并发编程来提高程序的并发性能,充分利用多核处理器的能力。
  1. 单元测试和代码覆盖率:
  • 编写单元测试来验证代码的正确性和稳定性。
  • 使用Go提供的testing包来编写测试用例和运行测试。
  • 使用代码覆盖率工具来检查测试覆盖率,确保测试覆盖到所有的代码路径。 通过以上的方法,可以提高Go语言编程的质量和效率,使得代码更加可靠、高效和易于维护。

注释应该解释代码什么情况会出错; (好的代码有很多注释,坏代码需要很多注释) 1.2.4编码规范-注释 1.解释应该解释代码作用: 适合注释公共符号; 2.注释应该解释代码如何做的: 适合注释实现过程; .注释应该解释代码实现的原因: 适合解释代码的外部因数; 提供额外上下文; 4.注释应该解释代码什么情况会出错: 适合解释代码的限制条件; 5.公共符号始终要解释: 包含声明的每个公共符:变量,常量,函数以及结构都需要添加注释; 任何既不明显也不简短的公共功能必须予以注释; 无论长度或复杂程度如何对库中的任何函数都必须进行注释; 6.小结: 代码是最好的注释; 注释应该提供代码未表达出的上下文信息。 1.2.5编码规范-命名规范 1.variable: 简洁胜于冗长。 缩略词全大写,但当其位于变量开头且不需要导出时,使用小写。 变量距离被使用的地方越远,则需要携带越多的上下文信息。 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义。 2.function: 韩树明不携带包名的上下文信息,因为包名和函数名总是成对出现的。 函数名尽量简短。 当名为foo的包包含某个函数返回类型Foo时,可以省略类型的信息而不导致歧义。 当名为foo的包某个函数返回类型T时,T并不是Foo。可以在函数名中加入类型信息。 3.package: 字由小写字母组成,不包含大写字母和下划线等字符; 简短并包含一定的上下文信息。 不要与标准库同名; 以下规则尽量满足,以标准库报名为例: 不使用常用变量名作为包名; 使用单数,而不是复数; 谨慎的使用缩写; 4.小结: 核心目标是降低阅读理解代码的成本; 重点考虑上下文信息,设计简洁清晰的名称; 1.2.6编码规范-控制流程 1.避免嵌套,保持正常流程清晰; 2.尽量保持正常代码路径为最小缩进; 3.小结: 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支; 正常留存代码沿着屏幕向下移动; 提升代码可维护性和可读性; 故障问题大多出现在复杂的条件语句和循环语句中; 1.2.7编码规范-错误和异常处理 1.简单错误: 简单的错误指的是仅出现一次的错误且在其他地方不需要捕获该错误; 优先使用error.New,来创建匿名变量来直接表示简单错误; 如果有格式化的需求使用fmt.Error; 2.错误的Wrap和Unwrap: 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链; 在fmt.Errorf中使用:%w关键字来将一个错误关联至错误链中; 3.错误判定: 判定一个错误是否为特定错误使用error.ls; 不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误; 4.错误判定: 在错误链上获取特定种类的错误,使用error.As; 5.Panic: 不建议在业务代码中使用panic; 调用函数不包含recover会造成程序崩溃; View问题可以被屏蔽或解决,建议使用error代替panic; 当程序启动阶段发生不可逆逆转的错误时,可以在init或main函数中使用panic; 6.Recover: recover只能在被defer的函数中使用; 嵌套无法生效; 只在当前goroutine生效; defer的雨季是后进先出; 如果需要更多的上下文信息,可以在recovered后在log中记录当前的调用栈。 7.小结: error尽可能提供简明的上下文信息链,方便定位问题; panic用于真正异常的情况; recover生效范围在当前goroutine的被defer还是中生效; 1.3性能优化建议: 1.简介: 性能优化的前提是满足正确,可靠,简洁,清晰的质量因素; 性能优化是综合评估,有时候时间效率和空间效率可能对立; 针对go语言特性介绍go相关的性能优化建议; 1.3.1性能优化建议-benchmark 1.如何使用: 性能表现需要实际数据衡量; Go语言提供了支持基准性能测试的benchmark工具; 1.3.2性能优化建议-Slice 1.slice预分配内存: 尽可能的在使用make()初始化切片时提供容量信息; 2.陷阱: 大内存未释放; 1.3.3性能优化建议-Map map预分配内存: 不断向map中添加元素的操作会触发map的扩容; 提前分配好空间,可以减少内存拷贝和rehash的消耗; 建议根据实际需求提前预估好需要的空间; 1.3.4性能优化建议-字符串处理 使用strings.Bileder: 使用+拼接性能最差,strings.Bulider,bytes.Buffer相近,string.Buffer更快; 1.3.5性能优化建议-空结构体 使用空结构体结构节省内存: 空结构体是指没有任何字段或成员的结构体。 在某些情况下,我们可能只需要一个标识符或者仅仅需要利用结构体类型的特性,而不需要实际存储任何数据。这时,使用空结构体可以避免不必要的内存分配和存储开销。

package main
import "fmt"
type EmptyStruct struct{}
func main() {
var empty EmptyStruct
fmt.Println(empty) // 输出:{}
fmt.Println(sizeof(empty)) // 输出:0
}
// sizeof 函数用于获取变量占用的内存大小
func sizeof(v interface{}) int {
return int(unsafe.Sizeof(v))
}

在上述示例中,EmptyStruct 是一个空结构体,它没有任何字段。我们可以创建一个空结构体变量 empty,并输出它的值,结果是一个空的花括号 {}。然后,我们使用 sizeof 函数获取 empty 变量占用的内存大小,结果为 0。 需要注意的是,尽管空结构体不占用任何内存空间,但在某些情况下,编译器可能会对空结构体进行内存对齐,以确保结构体数组的元素对齐。因此,在实际使用中,还需要考虑编译器的优化行为和对齐规则。 1.3.6性能优化建议-atomic包:

如何使用atomic包:

atomic包是Go语言提供的用于原子操作的包。原子操作是指在多个goroutine并发访问共享资源时,保证操作的原子性,避免竞态条件和数据竞争。 使用atomic包的一般步骤如下:

  1. 导入atomic包:在代码中导入atomic包,可以使用import语句导入。
  2. 定义共享资源:定义一个需要进行原子操作的共享资源,例如一个int32类型的变量。
  3. 执行原子操作:使用atomic包提供的函数对共享资源进行原子操作,例如原子读取、写入、加法操作或比较和交换操作。
package main
import (
	"fmt"
	"sync/atomic"
)
func main() {
	var counter int32
	// 原子加法操作
	atomic.AddInt32(&counter, 1)
	// 原子读取操作
	value := atomic.LoadInt32(&counter)
	fmt.Println("Counter:", value)
	// 原子写入操作
	atomic.StoreInt32(&counter, 10)
	// 原子比较和交换操作
	swapped := atomic.CompareAndSwapInt32(&counter, 10, 20)
	fmt.Println("Swapped:", swapped)
}

在上述示例中,首先定义了一个int32类型的counter变量作为共享资源。然后使用atomic包提供的函数进行原子加法操作、原子读取操作、原子写入操作和原子比较和交换操作。最后打印结果。 使用atomic包可以保证对共享资源的原子操作,避免竞态条件和数据竞争,确保并发程序的正确性和稳定性。 1.4 小结: 要实现高质量的Go语言编程,我们可以从许多方面入手:

  1. 编写清晰、简洁的代码:
  • 使用有意义的变量和函数命名,避免使用过于简单或者过于复杂的命名。
  • 遵循Go的命名规范,如使用驼峰命名法、使用首字母大写表示公有成员等。
  • 避免冗余的代码和重复的逻辑,尽量保持代码的简洁性。
  • 使用注释来解释代码的用途和实现逻辑,提高代码的可读性。
  1. 引入合适的包和库:
  • 使用标准库提供的功能,避免重复造轮子。
  • 选择合适的第三方库,注意查看其文档和社区支持情况。
  • 注意库的稳定性和性能,避免使用