100 Go Mistakes and How to Avoid Them

https://100go.co/zh/

100 Go Mistakes and How to Avoid Them 目前还没有中文版 有中文版了 100个Go语言典型错误

仓库地址 teivah/100-go-mistakes

readme.md中推荐的中文博客:

深度阅读之《100 Go Mistakes and How to Avoid Them》

100 Go Mistakes 随记 - 01 Code and project organization


鸟窝大佬整理的 cheatsheet原文:100个Go编程错误小抄




27. 初始化 map 时,指定一个长度

它能给 runtime 以提示,这样后续可以减少重新分配元素的开销。并且要注意:这个长度并不是说 map 只能放这么多元素,这里面有一个公式会计算。

28. map 和内存泄漏

map 的 buckets 数只会增,不会降。所以当在流量冲击后,map 的 buckets 数扩容到了一个新高度,之后即使把元素都删除了也无济于事。内存占用还是在,因为基础的 buckets 占用的内存不会少。

关于这一点,之前专门写过一篇Go map 竟然也会发生内存泄漏?去讲,私以为比书里讲得更详细。

Go map 竟然也会发生内存泄漏?

和我那篇 流量突增后性能下降 原理类似

todo

不要边遍历 map 边写入 key
在遍历 map 的过程中,新写入的 key 可能被遍历出来,也可能不被遍历出来,可能会与预期的行为不符,因此不要边遍历边写入。

34

break 可以作用于 for, select, switch
break 只能跳出一重循环,因此要注意,break 是否跳到了你预想的地方。可以用 break with label 来解决。毕竟标准库里也这样用了:


33

golang 滥用嵌入类型,什么意思?


在Go语言中,嵌入类型(Embedded Types)是一种特性,它允许一个结构体类型(或者其他具有命名类型)将另一个类型的字段和方法包含在内,而无需显式地声明这些字段和方法。

滥用嵌入类型指的是在设计和编写Go代码时过度使用嵌入类型的做法,导致代码变得复杂、难以维护和理解。这种滥用通常是由于开发者过度追求代码重用或者在模拟继承(Go中没有传统的继承机制)的过程中过度使用嵌入类型。

以下是一些可能导致滥用嵌入类型的情况:

  1. 多层嵌套:过度嵌套结构体,导致代码层级复杂,难以追踪字段和方法的来源。

  2. 嵌入大型依赖:在结构体中嵌入大型依赖,使结构体变得庞大而笨重,影响性能和可维护性。

  3. 钻石问题:当多个结构体嵌入同一个类型时,可能会出现“钻石问题”,即多个嵌入层级之间产生冲突或不明确的字段或方法。

  4. 过度使用匿名嵌入:将所有字段和方法都使用匿名嵌入,使得代码难以理解其组成和层次结构。

为了避免滥用嵌入类型,开发者应该谨慎地使用嵌入,仅在确实有必要时使用,并且要注意代码结构的清晰性和可读性。在某些情况下,使用接口或组合可能更合适,这样可以更好地表示代码的意图和关系,同时保持代码的简洁和易于理解。



50


golang中 errors.As 和 errors.Is有何区别?举例说明


在Go语言中,errors.Aserrors.Is都是用于错误处理的函数,它们主要用于在处理错误时检查错误类型或者判断一个错误是否是特定类型的错误。它们在处理错误链或多个嵌套错误时非常有用。

  1. errors.As函数:

errors.As函数用于检查错误是否可以被转换为指定类型的错误,并将转换后的错误存储在目标变量中。它的函数签名如下:

1
func As(err error, target interface{}) bool
  • err:要检查的错误。
  • target:目标变量,必须是一个指向指定错误类型的指针。

如果错误err可以被转换为target指定的错误类型,则errors.As函数会将错误转换,并返回true;否则,返回false。

下面是一个使用errors.As的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"errors"
"fmt"
)

type CustomError struct {
Message string
}

func main() {
err := CustomError{Message: "custom error"}
var targetErr *CustomError

if errors.As(err, &targetErr) {
fmt.Println("Successfully converted err to CustomError.")
fmt.Println("Error message:", targetErr.Message)
} else {
fmt.Println("Failed to convert err to CustomError.")
}
}
  1. errors.Is函数:

errors.Is函数用于检查错误链中是否存在指定类型的错误。它的函数签名如下:

1
func Is(err, target error) bool
  • err:要检查的错误。
  • target:指定类型的目标错误。

如果错误链中存在target指定类型的错误,则errors.Is函数返回true;否则,返回false。

下面是一个使用errors.Is的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"errors"
"fmt"
)

