文章目录

  • 1.不需要指向 interface 的指针
  • 2.编译期验证 interface 合理性
  • 3.接收器为值和指针实现接口的区别
  • 4.零值 Mutex 是有效的
  • 5.在边界处拷贝 slice 和 map
  • 6.使用 defer 释放资源
  • 7.channel size 要么是 1 或 0
  • 8.枚举从 1 开始
  • 9.使用 time 处理时间
  • 10.关于 error 的处理
    • 10.1 错误类型
    • 10.2 错误包装
  • 11.使用 go.uber.org/atomic 包
  • 12.避免可变全局变量
  • 13.避免在公共结构中嵌入类型
  • 14.避免使用内置标识符
  • 15.主函数退出方式
    • 15.1 在 main() 中退出程序
    • 15.2 退出一次
  • 16.不要在 for 循环中使用 defer
  • 参考文献

1.不需要指向 interface 的指针

我们几乎不需要使用指向接口的指针,应该将接口直接赋值给接口,这样在传递过程中,实质上传递的底层数据仍然是指针,即不存在值拷贝的情况。

type Foo struct {T string
}func bar(i interface{}) {...
}var foo interface{} = Foo{...}// Bad
bar(&foo)// Good
bar(foo)

为什么可以这样,因为接口实质上在底层用两个字段表示:
(1)一个指向某些特定类型信息的指针;
(2)一个指向具体数据的指针。如果存储的数据是指针,则直接存储。如果存储的数据是一个值,则存储指向该值的指针。

具体可以看下 Go 源码 runtime 包两种接口类型的定义。

一种是带有一组方法的接口runtime.iface

type iface struct {tab  *itabdata unsafe.Pointer
}

一种是不含任何方法的空接口runtime.eface

type eface struct {_type *_typedata  unsafe.Pointer
}

我们可以看下接口变量的内存宽度。

var foo interface{} = Foo{...}
fmt.Println(unsafe.Sizeof(foo)) // 16

当然,凡事无绝对。如果需要修改接口变量本身,那么应该使用指向接口变量的指针,当然你必须清楚自己在干什么。

类似地,在了解 map、slice、channel 的底层结构后,我们应该知道在传递过程中一般也不需要使用指向它们的指针。

2.编译期验证 interface 合理性

在编译时验证接口的符合性,这包括:
(1)导出类型的部分 API 实现了接口;
(2)导出或非导出类型实现了接口且属于某类型的一部分(匿名嵌套);
(3)任何其他情况都违反接口合理性检查,终止编译并通知给用户。

上面这 3 条是编译器对接口的检查机制, 错误地使用接口会在编译期报错. 所以可以利用这个机制让部分问题在编译期暴露。

// Bad
type Handler struct {// ...
}func (h *Handler) ServeHTTP(w http.ResponseWriter,r *http.Request,
) {// ...
}// Good
type Handler struct {// ...
}var _ http.Handler = (*Handler)(nil)func (h *Handler) ServeHTTP(w http.ResponseWriter,r *http.Request,
) {// ...
}

如果*Handlerhttp.Handler的接口不匹配, 那么语句var _ http.Handler = (*Handler)(nil)将无法编译通过。

赋值的右边应该是断言类型的零值。 对于指针类型(如 *Handler)、切片和映射是 nil;对于结构类型是空结构。

type LogHandler struct {h   http.Handlerlog *zap.Logger
}var _ http.Handler = LogHandler{}func (h LogHandler) ServeHTTP(w http.ResponseWriter,r *http.Request,
) {// ...
}

3.接收器为值和指针实现接口的区别

接收器为值的方法和接收器为指针的方法在调用方式上是有区别的。

使用值接收器的方法既可以通过值调用,也可以通过指针调用。带指针接收器的方法只能通过指针或 addressable values 调用。

type S struct {data string
}func (s S) Read() string {return s.data
}func (s *S) Write(str string) {s.data = str
}sVals := map[int]S{1: {"A"}}// 你只能通过值调用 Read
sVals[1].Read()// 编译不通过,因为无法对 sVals[1] 取址
//  sVals[1].Write("test")sPtrs := map[int]*S{1: {"A"}}// 指针既可以调用 Read,也可以调用 Write
sPtrs[1].Read()
sPtrs[1].Write("test")

