5

初学Golang易犯的错误

 2 years ago
source link: https://ray-g.github.io/golang/common-mistakes-for-newbies/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Golang是Google推出的面向软件工程的,拥有GC的,高并发的语言。

初学Go的时候很可能犯一些错误,犯错误并不怕,可怕的是一直犯同一个错误。 只要犯的错误足够多,那么就会成为大师了。

Master Yoda

不使用Go工具

初学Go的时候,不知道Go有那么多好工具,并且可以和编辑器很好的结合,一概都不使用。 把Go当作了脚本语言,直接开一个文本编辑器来处理,写作效率很低下。

其实Go有许多的工具,都可以和编辑器结合起来使用,使其成为强大的IDE,大大提高写Go的效率。

常见的工具有:

  • 代码补全工具 gocode
  • 代码格式化工具 gofmt
  • 代码跳转,查找引用等 guru
  • 代码自动补import等 goimports
  • 静态检查 golint
  • 本地查看文档 godoc

其实只要VSCode安装Go插件后,就可以一键下载并使用这些工具了。

没接受Interfaces

Go的Interface与其他语言(比如Java,C#)的Interface是有差别的。 Go的Interface是非侵入式的,Go的Interface只是一个合约。

从其他语言,尤其是OO过来的程序员,常常带着原先语言的口音来写Go。 经常定义一个Struct然后定义各种Method,但是从来不定义一个Interface。 又或者会有把Interface当作虚基类来定义的。

Go的Interface是Go中非常强大的一个功能,它给Go带来了很好的扩展性,并且很灵活。 Go的Interface是由方法定义的,那么Interface应当仅仅是一组行为,且同名的行为应当一致。

比如如下代码:

func (c *Content) saveAs(path string) {
    b := new(bytes.Buffer)
    b.Write(c.Content)
    c.save(b.Bytes(), path)
}

func (c *Content) save(by []byte, path string) {
    writeToDisk(path, bytes.NewReader(by))
}

这段代码本身没有什么问题,但是不够高效,且不宜扩展。我们在saveAs中申请了一个Buffer, 然后又将这个Buffer转换成了byte数组,传给了下游方法savesave拿到这个byte数组后, 又创建了一个Reader来最终执行写文件操作。

其实,Buffer是有Reader方法的,而且这个方法刚好与io.Reader Interface所一致, 因此,将代码改为如下写法,更高效,也更加Gopher。

func (c *Content) saveAs(path string) {
    b := new(bytes.Buffer)
    b.Write(c.Content)
    c.save(b, path)
}

func (c *Content) save(r io.Reader, path string) {
    writeToDisk(path, r)
}

Interface在Go中对各个struct进行了隔离,在实现的时候,大家可以完全遵循依赖倒置的原则,只需要针对接口编程。

定义一个过于宽泛的Interface

Go中的Interface以及struct其实都是可以很容易的组合在一起的,没有必要定义一个特别宽泛的Interface, 而是应该定义尽可能小的Interface,在需要的地方对其进行组合。

比如 sort.Sort所接收的参数就是一组最小的行为集合,有获取长度,比较以及交换,缺一不可。

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

但是,假设我们需要创建一个文件系统,那么需要定义一个File的接口,我们会怎么定义呢?

type File interface {
    Open()
    Close()
    Read(...)
    ReadAt(...)
    Write(...)
    WriteAt(...)
    Seek(...)
}

假如我们这样定义,语法上当然没问题了,但是在使用的时候,当在一个方法的参数传递这个File接口,如下 那么问题就来了,要使用这个方法的话,每个使用者都必须实现那么一组方法,不管那些方法是否需要。听起来就不那么好。

func (fs *FileSystem) ReadFile(f File) {
    ...
}

那么,什么是相对好一些的做法呢?我们定义或复用接口,并在方法上用且仅用必须的那组接口。

type File interface {
    Open()
    Close()
    io.Read(...)
    io.ReadAt(...)
    io.Write(...)
    io.WriteAt(...)
    io.Seek(...)
}

func (fs *FileSystem) ReadFile(r io.Reader) {
    ...
}

到处请求空Interface

Go的任意的struct都可以是空Interface,于是为了方便,那么到处传递这种空Interface,直到用的时候再转换回之前的类型使用

type Value interface{}

func (n *Node) Insert(v Value) {
    original := v.(OriginalType)
}

这样有问题吗?大部分的时候没问题,但是这个接口其实是能传递进来任意东西的,那么当传入的不是一个OriginalType的时候, 在方法内部做类型转换的时候就会发生panic

然而,如果使用正确的Interface,则可以在编译期就发现这样的问题。

方法实现不一致

其实不仅仅是在写Go,在写任意代码的时候,同样名字的函数应该有相同的参数以及相同的行为。 在Go中,其实应该更加严格的遵守这条规范,因为Go语言本身不支持重载,多态等。 Go是面向软件工程的,设计初衷之一就是想减少软件编码引入的错误, 如果函数名相同,而参数或者行为不一致,那么又会导致通过编码引入错误。

func (l *Logger) Info(format string, interface...) { ... }
func (c *ConsoleLogger) Info(format string, log string, print bool) { ... }

试看这样的代码,单独看都没有问题,但是大家都是logger,同名的Info却有着不一样的参数以及行为。 那么如果有一天,需要用Logger来替换ConsoleLogger的时候,编译运行都不会崩溃, 但是,真正运行起来的时候,则可能有意想不到或者完全未知的错误出现。

不使用 io.Reader 以及 io.Writer

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

这两个都是Interface,于是同不喜欢使用Interface一样,很多刚开始写Go的人对这两个Interface不闻不问。 经常在各个地方传递着字符串,[]byte等等。

这两个方法在Go中其实是很常见的,除了习以为常的读写文件以外,读写内存,读写管道,读写Buffer等等的struct, 也都实现了Reader以及Writer方法,且都与io.Reader以及io.Writer一致。

使用这两个接口,可以使你所写的Go程序更加的灵活且易于扩展。

方法 VS 函数

OO界的同学眼里只有Method方法,而PO界的同学眼里只有Function函数。 但是Go里两个都有。

那么来自不同国度的同学到了Go之后,就对何时使用Method何时使用Function有些混淆了。

Function只是一个函数,且是时不变的,也就是说,在给定输入的时候,输出也是一定的,function不应当依赖于系统的状态。 Method是依赖于一个对象的,Method会使用该对象内部的一些状态,是这个对象的一些行为。

使用 值 VS 指针

Go语言是有指针的,来自与C/C++的同学再熟悉不过了,而来自于Java的同学则会经常把指针当作引用。

在定义type的方法的时候,接收器可以指定为值也可以指定为指针,但是什么时候该用哪种接收器呢? 在给函数传递参数的时候,也可以定义参数为指针或者值。而且,不管如何定义,在使用这些方法或函数的时候,书写的代码并无差别。

或许会有以下声音:

  • 只使用指针,因为指针的性能更好
  • 只使用,我们需要对象的拷贝

在Go语言中,使用指针或者值,通常情况下,不应当是以性能作为参考,而应当是以共享对象作为参考。 当这个对象需要被共享访问的时候,或者需要改变其内部状态的时候,那么就用指针,否则就用值。

把Error当作String处理

Error在Go语言中,仅仅是一个Interface,且这个interface中仅有一个返回字符串的方法。

type error interface {
    Error() string
}

于是,很多同学在使用error的时候,会不自觉的把error当作string来处理。比如:

func foo() error {
    return errors.New("Some Error")
}

func main() {
    err := foo()
    if err != nil && err.Error() == "Some Error" {
        ...
    }
}

这并不是一个很好的写法,error的描述很有可能会改变,应该把error先定义好,然后判断对象会更好。

var SomeError = errors.New("Some Error")

func foo() error {
    return SomeError
}

func main() {
    err := foo()
    if err != nil && err.Error() == SomeError {
        ...
    }
}

另外,要对Error进行客制化,也不需要每次通过fmt.Errorf来搞定。

我们完全可以对Error进行扩展

type Error struct {
    ErrCode int
    Message string
}

// Error returns a human readable representation of the error.
func (e Error) Error() string {
    return fmt.Sprintf("%d : $s", e.ErrCode, e.Message)
}

这样,不仅仅判断的时候可以通过ErrCode非常容易的比较,而且还能得到一个很有好的error字符串, 且这个Error一样可以通过Go原生的error接口返回。

并发安全问题

Go语言是高并发的,我们在定义一个新的数据结构的时候需要考虑并发情况。

然而真实情况是这样吗?

并发的安全,是有代价的,这个代价就是性能。其实,作为底层数据结构,考虑并发其实是多余的, Go语言自己的map都没有考虑并发安全的问题,并发安全的问题一直是由使用者来保证的。

使用者可以通过使用mutex来对访问的对象加锁,抑或通过channel来传递。

其实在Go语言中,不提倡通过共享内存来交流,更提倡通过channel来交换数据。

Don’t communicate by sharing memory, share memory by communicating.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK