Go语言7种常见的错误用法

这篇文章的内容来自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没必要总是线程安全的
  • 消费端可以在需要的时候实现线程安全
  • 消费端可以使用他们想要的方法实现线程安全

最严重的错误

最后再重申一下,最严重的错误就是不犯错误。犯错是一个学习和探索的过程。如果你没有经历错误,你可能在制造一个更深远更大的错误。
在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

番茄大圣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值