读书笔记-Common Go Mistakes


原书地址

https://100go.co/

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
}

使用类型嵌套的原则:

  1. 不要为了简化字段访问而使用类型嵌入,例如Foo.Baz()而不是Foo.Bar.Baz()
  2. 他不能够将嵌入的类型中原本不可访问的数据(即不能让小写,不可被包外数据访问的类型变得可以访问)

尽可能的使用 function option模式

尽量参考 Project Layout 来组织整个项目

尽量不要创建 common 这类过于宽泛的包名

提供代码文档,特别是导出元素

数据类型

slice

参考:https://anubis.cafe/523eba

map

  1. map 本身是强制无序的

  2. map 在条件允许的情况,进行 cap 预分配是比较好的

  3. 一个map的buckets占用的内存只会增长,不会缩减。

    因此针对生命周期超过一个函数调用的有一下几点建议:

    1. 不要直接存储数据,存储指针(推荐:( •̀ ω •́ )✧)
    2. 如果数据量先增大后减小,可以考虑重新创建一个
  4. 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)
	}
}

如果本文帮助到了你,帮我点个广告可以咩(o′┏▽┓`o)


评论
 上一篇
经典的 Go 多线程编程题总结 经典的 Go 多线程编程题总结
本文通过具体的Go语言代码示例,详细介绍了实现线程同步和并发数据同步的多种方法。从基本的线程顺序控制到复杂的生产者消费者模型,这些示例覆盖了并发编程中的常见需求。通过通道、WaitGroup、Mutex、Atomic和条件变量等同步原语。
2024-02-12
下一篇 
使用中间件统计 gin 响应结果 使用中间件统计 gin 响应结果
如何使用中间件来统计 gin 的响应结果,主要用于 pv,uv 等统计
2024-02-07
  目录