diff --git a/chap-40-Design-Recommendations/chap-40-Design-Recommendations.md b/chap-40-Design-Recommendations/chap-40-Design-Recommendations.md new file mode 100644 index 0000000000000000000000000000000000000000..961e3f78512b7f15c5e1d5e647a6b68c53a7007b --- /dev/null +++ b/chap-40-Design-Recommendations/chap-40-Design-Recommendations.md @@ -0,0 +1,817 @@ +# chap-40-设计建议 + +## 1 本章中能学到的内容 + +- 我们将详细介绍一些实用的设计建议来改进你的代码。 + +## 2 本章中提及的技术概念 + +- 包 +- 接口 +- 方法 +- 接收者 +- 圈复杂度 +- Halstead 指标 + +## 介绍 + +本章将尝试回答“如何设计我的 Go 代码?”这个问题。作为来自其他语言的 Go 新开发者,我在一开始时问了自己这个问题。当你编写仅供自己使用的软件时,这个问题并不重要。当你在团队中工作时,你必须遵循惯例和最佳实践。 + +我阅读了 Go 开发者写的热门博客文章来编写本章。我在本章中的目标是汇总这些建议。不要虔诚地遵循他们。请记住,每个项目都是不同的。 + +## 3 包名 + +包的名称向包的用户公开。因此,开发人员必须谨慎地选择包名。向包的用户公开是什么意思?它的意思是当有人想使用你包内的函数 `Bar` 时,他必须这样写: + +```go +pkgName.Bar() +``` + +以下是我们可以遵循的一些标准规则。 + +**建议:** + +- 简短: 不超过一个单词 +- 不使用复数 +- 小写 +- 包提供的服务的信息 +- 不使用公共工具包 + +**包名的建议:** + +- 例子 + - **net** 很短,没有复数,全部小写,我们立刻就知道这个包包含网络功能。 + - **os** +- 反例 + - **models**:表示此处有定义数据模型的类型结构的包。包提供的服务并不清楚(除了它会收集数据模型)。例如,我们可以用一个 user 包来保存与用户相关的用户模型和函数。 + - **userManagement**: 这不是单个单词。我们会使用它来管理程序的用户,但在我看来,这些功能应该存在于 user 包中(带有指向用户类型的指针的方法作为接收器)。 + - **utils**: 这个包通常保存其他包中使用的函数。因此建议将函数直接移动到使用它们的包中。 +- **关于工具包**:对于来自其他语言的开发人员来说,拥有一个 utils 包似乎是合法的,但对于 Go 语言开发者来说,这没有意义,因为它将必须混合可以被直接插入到使用位置的函数。这是我们在项目开始时把在其他地方有用的函数放入此类型包的某种反射。但工具包并未被禁止。Go 标准库提供了工具类的函数,只不过倾向于按类型对它们进行分组。例如 strings 或 bytes。 + +## 4 使用 Interfaces + +接口用于定义行为。基于这个行为的定义,你可以定义多个行为的实现。你的包的用户对你的实现不感兴趣。他们只关心你为他们提供的服务。接口定义了使用公共 API 的约定。实现可能会改变。例如,你可以提高你实现的方法性能。即使你彻底改变了包的工作方式,调用它的方式也将保持稳定。 + +![interface_implementation.41c91f23](./imgs/interface_implementation.41c91f23.png) + +在 Go 中,interface 是一种特殊的类型。你可以将其用作函数或方法的参数。 + +**建议:** + +- 使用 interface 作为函数或方法的参数和字段类型 +- 小接口更好 + +**接口使用的建议:** + +- **使用 interface 作为函数或方法的参数和字段类型**(请记住,它们也是类型)。通过接受 interface 作为参数,你可以突出这样一个事实,即在你的函数内部,你将只使用 interface 定义的行为。 + + 为了更好地理解,让我们举个例子。想象一下构建一个新的花哨的加密算法来与朋友秘密交换消息。 + + 你将需要开发一个函数来解密消息(也可以加密消息)。在下面的代码中,你可以看到你的第一次尝试 (`Decrypt1`): + + ```go + func Decrypt1(b []byte) ([]byte, error) { + //... + } + ``` + + 它接受一个字节切片作为参数并返回一个字节切片和一个错误。这个函数并没有什么不好,除了我们使用它时只能用一个字节切片作为输入。想象一下,你想用这种方法解密整个文件而不是一个字节切片?你需要从文件中读取所有字节并将其传递给 `Decrypt1` 函数。 + + 我们必须找到一种类型,使我们的 `Decrypt1` 函数更通用。用于此目的的 interface 是 `io.Reader`。标准库中的许多类型都实现了这个接口: + + - `os.File` + - `net.TCPConn` + - `net.UDPConn` + - `net.UnixConn` + + 如果你接收 `io.Reader` 作为参数,则既可以解密文件,也可以将其用于通过 TCP 或 UDP 传输的数据。这是该函数的第二个版本: + + ```go + func Decrypt2(r io.Reader) ([]byte, error) { + //... + } + ``` + + `io.Reader` 接口定义了一种行为,即 `Read`。实现了接口 `io.Reader` 中定义的 `Read` 函数的类型就是一个 `io.Reader`。 + + 这意味着我们的 `Decrypt2` 函数可以采用任何实现了 io.Reader 接口的类型。 + +- **小接口更好** + + 如果我们以标准 Go 库的接口为例,你会注意到它们通常非常小。行为的数量定义了接口的大小(换句话说,指定的方法签名的数量)。 + + 在 Go 中,你不需要指明类型实现一个接口。因此,当你的接口由许多行为组成时,很难看出哪些类型实现了这个接口。这就是为什么小接口在程序员的日常中更容易处理的原因。 + + 你可以注意到,Go 标准库中的许多接口是由 2-3 个方法组成的。我们以两个著名的 io.Reader 和 io.Writer 为例: + + ```go + type Reader interface { + Read(p []byte) (n int, err error) + } + type Writer interface { + Write(p []byte) (n int, err error) + } + ``` + + 这是一个反例: + + ```go + type Bad interface { + Foo(string) error + Bar(string) error + Baz([]byte) error + Bal(string, io.Closer) error + Cux() + Corge() + Corege3() + } + ``` + + `Bad` 接口很难实现。想要实现它的人需要开发七个方法!如果你打算构建一个广泛使用的包,你会让新手很难使用你的抽象方式。 + +## 5 源文件 + +一个包可以由单个文件组成。这是完全合法的,但是如果你的文件超过 600 行,则会变得难以阅读。这里有一些实用的建议,可以提高源代码的可读性。 + +**建议:** + +- 将一个文件的名称命名为包名 +- 每个文件不超过 600 行 +- 一个文件 = 一份责任 + +**源文件的建议:** + +![](./imgs/advice_sources.5e438d63.png) + +- **将单文件的名称命名为包名**:如果你的包中有多个文件,最好将一个文件命名为包的名称。例如,在上图中,你可以看到我们有两个包:fruit 和 bike。在 fruit 包中,我们有一个 fruit.go,在 bike 包中,我们有一个 bike.go 源文件。这些文件可以存放所有 fruit(或 bike)共有的共享类型、接口和常量。 +- **每个文件不超过 600 行**:此建议将提高你的程序或包的可读性。文件应该很短(但不能太短);这会让维护者的生活更轻松一点(滚动长文件会很无聊)。请注意,此限制是随意的,你可以根据自己的标准进行调整。 +- **一个文件 = 一份责任**:想象一下,你是 Go 开发团队的一员,你被分配去修复一个讨厌的 bug。在 GitHub issue 上,用户正在抱怨 HTTP 客户端处理 cookie 的方式。你需要找到管理 cookie 的位置。毫无疑问,cookie 是在文件 `net/http/cookie.go` 中管理的。这种命名约定让开发人员可以轻松定位源代码责任。 + +## 6 错误处理 + +错误和问题是编程的一部分。你的程序必须处理可能发生的所有错误。作为程序员,你必须考虑最坏的情况。问问自己这行代码可能会出什么问题?恶意用户可能使用什么技术来让你的程序崩溃? + +**建议**: + +- 始终给错误添加上下文信息 + +- 从不忽略错误 + +- 谨慎使用 fatal 错误 + +- 编写可以容错的程序 + +**错误处理的建议:** + +- **始终给错误添加上下文信息**:当创建错误时,给用户(也适用于维护你程序的团队)提供足够的信息。没有上下文信息的错误是非常难以理解的,在源代码中要找到它们的出处也非常困难。 + + ```go + func main() { + err := foo("test") + if err != nil { + fmt.Println(err) + } + } + + func foo(bar string) error { + err := baz() + if err != nil { + return err + } + return nil + } + + func baz() error { + return corge() + } + + func corge() error { + _, err := ioutil.ReadFile("/my/imagination.go") + if err != nil { + return err + } + return nil + } + + func looping() ([]byte, error) { + return ioutil.ReadFile("/my/imagination.go") + } + ``` + + 在这个小例子中,我们创建了三个函数 `foo`, `baz`, `corge` 和 `looping`。在 main 函数中,我们调用了 `foo`。这个函数将会调用 `baz`,`baz` 会调用 `corge`,`corge` 最终会尝试打开一个文件(此文件不存在)。 + + 当我们执行程序时,得到以下输出: + + ``` + open /my/imagination.go: no such file or directory + ``` + + 错误从何而来?它来自函数 `corge` 吗?它来自函数 `looping` 吗?如果你想知道,你必须完全按照执行路径去找,最终发现 `looping` 从未被调用,因此错误来自于 `corge`。 + + 在这个例子中,定位源码里错误发生的位置很难,对于包中包含数百个文件的更大程序来说,它可能变成一场噩梦。 + + 解决方案?使用 Dave Cheney 原生开发的包 `errors`: + + ```go + // recommendation/errors/main.go + package main + + import ( + "fmt" + "io/ioutil" + + "github.com/pkg/errors" + ) + + func main() { + err := foo("test") + if err != nil { + fmt.Println(err) + } + } + + func foo(bar string) error { + err := baz() + if err != nil { + return errors.Wrap(err, "fail to do baz") + } + return nil + } + + func baz() error { + err := corge() + if err != nil { + return errors.Wrap(err, "fail to do corge") + } + return nil + } + + func corge() error { + _, err := ioutil.ReadFile("/my/imagination.go") + if err != nil { + return errors.Wrap(err, "fail to open imaginary file") + } + return nil + } + + func looping() ([]byte, error) { + return ioutil.ReadFile("/my/imagination.go") + } + ``` + + 我们简单地调用 `Wrap` 方法,将错误和一条信息作为参数。通过这个简单的添加,我们程序现在的输出是: + + ``` + fail to do baz: fail to do corge: fail to open imaginary file: open /my/imagination.go: no such file or directory + ``` + + 可以看到错误更清晰,故障定位也立竿见影。 + + `errors` 包还有另一个功能,可以使用格式化指令 `%+v` 打印有关错误的更多信息。使用这种格式将详细打印错误堆栈跟踪的每个帧。 + +- **从不忽略错误**。这也许很显而易见,但许多开发人员仍然犯了这个错误。出现的错误应该这样处理: + + - 返回给调用者 + - 或者处理掉(你的程序实现了某种自动校正机制) + +- **谨慎使用 fatal 错误**。当你调用 `log.Fatal` 时,意味着你在强制你的程序突然退出(使用 `os.Exit(1)`)。程序会立即中止,defer 的函数将不会运行。defer 的函数通常用于清理逻辑(例如关闭文件描述符)。因此,我们最好还是运行 defer 函数。 + + ```go + // standard log package. + // Fatal is equivalent to Print() followed by a call to os.Exit(1). + func Fatal(v ...interface{}) { + std.Output(2, fmt.Sprint(v...)) + os.Exit(1) + } + ``` + +- **编写可以容错的程序**。硬件工程师经常使用“容错”一词。大多数硬件组件旨在优雅地处理故障并从中恢复。软件工程师也应该在构建他们的程序时容忍错误。尽管执行失败(可能是暂时的或永久性的),但程序仍应该达到其目的。 + +### 6.1 发生错误时,检查它并确定它是否可以被恢复 + +例如,你正在构建一个调用 Web 服务的程序。在你的程序执行期间,调用失败。失败的原因是网络(你的服务器已与互联网断开连接)。这个错误是可以恢复的,因为网络将在某个时候再次可用。 + +如果通过网络对你的 Web 服务调用成功,但返回了 http 301 错误(“资源永久移动”),则该错误不可以恢复。因为你定义了错误的 Web 服务的 URL,或者你的 Web 服务提供商在没有警告你的情况下更改了某些内容。这种情况下人为干预将是必要的。 + +- **实现备用选项** + +备用选项是“如果首选选项不可用,则是一种应急选项”(维基百科)。例如,在我们的程序中,网络调用是不可用的或返回了错误。我们应该考虑多种选项。 + +根据错误是否可恢复,选项将不相同。 + +如果遇到网络故障,你可以实现一个重试机制,而不是直接返回错误。重试机制可以让你按可配置的次数重连 Web 服务。 + +## 7 函数和方法 + +函数和方法在程序中无处不在。一个语法正确的函数(即通过程序编译)可能在代码风格上并不正确。我们想在这里介绍一些与函数编写相关的建议,即如何写出风格正确的函数。 + +**建议:** + +- 一个函数一个目的 +- 名称简单 +- 限制长度(最大 100 行) +- 减少圈复杂度 +- 减少嵌套层数 + +### 7.1 一个函数一个目的 + +函数是一个执行**特定**任务的有名称的过程(或例程)。它可以有输入参数,也可以有输出参数。这里的重要术语是“特定”。函数(或方法)执行单个任务,而不是多个。它只有一个目的。 + +一个好的的函数只做一件事,并且做得非常好。例如,在 `math` 包中,指数函数将为每个 x 实数值计算 `exp(x)` 的值。 + +这个函数应该只有一个目的,这很容易理解。该函数不会同时计算 x 的指数和对数值。相反,我们有两个函数,指数函数和对数函数。 + +这是一个反例: + +```go +type User struct { + //... +} + +func (u *User) saveAndAuthorize error { + //... + return nil +} +``` + +这个 `saveAndAuthorize` 方法会执行 2 个任务: + +- 保存这个用户 +- **并**授权它 + +两个不同的任务需要不同的能力(写入数据库、读取数据库、检查访问令牌有效性...)。这个程序可以通过编译,但很难进行测试。返回的错误可以由数据层的故障引起,也可以由应用程序的安全层引起。 + +一种解决方案是将函数拆分为两个不同的函数:`create` 和 `authorize`。 + +```go +func (u *User) create error { + //.. + return nil +} + +func (user *User) authorize error { + //... + return nil +} +``` + +### 7.2 名称简单 + +- 不要在方法名称中重复接收器的名称 + +例如: + +```go +func (u *User) saveUser() error { + + return nil +} + +func (u *User) authorizeUser() error { + + return nil +} +``` + +我们可以重命名这两个函数: + +```go +func (u *User) save() error { + + return nil +} + +func (u *User) authorize() error { + + return nil +} +``` + +我们通过删除类型名称 user 来减短函数名称的长度。记住要始终**站在包调用者的角度**思考。让我们来比较这两个代码片段: + +```go +user := user.NewUser() +err := user.saveUser() +if err != nil { + //.. +} + +user := user.New() +err := user.save() +if err != nil { + //.. +} +``` + +第二个比第一个简洁得多。第一个由 65 个字符组成,而第二个由 57 个字符组成(包括空格)。 + +### 7.3 限制代码行数 + +一个函数应该只有一个目的(见上一节),而且应该很小。当你增加函数中的行数时,你也增加了阅读和理解它的时间和认知所需的努力。 + +例如,这是包 `heap` 中的函数 `Pop` : + +```go +func Pop(h Interface) interface{} { + n := h.Len() - 1 + h.Swap(0, n) + down(h, 0, n) + return h.Pop() +} +``` + +这个函数的行数只有 4。这使它变得非常容易和理解。将这个函数与 `ascii85` 包中的这个函数比较: + +```go +func (d *decoder) Read(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + if d.err != nil { + return 0, d.err + } + + for { + // Copy leftover output from last decode. + if len(d.out) > 0 { + n = copy(p, d.out) + d.out = d.out[n:] + return + } + + // Decode leftover input from last read. + var nn, nsrc, ndst int + if d.nbuf > 0 { + ndst, nsrc, d.err = Decode(d.outbuf[0:], d.buf[0:d.nbuf], d.readErr != nil) + if ndst > 0 { + d.out = d.outbuf[0:ndst] + d.nbuf = copy(d.buf[0:], d.buf[nsrc:d.nbuf]) + continue // copy out and return + } + if ndst == 0 && d.err == nil { + // Special case: input buffer is mostly filled with non-data bytes. + // Filter out such bytes to make room for more input. + off := 0 + for i := 0; i < d.nbuf; i++ { + if d.buf[i] > ' ' { + d.buf[off] = d.buf[i] + off++ + } + } + d.nbuf = off + } + } + + // Out of input, out of decoded output. Check errors. + if d.err != nil { + return 0, d.err + } + if d.readErr != nil { + d.err = d.readErr + return 0, d.err + } + + // Read more data. + nn, d.readErr = d.r.Read(d.buf[d.nbuf:]) + d.nbuf += nn + } +} +``` + +这个函数有 50 行。 + +多大的行数是合适的。在我看来,一个好的函数不应该超过 30 行。你应该能够在你的 IDE(代码编辑器)窗口中显示一个函数而无需向下滚动。在我的 IDE 上,我一次只能阅读 38 行。 + +### 7.4 降低圈复杂度 + +函数内部的行数并不足以判断其简单性。1976 年,Thomal J.McCabe 提出了一个有趣的概念,称为“圈复杂度”。这个想法是我们可以使用图论来检测程序中的复杂性。 + +我们可以用一个或多个条件语句组成一个函数。例如,我们可以有多个 if 语句。让我们看看下面的例子。 + +```go +package main + +import "fmt" + +func main() { + fmt.Println(foo(2, 3)) + fmt.Println(foo(11, 0)) + fmt.Println(foo(8, 12)) +} + +func foo(a, b int) int { + if a > 10 { + return a + } + if b > 10 { + return b + } + return b - a +} +``` + +在函数 `foo` 中,我们有两个输入参数 `a` 和 `b`。在函数体中,我们可以看到两个 if 语句。我们有两个条件(我们将 `a` 和 `b` 与特定数字进行比较)。 + +当我们运行我们的函数时,我们可以想象三个逻辑“路径”: + +- 第一个条件为真。不评估第二个条件。返回值为 `a`。 +- 第一个条件为假,第二个条件为真。返回值为 `b`。 +- 第一个条件为假,第二个条件也为假。返回值为 `b-a`。 + +我们有三个路径。你拥有的路径越多,理解该功能所需的努力就越多。 + +你获得的路径越多,你必须开发的单元测试就越多,以涵盖所有可能的情况。 + +#### 7.4.0.1 计算圈的数量 + +本节不是理解降低圈复杂度的概念所必需的。但是,你可能会发现了解“圈复杂度”背后的推理很有趣。 + +首先,每个程序都可以看作是一个图,图由节点和边组成。例如,在下图中你可以看到一个图。图由节点和边组成。每个节点将代表一组代码。边将代表程序中的控制流。 + +![](./imgs/edge_nodes.a895b46e.png) + +举一个简单的例子。我们有以下功能: + +```go +func bar(a int) { + fmt.Println("start of function") + if a > 2 { + fmt.Println("a is greater than 2") + return + } + fmt.Println("you got") + fmt.Println("bad luck") +} +``` + +![](./imgs/cyclo_complexity.a4f7ffbc.png) + +我们在这里用一组节点和边来表示程序。每个代码块由一个节点表示。这里非常重要。我们不是为每条语句添加一个节点,而是为每组跟在决策规则之后的语句添加一个节点。这里我们必须调用由单个节点表示的 `fmt.Println`。 + +让我们来计算该图上的节点和边: + +- 三个节点 +- 三条边 + +得到圈复杂度的公式是(对于一个函数): + +``` +V(G)= 边的数量 - 节点数量 + 2 +``` + +圈数表示为 V(G)。 + +``` +V(G)= 3 - 3 + 2 = 2 +``` + +这里圈数等于 2,这意味着我们的程序定义了两条线性无关的路径。当这个数字增加时,你的函数的复杂性也会增加: + +- 更多的路径意味着需要开发更多单元测试以完全覆盖你的代码。 +- 更多的路径意味着你的同事需要更多的脑力来理解你的代码。 + +**关于圈数的一些重要结论:** + +- 这个数字只取决于“图的决策结构”。 +- 当你向代码中添加功能语句时,它不会受到影响。 +- 如果在图中插入一条新边,那么圈数就会增加 1。 + +### 7.5 Halstead 指标 + +我想在本节中专门讨论所谓的“Halstead 复杂性指标”。 Maurice Howard Halstead 是计算机科学的先驱之一。他在 1977 年开发了度量标准,以使用源自其源代码的度量标准评估程序的复杂性。 + +- 程序的**词汇** +- 程序的**长度** +- 编写程序所需的**努力** +- 阅读和理解程序所需的**难度** + +Halstead 指标基于两个概念。运算符和操作数。程序由标记组成。这些标记是关键字、变量名称、方括号、大括号...等。这些标记可以分为两大类: + +1. **运算符:** + 1. 所有的关键字。(func, const, var,...) + 2. 成对的括号,成对的大括号。 ({},()) + 3. 所有的比较和逻辑运算符。 ( >,<,&&,||,...) +2. **操作数:** + 1. 标识符 (a, myVariableName, myConstantName, myFunction,...) + 2. 常量 (“this is a string”, 3, 22,...) + 3. 类型 (int, bool,...) + +从这两个定义中,我们可以提取一些基数(我们将使用它来计算 Halstead 指标): + +- $n_1$ 不同运算符的数量 +- $n_2$ 不同操作数的数量 +- $N_1$ 运算符的总数 +- $N_2$ 操作数的总数 + +让我们以一个示例程序来提取这四个数字: + +```go +func bar(a int) { + fmt.Println("start of function") + if a > 2 { + fmt.Println("a is greater than 2") + return + } + fmt.Println("you got") + fmt.Println("bad luck") +} +``` + +$n_1$ 是不同运算符的数量 + +- func, bar, (), {}, if, >, return + - 我们有 7 个不同的运算符 + +$n_2$ 是不同操作数的数量 + +- int, a, fmt.Println,start of function, 2, a is greater than 2,you got,bad luck + - 我们有 7 个不同的操作数 + +$N_1$ 是运算符的总数 + +- func, bar, (), () ,() ,() ,() ,{},{}, if, >, return + - 我们有总共 12 个运算符 + +$N_2$ 是操作数的总数 + +- a, a,start of function, 2, a is greater than 2,you got,bad luck, fmt.Println, fmt.Println,fmt.Println, fmt.Println, int + - 我们有总共 12 个操作数 + +来计算我们程序的 Halstead 指标: + +- **词汇** + + $n = n_1 + n_2 = 8 + 9 = 17$ + +- **长度** + + $N = N_1 + N_2 = 12 + 12 = 24$ + +- **难度** + + $\frac{n_1}{2} \times \frac{N_{2}}{n_{2}}=5.33$ + +- **体积** + + $length×log_2(词汇)=98.10$ + +- **努力** + + $难度 × 体积 = 523.20$ + +这些公式需要一些解释。 + +- 程序的**词汇**就像一些文章的词汇。对于一篇英语文章,我们可以说作者的词汇量是不同单词的总数。对于程序,词汇是不同运算符和操作数相加的总数。如果程序只使用关键字和非常少的标识符,它的词汇量将很少。相反,如果你的程序使用了很多标识符,词汇量就会增加。 +- 程序的**长度**是使用的运算符和操作数的总数。这里我们不计算不同的标记,而是计算标记的总数。 +- 程序的**难度**是编写程序和阅读程序所需时间的概念。该指标等于操作数数量的一半乘以运算符总数与不同的操作数数量之间的商。如果你的程序使用较少的操作数,则难度会降低。如果操作数的总数增加,难度也会增加(更多的比较运算符,更多的标识符,更多的类型需要处理和记忆)。 +- **努力**指标可以用来衡量编写程序所需的时间。 + + 编写程序的时间:E/18(单位:秒);在我们的示例中:29 秒(523,20 / 18)。 + +Halstead 还详细估计了 bug 的数量! + +$B=\frac{E^{2/3}}{3000}$ + +#### 7.5.0.1 评价 + +- 这些指标很有趣,但我们应该谨慎对待。 +- 它们强调我们编写的代码越多,我们的程序就会变得越复杂。 +- 简单、简短和愚蠢的代码也比过度设计的解决方案要好。 + +### 7.6 降低嵌套层数 + +这个建议必须与上一节连起来看。在编写程序时,可以引入嵌套语句,即在特定分支中执行的语句。让我们举个例子。我们这里有一个虚拟函数,它的第一个条件创建了两个分支。在第一个分支中,可以看到我们引入了另一个条件语句(if b < 2 ),它也将创建两个分支。 + +```go +// recommendation/nesting/main.go +//... +func nested(a, b int) { + if a > 1 { + if b < 2 { // nested condition + fmt.Println("action 1") + } else { + fmt.Println("action 2") + } + } else { + fmt.Println("action 3") + } + fmt.Println("action 4") +} +``` + +可以在序列图中把分支可视化(下图)。 + +![](./imgs/nested_stetements.f4da2548.png) + +我们可以再添加一层嵌套: + +```go +// recommendation/nesting/main.go +//... +func nested2(a, b int) { + if a > 1 { + if b < 2 { // nested condition + if a > 100 { + fmt.Println("action 1") + } else { + fmt.Println("action 2") + } + } else { + fmt.Println("action 3") + } + } else { + fmt.Println("action 4") + } + fmt.Println("action 5") +} +``` + +在下图中,你可以看到这层新的嵌套对序列图的影响。 + +![](./imgs/nested_statement_2.103058c3.png) + +你的嵌套越多,你的代码就会变得越复杂。一个通用的建议是限制嵌套数量。如果你发现自己无法避免嵌套,则应该创建另一个函数来支持这种复杂性: + +```go +// recommendation/nesting/main.go +//... +func nested3(a, b int) { + if a > 1 { + subFct1(a, b) + } else { + fmt.Println("action 4") + } + fmt.Println("action 5") +} + +func subFct1(a, b int) { + if b < 2 { // nested condition + if a > 100 { + fmt.Println("action 1") + } else { + fmt.Println("action 2") + } + } else { + fmt.Println("action 3") + } +} +``` + +## 8 要点 + +- 包名 + + - 简短: 不超过一个单词 + - 不使用复数 + - 小写 + - 包提供的服务的信息 + - 不使用公共工具包 + +- 接口 + + - 使用 interface 作为函数或方法的参数和字段类型 + - 小接口更好 + +- 源文件 + + - 将一个文件的名称命名为包名 + - 每个文件不超过 600 行 + - 一个文件 = 一份责任 + +- 错误处理 + + - 始终给错误添加上下文信息 + + - 从不忽略错误 + + - 谨慎使用 fatal 错误 + + - 编写可以容错的程序 + +- 函数和方法 + + - 一个函数一个目的 + - 名称简单 + - 限制长度(最大 100 行) + - 减少圈复杂度 + - 减少嵌套层数 + +*** + +1. 完全随意的数字 +2. https://github.com/pkg/errors, 现在是标准库的一部分 + +## 参考文献 + +[dave-design] Cheney, Dave. 2019. “Practical Go: Real World Advice for Writing Maintainable Go Programs.” https://dave.cheney.net/practical-go/presentations/qcon-china.html. + +[ds-design] Shuralyov, Dmitri. n.d. “Idiomatic Go.” https://dmitri.shuralyov.com/idiomatic-go. + +[dave-design] Cheney, Dave. 2019. “Practical Go: Real World Advice for Writing Maintainable Go Programs.” https://dave.cheney.net/practical-go/presentations/qcon-china.html. + +[dave-design] Cheney, Dave. 2019. “Practical Go: Real World Advice for Writing Maintainable Go Programs.” https://dave.cheney.net/practical-go/presentations/qcon-china.html. + +[mccabe1976complexity] McCabe, Thomas J. 1976. “A Complexity Measure.” IEEE Transactions on Software Engineering, no. 4: 308–20. + +[mccabe1976complexity] McCabe, Thomas J. 1976. “A Complexity Measure.” IEEE Transactions on Software Engineering, no. 4: 308–20. + diff --git a/chap-40-Design-Recommendations/imgs/advice_sources.5e438d63.png b/chap-40-Design-Recommendations/imgs/advice_sources.5e438d63.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e76238bbdcad0c71a5fab78d86e7c14e0cf29a Binary files /dev/null and b/chap-40-Design-Recommendations/imgs/advice_sources.5e438d63.png differ diff --git a/chap-40-Design-Recommendations/imgs/cyclo_complexity.a4f7ffbc.png b/chap-40-Design-Recommendations/imgs/cyclo_complexity.a4f7ffbc.png new file mode 100644 index 0000000000000000000000000000000000000000..3da7c56167e940459732527002bafc589c3b3ad4 Binary files /dev/null and b/chap-40-Design-Recommendations/imgs/cyclo_complexity.a4f7ffbc.png differ diff --git a/chap-40-Design-Recommendations/imgs/edge_nodes.a895b46e.png b/chap-40-Design-Recommendations/imgs/edge_nodes.a895b46e.png new file mode 100644 index 0000000000000000000000000000000000000000..f1487c51868e270dd7fca5c877b28559f57afbb8 Binary files /dev/null and b/chap-40-Design-Recommendations/imgs/edge_nodes.a895b46e.png differ diff --git a/chap-40-Design-Recommendations/imgs/interface_implementation.41c91f23.png b/chap-40-Design-Recommendations/imgs/interface_implementation.41c91f23.png new file mode 100644 index 0000000000000000000000000000000000000000..087376030d45cff4b3458251b3b4289e98eaf20c Binary files /dev/null and b/chap-40-Design-Recommendations/imgs/interface_implementation.41c91f23.png differ diff --git a/chap-40-Design-Recommendations/imgs/nested_statement_2.103058c3.png b/chap-40-Design-Recommendations/imgs/nested_statement_2.103058c3.png new file mode 100644 index 0000000000000000000000000000000000000000..f5b016ab6fb2a18c6aae5ee016a1d6fc31dea9c2 Binary files /dev/null and b/chap-40-Design-Recommendations/imgs/nested_statement_2.103058c3.png differ diff --git a/chap-40-Design-Recommendations/imgs/nested_stetements.f4da2548.png b/chap-40-Design-Recommendations/imgs/nested_stetements.f4da2548.png new file mode 100644 index 0000000000000000000000000000000000000000..e8942dba2888f10af880bea6b4e77af38353e212 Binary files /dev/null and b/chap-40-Design-Recommendations/imgs/nested_stetements.f4da2548.png differ