人们普遍认为,编码是一门艺术,就像每个工匠都会制作精彩的艺术品并为它们感到自豪一样,我们作为开发者也确实为我们编写的代码感到自豪。为了达到最好的效果,艺术家们不断地寻找方法和工具来提高他们的技艺。同样,我们作为开发者也在不断提高自己的技能,并保持对**"如何写好代码 "这个最重要的问题的好奇心,以了解答案。**🤯
Frederick P. Brooks在他的书 "The Mythical Man Month:关于软件工程的论文》中写道:
"程序员,就像诗人一样,他的工作只是略微脱离了纯粹的思想材料。他在空气中建造他的城堡,从空气中,通过发挥想象力进行创造。很少有创造的媒介是如此灵活,如此容易打磨和重做,如此容易实现宏伟的概念结构。

这篇文章试图探讨上述漫画中的大问号的答案。写好代码的最简单的方法是避免在我们写的代码中包含反模式😇。
什么是反模式?🤔
当编写代码时没有考虑到未来的因素,就会出现反模式。反模式最初可能是问题的适当解决方案,但实际上,随着代码库的扩展,这些模式会变得晦涩难懂,并给我们的代码库增加 "技术债务"。
反模式的一个简单例子是在编写API时没有考虑到API的消费者会如何使用它,正如下面的例子1所解释的。意识到反模式并在编程时有意识地避免使用它们,肯定是朝着更可读和可维护的代码库迈出的重要一步。在这篇文章中,让我们来看看Go中几个常见的反模式。
1.从导出的函数中返回未导出的类型的值
在Go中,要export 任何field 或variable ,我们需要确保其名称以大写字母开头。导出它们背后的动机是为了使它们对其他软件包可见。例如,如果我们想使用math 包中的Pi 函数,我们应该把它定位为math.Pi 。使用math.pi 将无法工作,并且会出错。
以小写字母开头的名称(结构字段、函数、变量)是未导出的,并且只在它们定义的包内可见。
一个导出的函数或方法返回一个未导出的类型的值,使用起来可能会令人沮丧,因为从其他包中调用该函数的人将不得不再次定义一个类型来使用它:
// Bad practice
type unexportedType string
func ExportedFunc() unexportedType {
return unexportedType("some string")
}
// Recommended
type ExportedType string
func ExportedFunc() ExportedType {
return ExportedType("some string")
}
2.不必要地使用空白标识符
在各种情况下,为空白标识符赋值是不需要的,也是不必要的。在for 循环中使用空白标识符的情况下,Go规范中提到。
如果最后一个迭代变量是空白标识符,则range子句等同于没有该标识符的相同子句:
// Bad practice
for _ = range sequence
{
run()
}
x, _ := someMap[key]
_ = <-ch
// Recommended
for range something
{
run()
}
x := someMap[key]
<-ch
当把多个分片追加到一个分片中时,没有必要在分片上进行迭代并逐一追加每个元素。相反,在一个单一的append 语句中这样做会更好、更有效率。
作为一个例子,下面的代码段通过迭代sliceTwo ,一个一个地追加元素来进行连接:
for _, v := range sliceTwo {
sliceOne = append(sliceOne, v)
}
但是,由于我们知道append 是一个variadic 函数,因此,它可以用零或多个参数来调用。因此,上面的例子可以用更简单的方式重写,只用一个append 的函数调用就可以了:
sliceOne = append(sliceOne, sliceTwo…)
4.make 调用中的冗余参数
make 函数是一个特殊的内置函数,用于分配和初始化map、slice或chan类型的对象。对于使用make 来初始化一个分片,我们必须提供分片的类型,分片的长度,以及分片的容量作为参数。在使用make 来初始化一个map 的情况下,我们需要把map 的大小作为一个参数。
make然而,《指南》已经为这些参数提供了默认值:
- 对于通道,缓冲区的容量默认为零(无缓冲)。
- 对于地图,分配的大小默认为一个小的起始大小。
- 对于片子,如果容量被省略,则容量默认为长度。
因此:
ch = make(chan int, 0)
sl = make([]int, 1, 1)
可以被改写为:
ch = make(chan int)
sl = make([]int, 1)
然而,为了调试的目的,或为了适应数学,或为了平台特定的代码,使用带有通道的命名常量并不被认为是一种反模式:
const c = 0
ch = make(chan int, c) // Not an anti-pattern
5.在函数中使用无用的return
在没有返回值的函数中,把return 语句作为最后的语句,这被认为是不好的做法:
// Useless return, not recommended
func alwaysPrintFoofoo() {
fmt.Println("foofoo")
return
}
// Recommended
func alwaysPrintFoo() {
fmt.Println("foofoo")
}
然而,有名字的返回不应该与无用的返回相混淆。下面的返回语句确实返回了一个值:
func printAndReturnFoofoo() (foofoo string) {
foofoo = "foofoo"
fmt.Println(foofoo)
return
}
6.无用的break 语句在switch
在Go中,switch 语句没有自动fallthrough 。在C语言等编程语言中,如果前一个案例缺少break 语句,执行就会落入下一个案例。但是,人们普遍发现,fallthrough 在switch-case中的使用非常少,而且大多会导致bug。因此,许多现代编程语言,包括Go,改变了这一逻辑,默认情况下从不fallthrough 案例。
因此,不需要将break 语句作为switch 语句的case块中的最后一个语句。下面两个例子的行为是一样的。
坏的模式:
switch s {
case 1:
fmt.Println("case one")
break
case 2:
fmt.Println("case two")
}
好的模式。
switch s {
case 1:
fmt.Println("case one")
case 2:
fmt.Println("case two")
}
然而,对于在Go中的switch 语句中实现fallthrough ,我们可以使用fallthrough 语句。作为一个例子,下面给出的代码片断将打印23 :
switch 2 {
case 1:
fmt.Print("1")
fallthrough
case 2:
fmt.Print("2")
fallthrough
case 3:
fmt.Print("3")
}
7.不使用普通任务的辅助函数
某些函数,对于一组特定的参数,有速记功能,可以用它来代替,以提高效率和更好的理解/可读性。
例如,在Go中,为了等待多个goroutines完成,我们可以使用sync.WaitGroup 。而不是用1 ,然后再加上-1 ,使计数器的值达到0 ,以表示所有的goroutines都已经执行完毕,从而使sync.WaitGroup 计数器递增:
wg.Add(1)
// ...some code
wg.Add(-1)
使用wg.Done() 辅助函数更简单易懂,它本身就会通知sync.WaitGroup 所有的goroutines已经完成,而不需要我们手动将计数器带到0:
wg.Add(1)
// ...some code
wg.Done()
8.冗余的nil 片断的检查
一个nil 片断的长度被评估为零。因此,在计算一个片断的长度之前,没有必要检查它是否是nil 。
例如,下面的nil 检查是没有必要的:
if x != nil && len(x) != 0 {
// do something
}
上述代码可以省略nil 的检查,如下图所示:
if len(x) != 0 {
// do something
}
9.太复杂的函数字词
只调用一个函数的函数字头可以被删除,而不对内部函数的值做任何其他的改变,因为它们是多余的。相反,应该调用在外层函数内部调用的内层函数:
比如说:
fn := func(x int, y int) int { return add(x, y) }
可以简化为:
10.使用select 语句与单一案例
select 语句让一个goroutine等待多个通信操作。但是,如果只有一个操作/案例,我们实际上不需要select 语句。在这种情况下,一个简单的send 或receive 操作会有帮助。如果我们打算在不阻塞的情况下处理尝试发送或接收的情况,建议添加一个default 案例,使select 语句不阻塞。
// Bad pattern
select {
case x := <-ch:
fmt.Println(x)
}
// Recommended
x := <-ch
fmt.Println(x)
使用default:
select {
case x := <-ch:
fmt.Println(x)
default:
fmt.Println("default")
}
11.context.Context 应该是函数的第一个参数
context.Context 应该是第一个参数,通常命名为ctx 。ctx 应该是Go代码中许多函数的(非常)常见的参数,由于从逻辑上讲,把常见的参数放在参数列表的第一个或最后一个更好。为什么呢?因为它的使用模式是统一的,这有助于我们记住包含该参数。在Go中,由于变量可能只是参数列表中的最后一个,因此建议将context.Context 作为第一个参数。各种项目,甚至Node.js都有一些惯例,比如error first callback 。因此,context.Context 应该永远是一个函数的第一个参数,这是一个惯例:
// Bad practice
func badPatternFunc(k favContextKey, ctx context.Context) {
// do something
}
// Recommended
func goodPatternFunc(ctx context.Context, k favContextKey) {
// do something
}
当谈到在一个团队中工作时,审查其他人的代码变得很重要。DeepSource是一个自动化的代码审查工具,它可以管理端到端的代码扫描过程,并在有新的提交或新的拉动请求时自动提出修正的拉动请求。
为Go设置DeepSource是非常容易的。只要你设置了它,就会对你的整个代码库进行初步扫描,找到改进的范围,修复它们,并为这些修改打开拉动请求。
go build