Go Best Practices

二. 指导原则

指向interface的指针

您几乎不需要指向接口类型的指针。您应该将接口作为值进行传递,在这样的传递过程中,实质上传递的底层数据仍然可以是指针。

接口实质上在底层用两个字段表示:

  • 一个指向某些特定类型信息的指针。您可以将其视为“类型”。
  • 数据指针。如果存储的数据是指针,则直接存储。如果存储的数据是一个值,则存储指向该值的指针。

如果要接口方法修改底层数据,则必须用指向目标对象的指针赋值给接口类型变量(译注:感觉原指南中这里表达过于简略,不是很清晰,因此在翻译时增加了自己的一些诠释)。

接收器(receiver)与接口

使用值接收器的方法既可以通过值调用,也可以通过指针调用。

例如:

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].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”的精彩讲解。

译注:关于Go类型的method集合的问题,在我之前的文章《关于Go,你可能不注意的7件事》中有详尽说明。

零值Mutex是有效的

sync.Mutex和sync.RWMutex是有效的。因此你几乎不需要一个指向mutex的指针。

Bad:

mu := new(sync.Mutex)

mu.Lock()

vs.

Good:

var mu sync.Mutex

mu.Lock()

如果你使用结构体指针,mutex可以非指针形式作为结构体的组成字段,或者更好的方式是直接嵌入到结构体中。

如果是私有结构体类型或是要实现Mutex接口的类型,我们可以使用嵌入mutex的方法:

type smap struct {

sync.Mutex

data 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]

}

对于导出类型,请使用私有锁:

type SMap struct {

mu sync.Mutex

data map[string]string

}

func NewSMap() *SMap {

return &SMap{

data: make(map[string]string),

}

}

func (m *SMap) Get(k string) string {

m.mu.Lock()

defer m.mu.Unlock()

return m.data[k]

}

在边界处拷贝Slices和Maps

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

接收Slices和Maps

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

Bad

func (d *Driver) SetTrips(trips []Trip) {

d.trips = trips

}

trips := …

d1.SetTrips(trips)

// 你是要修改d1.trips吗?

trips[0] = …

vs.

Good

func (d *Driver) SetTrips(trips []Trip) {

d.trips = make([]Trip, len(trips))

copy(d.trips, trips)

}

trips := …

d1.SetTrips(trips)

// 这里我们修改trips[0],但不会影响到d1.trips

trips[0] = …

返回slices或maps

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

Bad

type Stats struct {

sync.Mutex

counters map[string]int

}

// Snapshot返回当前状态

func (s *Stats) Snapshot() map[string]int {

s.Lock()

defer s.Unlock()

return s.counters

}

// snapshot不再受到锁的保护

snapshot := stats.Snapshot()

vs.

Good

type Stats struct {

sync.Mutex

counters map[string]int

}

func (s *Stats) Snapshot() map[string]int {

s.Lock()

defer s.Unlock()

result := make(map[string]int, len(s.counters))

for k, v := range s.counters {

result[k] = v

}

return result

}

// snapshot现在是一个拷贝

snapshot := stats.Snapshot()

使用defer做清理

使用defer清理资源,诸如文件和锁。

Bad

p.Lock()

if p.count < 10 {

p.Unlock()

return p.count

}

p.count++

newCount := p.count

p.Unlock()

return newCount

// 当有多个return分支时,很容易遗忘unlock

vs.

Good

p.Lock()

defer p.Unlock()

if p.count < 10 {

return p.count

}

p.count++

return p.count

// 更可读

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

Channel的size要么是1,要么是无缓冲的

channel通常size应为1或是无缓冲的。默认情况下,channel是无缓冲的,其size为零。任何其他尺寸都必须经过严格的审查。考虑如何确定大小,是什么阻止了channel在负载下被填满并阻止写入,以及发生这种情况时发生了什么。

Bad

// 应该足以满足任何人

c := make(chan int, 64)

vs.

Good

// 大小:1

c := make(chan int, 1) // 或

// 无缓冲channel,大小为0

c := make(chan int)

枚举从1开始

在Go中引入枚举的标准方法是声明一个自定义类型和一个使用了iota的const组。由于变量的默认值为0,因此通常应以非零值开头枚举。

Bad

type Operation int

const (

Add Operation = iota

Subtract

Multiply

)

// Add=0, Subtract=1, Multiply=2

vs.

Good

type Operation int

const (

Add Operation = iota + 1

Subtract

Multiply

)

// Add=1, Subtract=2, Multiply=3

在某些情况下,使用零值是有意义的(枚举从零开始),例如,当零值是理想的默认行为时。

type LogOutput int

const (

LogToStdout LogOutput = iota

LogToFile

LogToRemote

)

// LogToStdout=0, LogToFile=1, LogToRemote=2

错误类型

Go中有多种声明错误(Error)的选项:

  • errors.New 对于简单静态字符串的错误
  • fmt.Errorf 用于格式化的错误字符串
  • 实现Error()方法的自定义类型
  • 使用 “pkg/errors”.Wrap的wrapped error

返回错误时,请考虑以下因素以确定最佳选择:

  • 这是一个不需要额外信息的简单错误吗?如果是这样,errors.New 就足够了。
  • 客户需要检测并处理此错误吗?如果是这样,则应使用自定义类型并实现该Error()方法。
  • 您是否正在传播下游函数返回的错误?如果是这样,请查看本文后面有关错误包装(Error Wrap)部分的内容
  • 否则,fmt.Errorf就可以。

如果客户端需要检测错误,并且您已使用创建了一个简单的错误errors.New,请使用一个错误变量(sentinel error )。

Bad

// package foo

func Open() error {

return errors.New(“could not open”)

}

// package bar

func use() {

if err := foo.Open(); err != nil {

if err.Error() == “could not open” {

// handle

} else {

panic(“unknown error”)

}

}

}

vs.

Good

// package foo

var ErrCouldNotOpen = errors.New(“could not open”)

func Open() error {

return ErrCouldNotOpen

}

// package bar

if err := foo.Open(); err != nil {

if err == foo.ErrCouldNotOpen {

// handle

} else {

panic(“unknown error”)

}

}

如果您有可能需要客户端检测的错误,并且想向其中添加更多信息(例如,它不是静态字符串),则应使用自定义类型。

Bad

func open(file string) error {

return fmt.Errorf(“file %q not found”, file)

}

func use() {

if err := open(); err != nil {

if strings.Contains(err.Error(), “not found”) {

// handle

} else {

panic(“unknown error”)

}

}

}

vs.

Good

type errNotFound struct {

file string

}

func (e errNotFound) Error() string {

return fmt.Sprintf(“file %q not found”, e.file)

}

func open(file string) error {

return errNotFound{file: file}

}

func use() {

if err := open(); err != nil {

if _, ok := err.(errNotFound); ok {

// handle

} else {

panic(“unknown error”)

}

}

}

直接导出自定义错误类型时要小心,因为它们已成为程序包公共API的一部分。最好公开匹配器功能以检查错误。

// package foo

type errNotFound struct {

file string

}

func (e errNotFound) Error() string {

return fmt.Sprintf(“file %q not found”, e.file)

}

func IsNotFoundError(err error) bool {

_, ok := err.(errNotFound)

return ok

}

func Open(file string) error {

return errNotFound{file: file}

}

// package bar

if err := foo.Open(“foo”); err != nil {

if foo.IsNotFoundError(err) {

// handle

} else {

panic(“unknown error”)

}

}