-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
32 changed files
with
377 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
|
||
# Tip #24 避免命名中的重复 | ||
|
||
> 原始链接:[Golang Tip #24: Avoid repetition in naming](https://twitter.com/func25/status/1759196416961032620) | ||
> | ||
在编写代码时,我们通常以动词开头给函数命名,比如 get、set、fetch、update、calculate 等等... | ||
|
||
## 1、包名与导出符号名称 | ||
在为对外可见(即在包外可见)的元素命名时,应避免重复使用包名。 | ||
|
||
否则,由于在包外使用这些符号时包名已经可见,会导致名称过长且更为重复: | ||
![](./images/024/1.png) | ||
|
||
这个“改进版”消除了重复。 | ||
|
||
当我们使用它时,语义自然:`chocolate.NewBar()`,清晰地创建了一个新的巧克力棒,没有冗余。 | ||
|
||
## 2、变量名与类型 | ||
我们通常不需要在变量名中重复其类型。 | ||
|
||
通常从上下文或使用方式即可清楚得知。 | ||
![](./images/024/2.png) | ||
|
||
然而,存在一些例外情况,应当予以考虑。 | ||
|
||
如果你同时拥有 `[]Car` 和 `map[string]Car`(可能是出于快速查找的目的),那么为了清晰起见,可以这样做。 | ||
|
||
“但如何命名呢?carList 和 carMap?” | ||
|
||
`CarList` 和 `carMap` 是不错的解决方案。 | ||
|
||
但我们可以通过指出数据的形式或状态使其更清晰,如:`[]Car cars` 和 `map[string]Car carLookup` | ||
|
||
以下为另一个示例: | ||
![](./images/024/3.png) | ||
|
||
在第二种方案中,显而易见其为字符串和输入值。 | ||
|
||
## 3、避免重复归结于“上下文” | ||
迄今为止我们讨论的所有内容都归结于“上下文” | ||
- 包名 | ||
- 方法名 | ||
- 类型名 | ||
- 文件名 | ||
|
||
这些应指导你选择既简单又具有信息性、避免不必要的重复的名称。 | ||
|
||
接下来讨论一些与“上下文”相关的其他情况: | ||
|
||
- 带有类型名的方法: | ||
![](./images/024/4.png) | ||
|
||
- 函数及其参数: | ||
![](./images/024/5.png) | ||
|
||
- 在函数内部,特别是在处理与函数目的密切相关参数或数据时,以一个不好的示例为例: | ||
![](./images/024/6.png) | ||
|
||
我们将函数名和局部变量名都进行重命名: | ||
![](./images/024/7.jpeg) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Tip #25 使用空标识符(_)明确忽略值,而不是无声地忽略它们 | ||
|
||
> 原始链接:[Golang Tip #26: Explicitly ignore values with blank identifier (_) instead of silently ignoring them](https://twitter.com/func25/status/1759900581315870883) | ||
> | ||
在编写Go语言代码时,函数可能会返回一些你可能想使用也可能不想使用的值。 | ||
|
||
在这种情况下,有两种处理方式: | ||
|
||
- 隐式:调用函数但不将其返回值分配给任何变量,这种方式简短且简洁。 | ||
![](./images/026/1.png) | ||
|
||
- 显式:稍显冗长一些,通过将返回值分配给空标识符 _ 来显式地忽略它。 | ||
|
||
> “为什么即使显式方式更冗长且不如隐式方式简洁,我们仍然更倾向于使用它呢?” | ||
在编程中,清晰性总是优于简洁性。 | ||
|
||
这种显式方式清楚地表明我们有意忽略了 PerformOperation() 的返回值。 | ||
|
||
使用 _ = 向其他开发者(或我们自己在不久的将来)发出信号,表明这种省略是故意的,而不是疏忽。 | ||
|
||
> “那错误怎么办呢?” | ||
无论如何,如果函数返回一个错误,一定要处理它,或者至少记录它。 | ||
|
||
同时,为了更好地提高清晰性,可以考虑添加注释来解释原因。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Tip #35 转换字符串时优先使用 strconv 而非 fmt | ||
|
||
> 原始链接:[Golang Tip #35: Prefer strconv over fmt for converting to/from string.](https://twitter.com/func25/status/1763187911942660330) | ||
> | ||
当需要将数字转换为字符串时,选择合适的工具可以加快处理速度。 | ||
|
||
**strconv** 包专门为这个场景而设计,每一点性能提升和内存节省都很重要。 | ||
|
||
我们来看一个简单的基准测试: | ||
|
||
![](./images/035/035_01.png) | ||
|
||
基准测试显示出显着的性能差异。 | ||
|
||
![](./images/035/035_02.png) | ||
|
||
*(虽然我不确定编译器是否做了优化,但两者的上下文是相同的)* | ||
|
||
- **strconv** 的函数是为特定的转换任务设计的,这使得它们能比更通用的 **fmt** 函数执行得更快。 | ||
- **fmt.Sprint** 函数及其变体需要通过反射来识别其正在处理的类型,并确定如何将其格式化为字符串。 | ||
|
||
![](./images/035/035_03.png) | ||
|
||
这个反射过程并非无成本,它既增加了时间也增加了内存开销。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Tip #41 将你结构体中的字段按从大到小的顺序排列 | ||
|
||
> 原始链接:[Golang Tip #41: Sort your fields in your struct from largest to smallest.](https://twitter.com/func25/status/1765371933053612110) | ||
> | ||
*(我之前发过一些关于字段填充和对齐的推文,但这次我想把它作为一条 tip 分享)* | ||
|
||
结构体中字段的顺序确实会影响到结构体自身的大小,这意味着我们可以利用这一点来优化内存的使用,不是吗? | ||
|
||
让我们来看一个示例(暂时忽略每个字段的注释): | ||
|
||
![example-1](./images/041/041_01.png) | ||
|
||
结构体 StructA 使用了 32 字节,而 OptimizedStructA 仅需要 16 字节。 | ||
|
||
为了理解为什么具有相同字段的两个结构体大小不同,让我们探讨一下字段的对齐和填充: | ||
|
||
- **对齐**:数据类型根据其大小具有特定的对齐要求。 | ||
|
||
例如,一个 int32 类型可能需要在 4 字节边界上对齐,这意味着它的启始内存地址应该是 4 的倍数。 | ||
|
||
- **填充**:为了满足对齐要求,编译器可能会在结构体字段之间插入未使用的空间(填充)。 | ||
|
||
让我们看看 StructA 的内部表示,它的大小为 8x4 字节,让我们尝试使用上述的思路来解释: | ||
|
||
以下是对 StructA 每个字段的解释: | ||
|
||
![example-1](./images/041/041_02.png) | ||
|
||
- A(byte):占用 1 字节,但由于下一个字段 B 需要 4 字节对齐,因此在 A 后面有 3 字节的填充以正确对齐 B。 | ||
- B(int32):4 字节,后面不需要填充,因为下一个字段 C 是一个字节。 | ||
- C(byte):同样占用 1 字节,但为了对齐 D(需要 8 字节对齐),在 C 后面添加了 7 字节的填充。 | ||
- D(int64):8 字节,完全利用了它的空间。 | ||
- E(byte):最后一个字节,在内存中直接跟在 “D” 的后面,根据上下文可能会导致在结构体的末尾添加额外的填充以将整个结构体的大小对齐到边界。 | ||
|
||
现在来看看 OptimizedStructA: | ||
|
||
![example-1](./images/041/041_03.png) | ||
|
||
- D(int64):最先被放置以利用其 8 字节对齐的要求,无需前置填充。 | ||
- B(int32):紧随其后,在 D 后自然对齐到 4 字节边界。 | ||
- A,C,E(byte):随后组合在一起,由于它们是单字节类型,它们之间不需要额外的填充。 | ||
|
||
通过将字段按从大到小的顺序排列,我们可以让所需的填充最小化,从而减少结构体(和内存)的总大小。 | ||
|
||
像 **betteralign** 这样的工具可以检测到效率低下的对齐方式,并可能帮助自动重新排序以提高效率: | ||
|
||
https://github.com/dkorunic/betteralign | ||
|
||
需要注意的是,为了效率而重新排序并**不**总是适用或必要的。 | ||
|
||
保持结构体字段按照其使用方式或重要性进行有意义的顺序排列,即使这种方式并不使用最少的内存,也可以使代码更易于阅读和使用。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Tip #42 单点错误处理,降低噪音 | ||
|
||
> 原始链接:[Golang Tip #42: Single Touch Error Handling, Less Noise.](https://twitter.com/func25/status/1765716456501817766) | ||
> | ||
这是我以前处理错误的方式,假设我们有一个函数 A 调用函数 B,两个函数都处理错误,如下所示: | ||
|
||
![](./images/042/042_01.png) | ||
|
||
- 当 B 产生一条错误时,将问题记录日志,并将错误传递给 A。 | ||
- A 收到此错误后,重复同样的操作:记录日志,也可能将错误传递给上层的调用链。 | ||
|
||
### 这有什么问题? | ||
|
||
这似乎是彻底的错误处理方式,因为我们可以从一条条日志中追溯错误来源,但实际上只会制造噪音。 | ||
|
||
这些问题是: | ||
|
||
- **重复记录日志:** 这会在日志文件中制造噪音,使得诊断问题变困难,因为相同的错误被记录了多次。 | ||
|
||
- **错误处理变复杂:** 它增加了错误处理逻辑的复杂度。 | ||
|
||
- **潜在的其他错误:** 多次错误处理意味着更多的代码,更多的代码意味着更多的潜在 bug。 | ||
|
||
一条错误,只考虑处理一次,但是如何有效的做到这点呢? | ||
|
||
### 更好的解决方案 | ||
|
||
一个更好的处理方法是决定在本层处理错误,还是将错误返回给上层处理(但不要同时都处理)。 | ||
|
||
如果你选择返回错误不记录日志,考虑给错误添加更多上下文(参考[Tip #38](./038.md) , 原链接[https://twitter.com/func25/status/...](https://twitter.com/func25/status/1764265328165753176)) | ||
|
||
![](./images/042/042_02.png) | ||
|
||
让调用者来决定如何处理错误,是记录日志,产生恐慌,包装额外的上下文,还是采取一些纠正措施。 |
Oops, something went wrong.