type CustomError struct {
Message string
}

func main() {
err := errors.New("some other error")
targetErr := CustomError{Message: "custom error"}

if errors.Is(err, targetErr) {
fmt.Println("Found target error in the error chain.")
} else {
fmt.Println("Target error not found in the error chain.")
}
}

在这个例子中,err是一个普通的错误,而targetErr是自定义的错误类型。errors.Is函数会检查err的错误链中是否包含与targetErr类型相匹配的错误。由于错误链中没有包含与targetErr匹配的错误,所以输出会是 “Target error not found in the error chain.”。

总结:

  • errors.As用于将错误转换为特定类型,并判断是否成功转换。
  • errors.Is用于检查错误链中是否存在特定类型的错误。




82. 不对测试进行分类 (build tags, 环境变量,短模式)

对测试进行必要的分类,可以借助 build tags、环境变量以及短模式,来使得测试过程更加高效。你可以使用 build tags 或环境变量来创建测试类别(例如单元测试与集成测试),并区分短测试与长时间测试,来决定执行哪种类型的。

ps: 了解下 go build tags,以及 go test -short。

我给time包添加example时短模式, Ian提到了这个问题

go test -short 有何作用?

go test -short 是 Go 语言测试工具中的一个选项,用于在运行测试时跳过那些具有特定标记的长时间运行的测试。

详细说明:

在 Go 测试中,开发者可以通过 testing.Short() 函数来检查测试是否是在 “short” 模式下运行。如果测试是以 -short 选项运行的,testing.Short() 会返回 true,根据此,可以有选择地跳过某些耗时较长的测试。

使用场景:

  • 当你编写了一些测试,但其中的某些测试运行时间较长(如需要访问网络、数据库、大量计算等),你可以选择在这些测试中使用 if testing.Short() { t.Skip("Skipping test in short mode.") } 来跳过它们。
  • 这样,开发者在日常开发中,可以通过 go test -short 快速地运行短时间内能够完成的测试,而跳过那些更耗时的测试,确保开发循环更高效。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package mypackage

import (
"testing"
)

func TestSomethingQuick(t *testing.T) {
// 这个测试很快,不会被跳过
t.Log("This is a quick test")
}

func TestSomethingSlow(t *testing.T) {
if testing.Short() {
t.Skip("Skipping slow test in short mode.")
}
// 假设这里有一个比较耗时的操作
t.Log("This is a slow test")
}

运行方式:

  • 运行所有测试(包括耗时的测试):

    1
    go test
  • 只运行短时间的测试(跳过耗时的测试):

    1
    go test -short

总结:

go test -short 是用于跳过长时间运行测试的标志,帮助开发者在快速测试时跳过不必要的耗时操作,以加快开发和测试的速度。

666

golang中函数选项模式什么意思?


在Go语言中,函数选项模式(Function Options Pattern)是一种用于构建函数的设计模式。它主要用于在函数调用时,通过传递一系列选项参数来配置函数的行为,从而使函数在不同场景下具备更大的灵活性和可扩展性。

在传统的函数调用中,如果一个函数有多个可选参数,我们通常会使用函数重载或者为每个可选参数定义默认值的方式来处理。然而,这种方式在参数数量增加时会导致函数接口变得复杂,并且容易产生混淆。

函数选项模式的实现通过将选项封装成一个个函数,每个函数设置一个特定的选项,并返回一个函数选项对象。在调用函数时,通过传递这些选项函数,可以方便地配置函数的行为。

下面是一个简单的示例来说明函数选项模式的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import "fmt"

type Options struct {
OptionA int
OptionB string
OptionC bool
}

type Configurable struct {
// ...
}

func NewConfigurable(options ...func(*Options)) *Configurable {
opts := &Options{}
for _, opt := range options {
opt(opts)
}

// 使用opts进行Configurable对象的初始化
// ...

return &Configurable{}
}

func WithOptionA(value int) func(*Options) {
return func(opts *Options) {
opts.OptionA = value
}
}

func WithOptionB(value string) func(*Options) {
return func(opts *Options) {
opts.OptionB = value
}
}

func WithOptionC(value bool) func(*Options) {
return func(opts *Options) {
opts.OptionC = value
}
}

func main() {
c := NewConfigurable(
WithOptionA(42),
WithOptionB("hello"),
WithOptionC(true),
)

// 使用配置好的c对象
// ...
}

在上面的示例中,我们定义了一个Configurable结构体,它包含一些可选的配置项。然后我们定义了一个Options结构体用于存储选项的值,以及一系列的选项函数(例如WithOptionAWithOptionBWithOptionC)用于设置Options结构体的值。NewConfigurable函数接收一系列的选项函数,并根据这些选项函数配置Configurable对象的初始化。