类似的,使用值接收器和指针接收器来实现接口,在判断与接口是否匹配时,也存在一定的区别。

type F interface {f()
}type S1 struct{}func (s S1) f() {}type S2 struct{}func (s *S2) f() {}s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}var i F
i = s1Val
i = s1Ptr
i = s2Ptr// 下面代码无法通过编译。因为 s2Val 是一个值,而 S2 的 f 方法中没有使用值接收器
// i = s2Val

为什么可取地址的值可以调用接收器为指针的方法,但是不能将指针接收器方法集和接口匹配的值赋给接口呢?

在 Effective Go 中一节 Pointers vs. Values 做了说明,因为接收器为指针的方法会修改接受器,将一个值赋给接口会产生一个拷贝,将接收器为指针的方法应用到拷贝身上,并不会使修改生效。为了避免产生这种错误的行为,便禁止将值赋给和指针接收器方法集匹配的接口。

4.零值 Mutex 是有效的

零值 sync.Mutex 和 sync.RWMutex 是有效的,所以指向 mutex 的指针基本是不必要的。

// Bad
mu := new(sync.Mutex)
mu.Lock()// Good
var mu sync.Mutex
mu.Lock()

Mutex 作为其他结构体一个字段时,应该使用值而不是指针。即使该结构体不被导出,也不要直接把 Mutex 嵌入到结构体中。

// Bad
type sMap struct {sync.Mutexdata map[string]string
}func NewSMap() *sMap {return &sMap{data: make(map[string]string),}
}func (m *sMap) Get(k string) string {m.Lock()defer m.Unlock()return m.data[k]
}// Good
type sMap struct {mutex sync.Mutexdata map[string]string
}func NewSMap() *sMap {return &sMap{data: make(map[string]string),}
}func (m *SMap) Get(k string) string {m.mutex.Lock()defer m.mutex.Unlock()return m.data[k]
}

Bad 部分会导致字段 Mutex、Lock 和 Unlock 方法是 sMap 导出的字段和 API,但又没有明确说明,既导致了模糊的文档,又泄露了 sMap 的实现细节。Good 部分可以做到字段 mutex 及其方法是 sMap 的实现细节,对其调用者不可见。

5.在边界处拷贝 slice 和 map

slice 和 map 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。

在 Go 源码文件 src/runtime/slice.go 我们可以找到切片的底层数据结构 runtime.slice:

type slice struct {array unsafe.Pointerlen   intcap   int
}

在 Go 源码文件 src/runtime/map.go 我们可以找到 map 底层核心数据结构 runtime.hmap:

// A header for a Go map.
type hmap struct {// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.// Make sure this stays in sync with the compiler's definition.count     int // # live cells == size of map.  Must be first (used by len() builtin)flags     uint8B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for detailshash0     uint32 // hash seedbuckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growingnevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)extra *mapextra // optional fields
}// mapextra holds fields that are not present on all maps.
type mapextra struct {// If both key and elem do not contain pointers and are inline, then we mark bucket// type as containing no pointers. This avoids scanning such maps.// However, bmap.overflow is a pointer. In order to keep overflow buckets// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.// overflow and oldoverflow are only used if key and elem do not contain pointers.// overflow contains overflow buckets for hmap.buckets.// oldoverflow contains overflow buckets for hmap.oldbuckets.// The indirection allows to store a pointer to the slice in hiter.overflow    *[]*bmapoldoverflow *[]*bmap// nextOverflow holds a pointer to a free overflow bucket.nextOverflow *bmap
}

在接收 slice 和 map 时,请记住当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。

// Bas
func (d *Driver) SetTrips(trips []Trip) {d.trips = trips
}trips := ...
d.SetTrips(trips)// 你是要修改 d.trips 吗?
trips[0] = ...// Good
func (d *Driver) SetTrips(trips []Trip) {d.trips = make([]Trip, len(trips))copy(d.trips, trips)
}trips := ...
d.SetTrips(trips)// 这里我们修改 trips[0],但不会影响 d.trips
trips[0] = ...

在返回 slice 和 map 时,同样的,请注意用户对暴露内部状态的 map 或 slice 的修改。

