二. 指导原则
指向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”)
}
}