使用函数选项模式,我们可以在不改变函数接口的前提下,轻松地添加或修改函数的选项,并且调用方可以根据需要选择传递哪些选项,避免了函数参数列表变得冗长和复杂的问题。


更具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import "fmt"

type Options struct {
OptionA int
OptionB string
OptionC bool
}

type Configurable struct {
OptionA int
OptionB string
OptionC bool
}

func NewConfigurable(options ...func(*Options)) *Configurable {
opts := &Options{}
for _, opt := range options {
opt(opts)
}

// 根据 Options 的值进行 Configurable 对象的初始化
c := &Configurable{
OptionA: opts.OptionA,
OptionB: opts.OptionB,
OptionC: opts.OptionC,
}

return c
}

func WithOptionA(value int) func(*Options) {
return func(opts *Options) {
opts.OptionA = value
}
}

func WithOptionB(value string) func(*Options) {
return func(opts *Options) {
opts.OptionB = value
}
}

func WithOptionC(value bool) func(*Options) {
return func(opts *Options) {
opts.OptionC = value
}
}

func main() {
c := NewConfigurable(
WithOptionA(42),
WithOptionB("hello"),
WithOptionC(true),
)

// 使用配置好的 c 对象
fmt.Println("OptionA:", c.OptionA)
fmt.Println("OptionB:", c.OptionB)
fmt.Println("OptionC:", c.OptionC)
}




84 未使用测试执行模式 (−parallel − shuffle)


golang中 −parallel − shuffle是什么?有何作用?


在 Go 语言的 testing 包中,-parallel-shuffle 是两个用于控制测试执行行为的标记(flag)。

  1. -parallel 标记:

-parallel 标记用于指示测试框架在运行测试时是否允许并行执行测试函数。在默认情况下,Go 测试框架会按照测试文件中定义的顺序依次运行测试函数,但当使用 -parallel 标记时,测试函数将有可能以并发的方式执行。

例如,你可以在运行测试时使用以下命令来开启并发执行:

1
go test -parallel 4

其中,4 表示并发执行的 goroutine 数量。通过并发执行测试,可以加快测试的执行速度,特别是当测试量较大时。

需要注意的是,并发执行测试函数可能导致一些并发问题,如共享资源的冲突或竞态条件。因此,在并发测试时,确保测试函数之间是相互独立的,避免引入不必要的并发问题。

  1. -shuffle 标记:

-shuffle 标记用于指示测试框架在运行测试时是否打乱测试函数的执行顺序。默认情况下,Go 测试框架会按照测试文件中定义的顺序依次运行测试函数,但使用 -shuffle 标记后,测试函数的执行顺序将被随机打乱。

例如,你可以在运行测试时使用以下命令来随机打乱测试函数的执行顺序:

1
go test -shuffle

通过随机打乱测试函数的执行顺序,可以帮助发现一些隐藏的依赖问题或者测试用例编写的依赖于特定执行顺序的情况。这样可以确保测试函数在不同顺序下都能正确运行,提高测试的可靠性。

总结:

  • -parallel 标记用于允许并发执行测试函数,加快测试的执行速度。
  • -shuffle 标记用于随机打乱测试函数的执行顺序,帮助发现隐藏的依赖问题,提高测试的可靠性。



85 未使用表格驱动型测试


golang中表格驱动型测试 是什么?请举例说明


表格驱动型测试(Table-Driven Testing)是一种测试方法,它通过创建一个包含输入数据和预期输出数据的表格,以及测试函数来进行测试。这种方法可以更全面地覆盖不同的测试案例,并使测试代码更易于维护和扩展。

在 Go 语言中,表格驱动型测试通常用于测试函数的不同输入组合,并验证函数是否按预期产生正确的输出。

下面是一个简单的示例,说明如何使用表格驱动型测试来测试一个函数:

假设我们有一个名为 Add 的函数,用于将两个整数相加。我们希望测试这个函数的各种输入组合,包括正整数、负整数和零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package mathutil

import "testing"

func Add(a, b int) int {
return a + b
}

func TestAdd(t *testing.T) {
testCases := []struct {
name string
inputA int
inputB int
expected int
}{
{"Positive numbers", 2, 3, 5},
{"Negative numbers", -5, -10, -15},
{"Zero and positive", 0, 8, 8},
{"Zero and negative", 0, -5, -5},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := Add(tc.inputA, tc.inputB)
if result != tc.expected {
t.Errorf("Expected %d, but got %d", tc.expected, result)
}
})
}
}