// Bad
type Stats struct {mu sync.Mutexcounters map[string]int
}// Snapshot 返回当前状态。
func (s *Stats) Snapshot() map[string]int {s.mu.Lock()defer s.mu.Unlock()return s.counters
}// snapshot 不再受互斥锁保护,可能会引发并发读写的错误
snapshot := stats.Snapshot()// Good
type Stats struct {mu sync.Mutexcounters map[string]int
}func (s *Stats) Snapshot() map[string]int {s.mu.Lock()defer s.mu.Unlock()result := make(map[string]int, len(s.counters))for k, v := range s.counters {result[k] = v}return result
}// snapshot 现在是一个拷贝
snapshot := stats.Snapshot()

6.使用 defer 释放资源

使用 defer 释放资源,诸如文件和锁。

// Bad
// 当有多个 return 分支时,很容易遗忘 unlock
p.Lock()
if p.count < 10 {p.Unlock()return p.count
}p.count++
newCount := p.count
p.Unlock()return newCount// Good
// 更可读且安全
p.Lock()
defer p.Unlock()if p.count < 10 {return p.count
}p.count++
return p.count

defer 的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做。使用 defer 提升可读性是值得的,因为使用它们的成本微不足道。尤其适用于那些不仅仅是简单内存访问的较大方法,在这些方法中其他计算的资源消耗远超过 defer。

7.channel size 要么是 1 或 0

channel 通常 size 应为 1 或是无缓冲的。默认情况下,channel 是无缓冲的,其 size 为零。任何其他尺寸都必须经过严格的审查,而不是拍脑袋给出一个大致的值。我们需要考虑如何确定大小,什么原因会导致 channel 在高负载下被填满并阻塞写入者,以及当这种情况发生时系统会发生哪些变化。

// Bad
// 应该足以满足任何情况!
c := make(chan int, 64)// Good
// size 为 1
c := make(chan int, 1)
// 或无缓冲 size 为 0
c := make(chan int)

8.枚举从 1 开始

Go 并没有关键字 enum 来定义枚举类型,引入枚举的标准方法是声明一个自定义类型和一个使用了自增器 iota 的 const 组来表示枚举值。

预先声明的标识符 iota 表示连续的非类型化整数常量,它的值是对应常量的下标(从零开始),它可用于构造一组相关常数:

type ByteSize float64const (_           = iota // ignore first value by assigning to blank identifierKB ByteSize = 1 << (10 * iota)MBGBTBPBEBZBYB
)

由于第一个值从 0 开始,如果需要使第一个值为有意义的值,我们应该从 1 开始。

// Bad
type Operation intconst (Add Operation = iotaSubtractMultiply
)
// Add=0, Subtract=1, Multiply=2// Good
type Operation intconst (Add Operation = iota + 1SubtractMultiply
)
// Add=1, Subtract=2, Multiply=3

当然,凡事无绝对。如果第一个枚举值为零是有意义的,如当零值是理想的默认行为时。

