原书地址
Code and Project Organization
减少隐藏变量(Unintended variable shadowing)
Avoiding shadowed variables can help prevent mistakes like referencing the wrong variable or confusing readers.
减少再代码中重新声明变量可以有效防止错误。但是其实也是分情况,毕竟对于我个人来说,err 确实就是代表错误的最直观变量名。
尽量的减少嵌套(Unnecessary nested code)
Avoiding nested levels and keeping the happy path aligned on the left makes building a mental code model easier.
简单来说,能不要 else 就不要 else
Good
if foo() { // ... return true } // no else
Bad
if foo() { // ... return true } else { // ... }
快速错误(这算是我单独分的)
快速结束程序,不要让带有错数据的代码继续往下执行。
Bad
if s != "" { // ... } else { return errors.New("empty string") }
Good
if s == "" { return errors.New("empty string") } // ...
不要滥用 init 函数
init 函数由于其执行位置的原因,在某些情况下作为初始化并不合适。
init 适合用来对某些变量进行初始化。
此处之所以不在全局声明的时候进行初始化,是因为 var 的执行顺序是较高的,如果你有些数据需要从文件中读取,那么很可能会出现顺序问题,init 的执行顺序会相对较低,这样子可以减少这种错误。
在大多数情况下,我们应该通过特设函数处理初始化。
不要强制使用getter和setter
Forcing the use of getters and setters isn’t idiomatic in Go. Being pragmatic and finding the right balance between efficiency and blindly following certain idioms should be the way to go.
go 中并不推荐使用这两个函数,当然,如果你是打算将部分结构体接口化,那么这两个函数还是很有用的。
interface 应该被发现,而不是被创造。
Abstractions should be discovered, not created. To prevent unnecessary complexity, create an interface when you need it and not when you foresee needing it, or if you can at least prove the abstraction to be a valid one.
go 不提倡先声明 interface 再来去进行实现,而是应该先实践再来去抽象出 interface。
为了避免不必要的复杂性,请在需要时创建接口,而不是在预见需要时创建接口,或者至少可以证明抽象是有效的。
如果不清楚接口如何使代码更好,我们可能应该考虑删除它,使我们的代码更简单。
Don’t design with interfaces, discover them.
——Rob Pike
将接口定义在实现方一侧
Keeping interfaces on the client side avoids unnecessary abstractions.
接口在Go中是隐式满足的,与具有显式实现的语言相比,这往往是一个游戏规则的改变者。
避免不必要的抽象
将接口保持在客户端(即使用接口的代码部分)可以减少不必要的抽象层。这样做的好处是可以减少代码的复杂性和提高其可读性。在许多其他语言中,生产者(即实现接口的代码部分)需要显式地声明它们实现了哪些接口,这往往导致创建了很多不必要的或过度设计的抽象层,而这些抽象层实际上可能并不需要。
抽象的发现而非创建
这种方法鼓励开发者”发现”而非”创建”抽象。在Go中,接口是一种发现服务的机制。它允许开发者在实现具体功能时保持灵活性,而不是一开始就强制实现特定的接口。这种方式促进了更自然的代码演进和重构,因为开发者可以在不改变现有代码的基础上,通过定义新的接口来满足新的需求。
客户端驱动的设计
由客户端确定是否需要某种形式的抽象,以及确定满足其需求的最佳抽象级别,这是一种客户端驱动的设计。这种设计哲学有助于确保只有真正需要的抽象被引入,而不是由生产者预先假设。这样做有助于创建更灵活、更适应变化的软件系统,因为接口的设计和实现更贴近实际的使用情况和需求。
它强调了抽象的自然发现过程,而不是前期过度设计和强制实现,从而有助于开发出更为简洁、易于维护和扩展的软件系统。
一个函数应该尽可能的接受接口,而拒绝返回接口,因为接口是隐性实现的
To prevent being restricted in terms of flexibility, a function shouldn’t return interfaces but concrete implementations in most cases. Conversely, a function should accept interfaces whenever possible.
- 用接口来作为参数的好处是便于实现。
- 将接口作为返回值的缺点是如果需要对参数内的值进行访问,需要用到一些其他的函数,而且这样子也容易需要对原本的接口进行修改,反而得不偿失。
不要让any
没传递任何信息
只有在需要接受或返回任意类型时,才使用 any
,例如 json.Marshal
。其他情况下,因为 any
不提供有意义的信息,可能会导致编译时问题,如允许调用者调用方法处理任意类型数据。
小心使用类型嵌入
Using type embedding can also help avoid boilerplate code; however, ensure that doing so doesn’t lead to visibility issues where some fields should have remained hidden.
类型嵌入的示例如下:
type Foo struct {
Bar // Embedded field 此处没有起一个字段名
//bar Bar 这个就不是类型嵌入
}
type Bar struct {
Baz int
}
使用类型嵌套的原则:
- 不要为了简化字段访问而使用类型嵌入,例如
Foo.Baz()
而不是Foo.Bar.Baz()
。 - 他不能够将嵌入的类型中原本不可访问的数据(即不能让小写,不可被包外数据访问的类型变得可以访问)
尽可能的使用 function option模式
尽量参考 Project Layout 来组织整个项目
尽量不要创建 common 这类过于宽泛的包名
提供代码文档,特别是导出元素
数据类型
slice
map
map 本身是强制无序的
map 在条件允许的情况,进行 cap 预分配是比较好的
一个map的buckets占用的内存只会增长,不会缩减。
因此针对生命周期超过一个函数调用的有一下几点建议:
- 不要直接存储数据,存储指针(推荐:( •̀ ω •́ )✧)
- 如果数据量先增大后减小,可以考虑重新创建一个
Go中比较两个类型值时,如果是可比较类型,那么可以使用
==
或者!=
运算符进行比较,比如:booleans、numerals、strings、pointers、channels,以及字段全部是可比较类型的structs。其他情况下,你可以使用reflect.DeepEqual
来比较,用反射的话会牺牲一点性能,也可以使用自定义的实现和其他库来完成。
控制结构
尽可能少用 range ,因为range 是一个拷贝
range
循环中的循环变量是遍历容器中元素值的一个拷贝。因此,如果元素值是一个struct并且想在 range
中修改它,可以通过索引值来访问并修改它,或者使用经典的for循环+索引值的写法(除非遍历的元素是一个指针)。
注意 range 的不同用法
slice
使用拷贝,内部修改不影响循环。
package main
import "fmt"
func main() {
A := []int{1, 2, 3, 4, 5}
for i, v := range A {
fmt.Println(i, v)
if i == 2 {
A = append(A, 6)
A[3] = 1
}
}
fmt.Println(A)
}
运行结果
0 1
1 2
2 3
3 4
4 5
[1 2 3 1 5 6]
对 slice 的修改并不会在循环中体现,说明是使用的 slice 的拷贝,而不是使用 slice 的原本
Array
使用拷贝,内部修改不影响循环。
package main
import "fmt"
func main() {
A := [10]int{1, 2, 3, 4, 5}
for i, v := range A {
fmt.Println(i, v)
if i == 2 {
A[3] = 1
}
}
fmt.Println(A)
}
输出结果
3 4
4 5
5 0
6 0
7 0
8 0
9 0
[1 2 3 1 5 0 0 0 0 0]
可以看到 A[3] 被实际修改了,但是在 for range 中并没有体现
Map
使用原值,会影响后续 for
(其实是因为拷贝过来的是指针,指向了原始数据)
package main
import "fmt"
func main() {
m := map[string]int{"a": 1, "b": 2, "d": 4}
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
if k == "d" {
m["c"] = 3
}
}
fmt.Println(m)
}
输出结果
a: 1
b: 2
d: 4
c: 3
map[a:1 b:2 c:3 d:4]
channel
访问的原始值
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
// 此处的结束条件是 errCh 被关闭,否则就会一直等待/打印错误
ch := make(chan int, 100)
go func() {
timer := time.NewTimer(5 * time.Second)
for {
select {
case <-timer.C:
close(ch)
return
default:
ch <- rand.Intn(20)
}
}
}()
for n := range ch {
fmt.Println(n)
}
}