在上面的示例中,我们首先定义了一个包含测试案例的结构体切片 testCases,其中每个测试案例包含一个测试名称、输入的两个整数和预期输出的整数。然后,我们使用一个循环迭代所有测试案例,并在每个测试案例中调用 Add 函数,并对输出进行断言。

运行这个测试函数时,Go 测试框架会遍历表格中的每个测试案例,并为每个案例创建一个单独的子测试,以便我们可以清楚地看到每个案例的测试结果。

使用表格驱动型测试的好处在于,我们可以轻松地添加更多的测试案例,而不需要修改测试函数的代码。这样,我们可以确保我们的函数在不同的输入组合下都能正确运行,同时测试代码也更加清晰和易于维护。




86 在单元测试中休眠 (默认超时 30 秒? 鸟窝大佬会修改)

100 个 Go 错误以及如何避免:11~12

Go 的单元测试和 Benchmark 都有超时时间,默认是10分钟,可以通过 -timeout 来修改。

捉虫大师:我好像发现了一个Go的Bug?

默认超时 30 秒这一项,这个好像是vscode默认的。go本身的话,ut/benchmark有个全局超时时间是10min

VSCode Golang 单元测试时 timeout 30s超时如何解决 以及这篇


在单元测试中休眠s有问题吗? go的单测默认超时 30 秒吗?


在单元测试中使用休眠是不推荐的做法,因为它会导致测试的执行时间变得不确定,并且会增加测试运行的时间。测试应该是自动化的,可重复的,以便在开发过程中快速运行和验证代码的正确性。使用休眠会导致测试变得不稳定,并可能隐藏真正的问题。

如果在测试中有需要等待的场景,可以使用 Go 语言中的time.Sleep(),但这样的测试会导致测试变慢,并且可能会影响测试的稳定性。

相反,Go语言的测试框架提供了一种更优雅的方法来处理等待和超时的情况。可以使用 testing.TTimeout 方法来指定单个测试用例的超时时间。如果在指定的超时时间内测试函数未完成,测试框架会将该测试标记为失败。

默认情况下,Go的单元测试框架确实有一个全局的超时时间,默认为10分钟,而不是30秒。这个默认超时时间可以在测试框架启动时通过 -timeout 标记进行设置。例如:

1
go test -timeout 5m

上面的命令会将全局的超时时间设置为5分钟。如果在这个时间内测试用例未完成,测试框架会将该测试标记为失败。

如果需要为某个特定的测试用例指定不同的超时时间,可以使用 testing.TTimeout 方法,如下所示:

1
2
3
4
5
func TestMyFunction(t *testing.T) {
t.Timeout(1 * time.Minute)

// 测试代码...
}

在上面的示例中,我们为 TestMyFunction 测试用例指定了1分钟的超时时间。

总结:

  • 在单元测试中使用休眠是不推荐的,会导致测试不稳定和执行时间不确定。
  • Go的单元测试默认有一个全局的超时时间,默认为10分钟,并且可以通过 -timeout 标记进行设置。
  • 可以使用 testing.TTimeout 方法为特定的测试用例指定超时时间。


88. 不使用测试相关的工具包 (httptest 和 iotest)

这个 httptest 包对处理 HTTP 应用程序很有帮助。它提供了一组实用程序来测试客户端和服务器。

这个 iotest 包有助于编写 io.Reader 并测试应用程序是否能够容忍错误。


90:未探索所有的 Go 测试特性


Go语言中常见100问题-#90 Not exploring all the Go testing features

go test 测试用例那些事

1 代码覆盖率

2 从一个不同的包进行测试

3 工具函数

4 设置和拆卸

TestMain函数可以在运行所有测试前执行一些初始化逻辑(如创建数据库链接),或所有测试都运行结束之后执行一些清理逻辑(释放数据库连接),如果测试文件中定义了这个函数,则go test命令会直接运行这个函数,否则go test会创建一个默认的TestMain()函数。这个函数的默认行为就是运行文件中定义的测试。我们自定义TestMain()函数时,也需要手动调用m.Run()方法运行测试函数,否则测试函数不会运行。

默认的TestMain函数如下:

1
2
3
func TestMain(m *testing.M) {
os.Exit(m.Run())
}

编写自定义的TestMain函数,在测试函数执行前执行后做一些其它逻辑。

1
2
3
4
5
6
func TestMain(m *testing.M) {
setupMySQL()
code := m.Run()
teardownMySQL()
os.Exit(code)
}