type LogOutput intconst (LogToStdout LogOutput = iotaLogToFileLogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2

9.使用 time 处理时间

时间处理很复杂。关于时间的错误假设通常包括以下几点:

(1)一分钟有 60 秒
(2)一小时有 60 分钟
(3)一天有 24 小时
(4)一周有七天
(5)一年 365 天

还有更多,具体可参考 Falsehoods programmers believe about time。

例如,在一个时间点上加上 24 小时并不总是产生一个新的日历日。

为什么常识性的认知是错误的呢?因为地球自转的不均匀性和长期变慢性,会存在时间修正的情况,比如闰秒,闰年等。

因此,在处理时间时始终使用 “time” 包,因为它有助于以更安全、更准确的方式处理这些不正确的假设。

  • 使用 time.Time 表达瞬时时间

在处理时间的瞬间时使用 time.Time,在比较、添加或减去时间时使用 time.Time 中的方法。

// Bad
func isActive(now, start, stop int) bool {return start <= now && now < stop
}// God
func isActive(now, start, stop time.Time) bool {return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}
  • 使用 time.Duration 表达时间段
// Bad
func poll(delay int) {for {// ...time.Sleep(time.Duration(delay) * time.Millisecond)}
}
poll(10) // 是几秒钟还是几毫秒?// Good
func poll(delay time.Duration) {for {// ...time.Sleep(delay)}
}
poll(10*time.Second) // 代码即注释

回到第一个例子,在一个时间时刻加上 24 小时,我们用于添加时间的方法取决于意图。如果我们想要下一个日历日(当前天的下一天)的同一个时间点,我们应该使用 Time.AddDate。但是,如果我们想保证某一时刻比前一时刻晚 24 小时,我们应该使用Time.Add

newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)
  • 对外部系统使用 time.Time 和 time.Duration

尽可能在与外部系统交互中使用 time.Time 和 time.Duration 。

(1)Command-line 标志:flag 通过 time.ParseDuration 支持 time.Duration ;

(2)JSON:encoding/json 通过其 Time.UnmarshalJSON 方法支持将 time.Time 编码为 RFC 3339 字符串;

(3)SQL:database/sql 支持将 DATETIMETIMESTAMP 列转换为 time.Time,如果底层驱动程序支持则返回;

(4)YAML:gopkg.in/yaml.v2 支持将 time.Time 作为 RFC 3339 字符串,并通过 time.ParseDuration 支持 time.Duration 。

当不能在这些交互中使用 time.Duration 时,请使用 intfloat64,并在字段名称中包含单位。

例如,由于 encoding/json 不支持 time.Duration,因此该单位包含在字段的名称中。

// Bad
// {"interval": 2}
type Config struct {Interval int `json:"interval"`
}// Good
// {"intervalMillis": 2000}
type Config struct {IntervalMillis int `json:"intervalMillis"`
}

当在这些交互中不能使用 time.Time 时,除非达成一致,否则使用 string 和 RFC 3339 中定义的格式时间戳。默认情况下,Time.UnmarshalText 使用此格式,并可通过 time.RFC3339 在 Time.Format 和 time.Parse 中使用。

尽管这在实践中并不成问题,但请记住,time 包不支持解析闰秒时间戳(8728),也不在计算中考虑闰秒(15190)。如果您比较两个时刻,则差异将不包括这两个时刻之间可能发生的闰秒。

10.关于 error 的处理

10.1 错误类型

声明错误可选的方式很少,在选择合适的错误申明方式之前,应考虑以下几点。

(1)调用者是否需要匹配错误以便他们可以处理它? 如果是,我们必须通过声明顶级错误变量或自定义类型来支持 errors.Is 或 errors.As 函数。

(2)错误消息是否为静态字符串,还是需要上下文信息的动态字符串? 如果是静态字符串,我们可以使用 errors.New,但对于后者,我们必须使用 fmt.Errorf 或自定义错误类型。

(3)我们是否正在传递由下游函数返回的新错误? 如果是这样,请参阅错误包装部分。

错误匹配 错误消息 指导
No static errors.New
No dynamic fmt.Errorf
Yes static top-level var with errors.New
Yes dynamic custom error type

例如,使用 errors.New 表示带有静态字符串的错误。 如果调用者需要匹配并处理此错误,则将此错误导出为变量以支持将其与 errors.Is 匹配。

// Bad// package foofunc Open() error {return errors.New("could not open")
}// package barif err := foo.Open(); err != nil {// Can't handle the error.panic("unknown error")
}// Good
// package foovar ErrCouldNotOpen = errors.New("could not open")func Open() error {return ErrCouldNotOpen
}// package barif err := foo.Open(); err != nil {if errors.Is(err, foo.ErrCouldNotOpen) {// handle the error} else {panic("unknown error")}
}

对于动态字符串的错误, 如果调用者不需要匹配它,则使用 fmt.Errorf, 如果调用者确实需要匹配它,则自定义 error。

无错误匹配。

// package foofunc Open(file string) error {return fmt.Errorf("file %q not found", file)
}// package barif err := foo.Open("testfile.txt"); err != nil {// Can't handle the error.panic("unknown error")
}

错误匹配。

// package footype NotFoundError struct {File string
}func (e *NotFoundError) Error() string {return fmt.Sprintf("file %q not found", e.File)
}func Open(file string) error {return &NotFoundError{File: file}
}// package barif err := foo.Open("testfile.txt"); err != nil {var notFound *NotFoundErrorif errors.As(err, &notFound) {// handle the error} else {panic("unknown error")}
}

