初学Golang易犯的错误
Golang是Google推出的面向软件工程的,拥有GC的,高并发的语言。
初学Go的时候很可能犯一些错误,犯错误并不怕,可怕的是一直犯同一个错误。 只要犯的错误足够多,那么就会成为大师了。
不使用Go工具
初学Go的时候,不知道Go有那么多好工具,并且可以和编辑器很好的结合,一概都不使用。 把Go当作了脚本语言,直接开一个文本编辑器来处理,写作效率很低下。
其实Go有许多的工具,都可以和编辑器结合起来使用,使其成为强大的IDE,大大提高写Go的效率。
常见的工具有:
- 代码补全工具 gocode
- 代码格式化工具 gofmt
- 代码跳转,查找引用等 guru
- 代码自动补import等 goimports
- 静态检查 golint
- 本地查看文档 godoc
- etc.
其实只要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数组,传给了下游方法save
,save
拿到这个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.