这篇文章的内容来自Docker的Steve Francia大神的分享,视频链接:https://www.youtube.com/watch?v=29LLRKIL_TI&t=357s
下面我尝试将自己的理解记录并分享,由于英文水平有限,如果文中有错误,望不吝指正。
最严重的错误
部分人最严重的错误是把错误当成恶魔,认为错误是不可饶恕的。事实却是当我们尝试使用一些新的事物时,出现错误是必然的。视频中提道,大师和初学者的区别就是大师比初学者尝试错误的次数要多得多:
Do you want to know the difference between a master and a beginner ?
The master has failed more times than the beginner has tried.
下面进入正文:
错误1:参数太具体(Not Accepting Interfaces)
作者拿hugo中的一段代码作例子:
func (page *Page) saveScourceAs(path string) {
b := new(bytes.Buffer)
b.Write(page.Source.Content)
page.saveSource(b.Bytes(),path)
}
func (page *Page) saveSource(by []byte, inpath string){
WriteToDisk(inpath,bytes.NewReader(by))
}
大家可以看出代码中的问题吗,传入saveSource方法的字节切片是从buffer中取出的,saveSource方法中又将其转换为一个reader。这些都是不必要的转换。导致这中问题的原因就是saveSource方法的参数定义得太具体了,如果尝试像下面这样写,将要读取的字节切片抽象为reader,会更加高效:
func (page *Page) saveScourceAs(path string) {
b := new(bytes.Buffer)
b.Write(page.Source.Content)
page.saveSource(b,path)
}
func (page *Page) saveSource(b io.Reader, inpath string){
WriteToDisk(inpath,b)
}
错误2:不使用io.Reader & io.Writer
先说说io.Reader和io.Writer的特点:
- 对于大部分输入输出操作来说,使用io.Reader和io.Writer可以使代码简单灵活
- 是可以提供大量功能的入口
- 方便扩展
大量的库都会经常使用io.Reader和io.Writer,io.Reader和io.Writer的定义如下:
type Reader inteface{
Read(p []byte) (n int,err error)
}
type Writer inteface{
Write(p []byte) (n int,err error)
}
当我们要设置输出的时候,使用io.Writer就很恰当了:
//code from cobra
func (c *Command) SetOutput(o io.Writer){
c.output = o
}
还有下面的错误示范(并非功能性错误,而是规范的问题):
//code from viper
func (v *Viper) ReadBufConfig(buf *bytes.Buffer) error{
...
}
更好的做法是将输入定义为一个reader:
func (v *Viper) ReadConfig(in io.Reader) error{
...
}
错误3:作为参数的接口太宽泛(Requiring Broad Interfaces)
先看看接口类型作为函数参数的准则:
- 函数应该只接收含有它需要的方法的接口(Functions should only accept interfaces that require the methods they need)
- 当参数都可以正常工作的时候,函数应该接收更细致(narrow)的接口而不是接收更宽泛(broad)的接口(Functions should not accept a broad interface when a narrow one would work)
- 宽泛的接口是从细致的接口组合而来的(Compose broad interfaces made from narrower ones)
作为参数的接口太宽泛的一个例子是:
func ReadIn(f File){
b := []byte{}
n,err:=f.Read(b)
}
其实ReadIn函数只是使用接口的read方法,所以File接口太过宽泛。下面的实现更为合理:
func ReadIn(r Reader){
b:=[]byte{}
n,err := r.Read(b)
}
错误4:方法和函数的使用(Methods Vs Functions)
这一节不太好理解,我自己没有理解得很清楚。下面的内容是我按照字面意思尽量去理解的笔记,写得并不好,有错误的话劳烦大家指正。
常见的错误是过多使用方法:
- 大部分有面向对象编程背景的人都过渡使用方法(A lot of people from OO backgrounds overuse methods)
- 下意识地将所有东西都定义成结构和方法(Natural draw to define everything via structs and methods)
为了避免这个错误,我们需要了解函数和方法的定义:
函数的定义:
- 有N1个输入和N2个输出(Operations performed on N1 inputs that results in N2 outputs)
- 同样的输入总是有同样的输出(The same inputs will always result in the same outputs)
- 函数不应该依赖各种状态(状态即结构体的变量,标识结构体的状态)(Functions should not depend on states)
方法的定义:
- 定义一种类型的行为(Defines the behavior of a type)
- 是一种在一个值上进行操作的函数(A function that operates against a value)
- 应该使用状态(Should use state)
- 逻辑上跟状态是相关联的(Logically connected)
函数和方法还有以下区别:
- 在定义上,方法的接收器类型是固定的(Methods, by definition, are bound to a specific type)
- 函数可以接收接口作为输入参数(Functions can accept interfaces as input)
错误5:指针和值的使用(Pointers Vs Values)
指针和值的使用可以参照以下准则:
- 传值还是传指针主要依据是“是否共享访问”,它们通常在性能上区别不大
- 如果你想要和函数或者方法共享一个值,那就传指针
- 如果你不想共享一个值,那就传值
是否使用指针接收器(receiver)?
- 当你想和某个方法分享值的时候,使用指针接收器
- 通常方法都用与管理状态值,所以指针接收器是很常用的
- 指针接收器方法是非并发安全的
是否使用值接收器?
- 当你想要复制值的时候,使用值接收器
- 如果接收器的类型是空结构体(没有状态),那就直接使用值传递
- 值接收器方法是并发安全的
下面是指针接收器的例子。因为我们要修改接收器的状态,我们希望接收器是共享的,所以使用指针接收器:
func (f *InMemoryFile) Close() error{
...
f.closed=true // modify state
...
}
下面是值接收器的例子。因为该接收器不需要被共享,所以使用值传递。另外也有个童鞋回答是因为"Time is ticking",我不知道该怎么理解这个原因。
func (t Time) IsZero() bool{
return t.sec==0 && t.nsec=0
}
错误6:把error当成字符串(Thinking Of Errors As Strings)
不少人取得一个error之后,为了判断是哪种error而对error的Error()方法返回的值进行字符串比对,这种做法并不太好,最好的做法是将error定义为公开的变量,判断error值是否相等,如:
var ErrNoName = errors.New("Zero length page name")
func Foo(name string)(error){
err := NewPage(name)
if err == ErrNoName{
newPage("default")
}
}
此外,我们还可以自定义error,好处有以下几点:
- 可以提供发生错误的上下文环境保证反馈的连贯性(避免使用复杂的格式化在error string中带上多个信息)
- 可以提供和错误值不同的类型(这句不太理解)
- 可以提供动态值(由错误状态决定值)
例如docker中的代码,Error中的Code和Detail是错误的上下文环境,且Detail是一个动态值,可以根据错误设定其值:
type Error struct{
Code ErrorCode
Message string
Detail interface{}
}
func (e Error) Error() string{
return fmt.Sprintf(...)
}
还有Go语言的OS包中定义的错误类型:
type PathError struct{
Op string
Path string
Err error
}
func (e *PathError) Error() string{
return ...
}
在遇上自定义error时,你可以这么处理:
func baa(f *file) error{
...
n,err := f.WriteAt(x,3)
if _,ok := err.(*PathError) {
...
} else {
...
}
}
或者这么处理:
if serr != nil{
if serr, ok := serr.(*PathError); ok && serr.Err == syscall.ENOTDIR{
return nil
}
return serr
}
错误7:保证"线程"安全或者不保证"线程"安全(To Be Safe Or Not To Be)
什么时候该考虑并发:
- 当你提供了一个库,别人会把它用在并发作业上时
- 当数据结构不是并发安全的时候
- 某些值不安全,你需要为这些值创造安全的行为的时候
如何保证线程安全?
- 使用Go语言的sync包,sync包里有Atomic/Mutext等工具
- 使用通道
为什么保留非线程安全的特性?
- 因为线程安全导致性能下降
- 在消费端保证线程安全
- 合适的API允许消费端在需要的时候添加线程安全保证(如map,map非线程安全)
- 消费端可以选择使用通道或者互斥实现
Go语言中的Map是非线程安全的,有以下几点原因:
- map没必要总是线程安全的
- 消费端可以在需要的时候实现线程安全
- 消费端可以使用他们想要的方法实现线程安全
最严重的错误
最后再重申一下,最严重的错误就是不犯错误。犯错是一个学习和探索的过程。如果你没有经历错误,你可能在制造一个更深远更大的错误。