请注意,如果您从包中导出错误变量或类型, 它们将成为包的公共 API 的一部分。

10.2 错误包装

在调用失败时传播错误有三个主要方式:

(1)返回原始错误;
(2)使用 fmt.Errorf 和 %w 添加上下文生成新错误
(3)使用 fmt.Errorf 和 %v 添加上下文生成新错误

如果没有要添加的其他上下文,则按原样返回原始错误。 这将保留原始错误类型和消息。 这非常适合底层错误消息有足够的信息来追踪它来自哪里的错误。

否则,尽可能在错误消息中添加上下文 这样就不会出现诸如“连接被拒绝”之类的模糊错误, 您会收到更多有用的错误,例如“呼叫服务 foo:连接被拒绝”。

使用 fmt.Errorf 为你的错误添加上下文, 根据调用者是否应该能够匹配和提取根本原因,在 %w 或 %v 格式之间进行选择。

(1)如果调用者应该可以访问底层错误,请使用 %w。 对于大多数包装错误,这是一个很好的默认值,但请注意,调用者可能会开始依赖此行为。因此,对于包装错误是已知变量或类型的情况,请将其作为函数契约的一部分进行记录和测试。
(2)使用 %v 来混淆底层错误,调用者将无法匹配它,但如果需要,您可以在将来切换到 %w。

在为返回的错误添加上下文时,通过避免使用"failed to"之类的短语来保持上下文简洁,当错误通过堆栈向上传递时,它会一层一层被堆积起来:

// Bad
s, err := store.New()
if err != nil {return fmt.Errorf("failed to create new store: %w", err)
}// Good
s, err := store.New()
if err != nil {return fmt.Errorf("new store: %w", err)
}

然而,一旦错误被发送到另一个系统,应该清楚消息是一个错误(例如 err 标签或日志中的 “Failed” 前缀)。

另外请参考:Don’t just check errors, handle them gracefully。

11.使用 go.uber.org/atomic 包

使用 sync/atomic 包的原子操作对基本类型 (int32, int64 等)进行操作,因为很容易忘记使用原子操作来读取或修改变量。

go.uber.org/atomic 通过隐藏基础类型为这些操作增加了类型安全性。此外,它包括一个方便的 atomic.Bool 类型。

// Bad
type foo struct {running int32  // atomic
}func (f* foo) start() {if atomic.SwapInt32(&f.running, 1) == 1 {// already running…return}// start the Foo
}func (f *foo) isRunning() bool {return f.running == 1  // race!
}// Good
type foo struct {running atomic.Bool
}func (f *foo) start() {if f.running.Swap(true) {// already running…return}// start the Foo
}func (f *foo) isRunning() bool {return f.running.Load()
}

12.避免可变全局变量

使用依赖注入方式避免改变全局变量,既适用于函数指针又适用于其他类型。这样做的好处是避免了全局变量被错误修改的可能。

// Bad// sign.govar _timeNow = time.Nowfunc sign(msg string) string {now := _timeNow()return signWithTime(msg, now)
}// sign_test.go
func TestSign(t *testing.T) {oldTimeNow := _timeNow_timeNow = func() time.Time {return someFixedTime}defer func() { _timeNow = oldTimeNow }()assert.Equal(t, want, sign(give))
}// Good// sign.gotype signer struct {now func() time.Time
}func newSigner() *signer {return &signer{now: time.Now,}
}func (s *signer) Sign(msg string) string {now := s.now()return signWithTime(msg, now)
}// sign_test.gofunc TestSigner(t *testing.T) {s := newSigner()s.now = func() time.Time {return someFixedTime}assert.Equal(t, want, s.Sign(give))
}

13.避免在公共结构中嵌入类型

嵌入类型会泄漏实现细节、禁止类型演化和产生模糊的接口文档。

假设您使用共享的 AbstractList 实现了多种列表类型,请避免在具体的列表实现中嵌入 AbstractList。相反,只需手动将方法写入具体的列表,委托给抽象列表的方法。

type AbstractList struct {}
// 添加将实体添加到列表中。
func (l *AbstractList) Add(e Entity) {// ...
}
// 移除从列表中移除实体。
func (l *AbstractList) Remove(e Entity) {// ...
}// Bad// ConcreteList 是一个实体列表。
type ConcreteList struct {*AbstractList
}// Good// ConcreteList 是一个实体列表。
type ConcreteList struct {list *AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {l.list.Remove(e)
}

泄漏实现细节指 AbstractList 的实现是 ConcreteList 的实现细节,被到处泄露了;

禁止类型演化指 ConcreteList 获得了同名嵌套类型字段 AbstractList,如果嵌入的类型是 public,那么字段是 public。为了保持向后兼容性,外部类型的每个未来版本都必须保留嵌入类型;

产生模糊的接口文档指 AbstractList 被导出的字段和方法全部成为了 ConcreteList 被导出的字段和方法,在 ConcreteList 又没有明确说明,会产生模糊的接口文档。

很少需要嵌入类型,虽然它可以帮助您避免编写冗长的委托方法。

即使嵌入兼容的抽象列表 interface,而不是结构体,这将为开发人员提供更大的灵活性来改变未来,但仍然泄露了具体列表使用抽象实现的细节。

// Bad// AbstractList 是各种实体列表的通用实现。
type AbstractList interface {Add(Entity)Remove(Entity)
}
// ConcreteList 是一个实体列表。
type ConcreteList struct {AbstractList
}// Good// ConcreteList 是一个实体列表。
type ConcreteList struct {list AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {l.list.Remove(e)
}

无论是使用嵌入结构还是嵌入接口,都会限制类型的演化。
(1)向嵌入接口添加方法是一个破坏性的改变;
(2)从嵌入结构体删除方法是一个破坏性的改变;
(3)删除嵌入类型是一个破坏性的改变;
(4)即使使用满足相同接口的类型替换嵌入类型,也是一个破坏性的改变。

尽管编写这些委托方法是乏味的,但是额外的工作隐藏了实现细节,留下了更多的更改机会,还消除了在未能描述出潜在接口的模糊文档。

14.避免使用内置标识符

Go语言 Language Specification 概述了几个内置的, 不应在 Go 项目中使用的预申明标识符。

Types:bool byte complex64 complex128 error float32 float64int int8 int16 int32 int64 rune stringuint uint8 uint16 uint32 uint64 uintptrConstants:true false iotaZero value:nilFunctions:append cap close complex copy delete imag lenmake new panic print println real recover

根据上下文的不同,将这些标识符作为名称重复使用, 将在当前作用域(或任何嵌套作用域)中隐藏原始标识符,混淆代码。 最好的情况下编译器会报错,最坏的情况下,这样的代码可能会引入潜在的、难以恢复的错误。

// Bad
var error string // 作用域隐藏内置 errorfunc handleErrorMessage(error string) {// 作用域隐藏内置 error
}type Foo struct {// 虽然这些使用内置标识符的自定义字段可以编译通过,但对 error 或 string 字符串的搜索存在二义性error  errorstring string
}func (f Foo) Error() error {// error 和 f.error 在视觉上是相似的return f.error
}func (f Foo) String() string {// string and f.string 在视觉上是相似的return f.string
}// Good
var errorMessage stringfunc handleErrorMessage(msg string) {}type Foo struct {// error 和 string 现在是明确的err errorstr string
}func (f Foo) Error() error {return f.err
}func (f Foo) String() string {return f.str
}

注意,编译器在使用预申明标识符时不会报错, 但是诸如 go vet 之类的代码检测工具会正确地指出这些和其他情况下的隐式问题。

15.主函数退出方式

Go 程序使用os.Exit或者log.Fatal*立即退出,使用 panic 不是退出程序的好方法。

15.1 在 main() 中退出程序

仅在 main() 函数中调用os.Exitlog.Fatal,所有其他函数应将错误返回到主调。

// Bad
func main() {body := readFile(path)fmt.Println(body)
}func readFile(path string) string {f, err := os.Open(path)if err != nil {log.Fatal(err)}b, err := ioutil.ReadAll(f)if err != nil {log.Fatal(err)}return string(b)
}// Good
func main() {body, err := readFile(path)if err != nil {log.Fatal(err)}fmt.Println(body)
}
func readFile(path string) (string, error) {f, err := os.Open(path)if err != nil {return "", err}b, err := ioutil.ReadAll(f)if err != nil {return "", err}return string(b), nil
}

当程序的多个函数具有退出能力时会存在一些问题:
(1)不明显的控制流:任何函数都可以退出程序,因此很难对控制流进行推理;
(2)难以测试:退出程序的函数也将退出调用它的测试,这使得函数很难测试,并跳过了尚未被运行的其他代码;
(3)跳过清理:当函数退出程序时,会跳过已经进入 defer 队列里的函数调用,这增加了跳过重要清理任务的风险。

15.2 退出一次

如果可能的话,你的 main() 函数中最多一次 调用os.Exit或者log.Fatal。如果有多个错误场景停止程序,请将该逻辑放在单独的函数下并从中返回错误。这会缩短 main() 函数,并将所有关键业务逻辑放入一个单独的、可测试的函数中。

// Bad
package mainfunc main() {args := os.Args[1:]if len(args) != 1 {log.Fatal("missing file")}name := args[0]f, err := os.Open(name)if err != nil {log.Fatal(err)}defer f.Close()// 如果我们在这行之后调用 log.Fatal,f.Close 将不会被执行b, err := ioutil.ReadAll(f)if err != nil {log.Fatal(err)}// ...
}// Good
package mainfunc main() {if err := run(); err != nil {log.Fatal(err)}
}func run() error {args := os.Args[1:]if len(args) != 1 {return errors.New("missing file")}name := args[0]f, err := os.Open(name)if err != nil {return err}defer f.Close()b, err := ioutil.ReadAll(f)if err != nil {return err}// ...
}

16.不要在 for 循环中使用 defer

尽可能地不要在 for 循环中使用 defer,因为这可能会导致资源泄漏(Possible resource leak, ‘defer’ is called in the ‘for’ loop)。

defer 不是基于代码块的,而是基于函数的。你在循环中分配资源,那么不应该简单地使用 defer,因为释放资源不会尽可能早地发生(在每次迭代结束时),只有在 for 语句之后(所有迭代之后),即所在函数结束时,defer 函数才会被执行。这带来的后果就是,如果迭代次数过多,那么可能导致资源长时间得不到释放,造成泄漏。

// Bad
for rows.Next() {fields, err := db.Query(.....)if err != nil {// ...}defer fields.Close()// do something with `fields`}

如果有一个类似上面分配资源的代码段,我们应该将其包裹在一个函数中(匿名函数或有名函数)。在该函数中,使用 defer,资源将在不需要时被立即释放。

// 1.将 defer 放在匿名函数中
for rows.Next() {func() {fields, err := db.Query(...)if err != nil {// Handle error and returnreturn}defer fields.Close()// do something with `fields`}()
}// 2.将 defer 放在有名函数中然后调用之
func foo(r *db.Row) error {fields, err := db.Query(...)if err != nil {return fmt.Errorf("db.Query error: %w", err)}defer fields.Close()// do something with `fields`return nil
}// 调用有名函数
for rows.Next() {if err := foo(rs); err != nil {// Handle error and returnreturn}
}

参考文献

github.com/uber-go/guide
stackoverflow.Take address of value inside an interface
stackoverflow.Does assigning value to interface copy anything?
Frequently Asked Questions (FAQ).When are function parameters passed by value?
stackoverflow.defer in the loop - what will be better?

Go 编码建议——功能篇相关推荐

  1. Go 编码建议——性能篇

    文章目录 常用数据结构 1.反射虽好,切莫贪杯 1.1 优先使用 strconv 而不是 fmt 1.2 少量的重复不比反射差 1.3 慎用 binary.Read 和 binary.Write 2. ...

  2. Go 编码建议——风格篇

    文章目录 1.格式化 主体风格 占位符 2.代码行 行长度 换行方式 不必要的空行 括号和空格 行数 3.字符串 4.依赖管理 依赖规范 import 规范 5.初始化 初始化 struct 初始化 ...

  3. 【Windows系统优化篇】谨慎开启“来自微软输入法的启用建议“功能

    [Windows系统优化篇]谨慎开启"来自微软输入法的启用建议"功能 出于个人隐私数据的保护,不建议开启这玩意,容易造成个人隐私数据泄露.-[蘇小沐] 1.实验环境 系统 版本 W ...

  4. 数据分析利器之Excel功能篇

    数据说·实操季 先相信自己,然后别人才会相信你.   --罗曼·罗兰 导读:今天我们要介绍的关于Excel功能的系列内容,在数据分析行业里面的地位是举足轻重的.从使用范围来看,微软办公软件Office ...

  5. 嵌入式软件工程师养成记-基本功能篇之c语言编程规范

    基本功能篇之c语言编程规范 为什么还在用c语言开发 首先将编程语言按照开发效率粗略的分为三个等级,低中高,对应的语言有汇编(低).c/c++(中).python(高).越低级的语言.开发效率越低.但是 ...

  6. 苹果推出App Store搜索建议功能

    4月30日消息,据国外媒体报道,苹果正式在App Store上推出了搜索建议功能,将使搜索应用变得更加容易. 据悉,现在在输入搜索关键词之后,App Store将尝试预测你在寻找什么,并提供建议词,当 ...

  7. 谷歌Chrome浏览器开发者工具教程—基础功能篇

    Chrome(F12开发者工具)是非常实用的开发辅助工具,对于前端开发者简直就是神器,但苦于开发者工具是英文界面,且没有中文,这让很多朋友都不知道怎么用.下载吧小编为大家带来Chrome开发者工具基础 ...

  8. 关于考教师资格证的一些建议——笔试篇

    关于考教师资格证的一些建议--笔试篇 一.报名及前期准备 二.中期准备 三.考试技巧 (这篇文章是2019年9月1日写的,时间可能对不上,但是懒得改了,后续已经通过了普通话和面试,有时间会更新哒~) ...

  9. 笔杆网试用---功能篇(一)

    引言 今天开始更新笔杆网试用体验的功能篇,鉴于笔杆网的大大小小的功能数目繁多,我会从论文写作的流程来一一介绍它的功能作用,功能具体操作,功能操作的不便之处,以及相比之下其他同类产品的功能对比结论. 笔 ...

最新文章

  1. Android版‘音乐一点通’音乐播放器详情
  2. dfs时间复杂度_一文吃透时间复杂度和空间复杂度
  3. java面试题42从以下哪一个选项中可以获得Servlet的初始化参数?
  4. 星载计算机西北工业大学,星载计算机SRAM加固可靠性的研究与设计
  5. 《英雄联盟:双城之战》全球首映 沉浸式观影打造追剧新潮流
  6. 平台层-适配层-核心层|拆分环信ONE SDK架构
  7. android状态栏背景色和图标颜色更改总结
  8. Linux 命令(15)—— umask 命令(builtin)
  9. 实现JNI的另一种方法:使用RegisterNatives方法传递和使用Java自定义类 (转)
  10. mysql 执行计划 改变_数据量增加导致mysql执行计划改变解决_MySQL
  11. Codeforces Round #533 (Div. 2) 部分题解A~D
  12. WPS快捷键之 EXCEL高级
  13. 华为手机最大屏是几英寸的_华为有史以来最大屏幕的手机,屏幕尺寸高达7.12寸,性价比很好!...
  14. SeedLab1: Sniffing Spoofing Lab
  15. 快速美化封面用word就可以
  16. ERROR 1010 (HY000): Error dropping database (can‘t rmdir ‘.\qpweb‘, errno: 41) 删库失败问题的解决
  17. win10虚拟机搭建 Hadoop集群
  18. 分布式tensorflow
  19. eclipse配置python django环境_windows下python+Django+eclipse开发环境的配置
  20. SQL之to_date()

热门文章

  1. C语言如何动态分配空间:malloc
  2. HTML中元素的position属性详解
  3. Effective_STL 学习笔记(十七) 使用 “交换技巧” 来修整过剩的容量
  4. StringUtils 中 isEmpty 和 isBlank 的区别
  5. Ubuntu 安装 CLI 并运行 ASP.NET Core 1.0
  6. mysql 简单游标
  7. 网站导航(URL 映射和路由)
  8. 应用虚拟化之规划篇二 项目流程规划
  9. Mybatis简单入门及配置文件标签详情
  10. [Java] 蓝桥杯PREV-8 历届试题 买不到的数目