这个坑好像坑了很久,终于填好了……
高级特性
接口与多态
这里有一篇不错的文章How to use interfaces in Go
go的接口有点Trait
的意思,并且不需要显式实现,这点不是很爽,虽然结果上没啥区别,但是总觉得看到定义时知道它实现了哪些接口比较方便。实际上interface除了是一组方法外,还是一种类型。
作为接口
一个简单的接口的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| package main import "fmt" type Animal interface { Speak() string } type Dog struct { } func (d Dog) Speak() string { return "Woof!" } type Cat struct { } func (c Cat) Speak() string { return "Meow!" } type Llama struct { } func (l Llama) Speak() string { return "?????" } type JavaProgrammer struct { } func (j JavaProgrammer) Speak() string { return "Design patterns!" } func main() { animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} for _, animal := range animals { fmt.Println(animal.Speak()) } }
|
作为类型
interface{}
作为类型时可以看做是一个实现了0个方法的匿名接口,任何类型都至少实现了0个方法,因此任何类型都可以实现了interface{}
接口,因为Go实现一个接口不需要显式实现,只要满足了接口的定义,就等于实现了这个接口,这里和c中的void *
有点像。前文提到的那篇文章中有一个有趣的错例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package main import ( "fmt" ) func PrintAll(vals []interface{}) { for _, val := range vals { fmt.Println(val) } } func main() { names := []string{"stanley", "david", "oscar"} PrintAll(names) }
|
这里并不能将[]T
转化成[]interface{}
,除非显式的进行转换。这里我的理解是[]T
本身是一个类型,而[]interface{}
则是“一个组类型”的类型,因此二者显然不是同一个类型,不能进行传递。
接口与指针
Go在定义接口时是没有指定到底是值还是指针作为receiver的,这里怎么定义都行,但是使用时需要注意,如果是指针作为receiver,当使用值作为该接口的实例调用会出现编译错,而相反的,使用值作为receiver,使用指针作为接口实例调用没有问题。这里主要是因为一个指针同一时间只能指向同一固定的对象实例,而一个对象实例同一时间可能有很多指针存在。
接口检查
因为Go不要求显式实现接口,因此有些时候需要对对象是否实现接口进行判断。当需要做runtime接口时,一般的:
1 2 3
| if _, ok := val.(json.Marshaler); ok { }
|
但运行时接口检查比较麻烦,假如我们知道某类型需要实现某接口时,可以定义:
1
| var _ json.Marshaler = (*RawMessage)(nil)
|
这样,可以在编译时确保*RawMessage
实现了json.Marshaler
接口。
Go没啥继承的概念,所以发明了一个“嵌入”的概念,例如:
1 2 3 4 5 6 7 8 9 10 11 12
| type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type ReadWriter interface { Reader Writer }
|
实现ReadWriter
就等于实现了Reader
和Writer
。这里需要主要接口里只能embedding接口。类似的,struct也可以embedding:
1 2 3 4 5 6
| type ReadWriter struct { *Reader *Writer }
|
注意这里不用写成员名,不写它是一个嵌入,写了就是一个成员了,嵌入用在struct里有点多重继承的味道。在struct的方法中可以直接以struct名调用这个“成员”:
1 2 3
| func (job *Job) Logf(format string, args ...interface{}) { job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...)) }
|
defer
defer
就是在当前区块生命周期结束前执行某函数,这点在关闭资源方面比较实用。defer相当于一个栈,每次调用会把方法推到栈里,执行defer时也是按出栈顺序(LIFO)。defer比较方便的是,可以在打开的时候写好defer,后面退出时就不需要再写了。c/c++里要实现类似的只写一处回收资源代码时,还得用上goto
。一个defer的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| func Contents(filename string) (string, error) { f, err := os.Open(filename) if err != nil { return "", err } defer f.Close() var result []byte buf := make([]byte, 100) for { n, err := f.Read(buf[0:]) result = append(result, buf[0:n]...) if err != nil { if err == io.EOF { break } return "", err } } return string(result), nil }
|
goroutine
Go的并发模型是协程的,因此goroutine相当于轻量级线程,用法上和线程大同小异,只是协程间切换成本更低。Go中创建一个goroutine也非常简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package main import ( "fmt" "time" ) func main() { go func (s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } }("world") say("hello") }
|
如果需要共享变量,还是需要同步的,可以使用sync
包。
Channel
基本用法
channel是go中goroutine之间用来共享值的一个FIFO队列,默认是阻塞的。存取数据均使用符号<-
,这里存变量时为毛不用->
,真是太不形象了。
1 2 3 4 5 6
| ch := make(chan int) ch <- v v := <-ch
|
如果创建channel时指定了size,那么将创建一个buffered channel,buffered channel只有在channel满的时候存数据才会阻塞,而在空的时候读数据会阻塞。(好像废话一样)
1 2
| ch := make(chan int, 20)
|
range&close
channel的生产者可以显式close
,这时消费者可以接收到这一状态,一般来说消费者不主动关闭channel:
一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i < n; i++ { c <- x x, y = y, x+y } close(c) } func main() { c := make(chan int, 10) go fibonacci(cap(c), c) for i := range c { fmt.Println(i) } }
|
select
select和linux的select类似,相当于同时监听多个channel,一直阻塞直到某个case分句可以执行,当多个case分句可以执行时,随机选择一个,一个例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } } func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) }
|
select可以加上default分句,以实现非阻塞,当没有case分句满足时执行default分句。
Timer
很多异步框架里Timer是个很重要的组件,Go里也提供了Timer,它是一个特殊的channel,有两种方式设置Timer/Ticker, 他们在使用上有一些差别:
1 2 3 4 5 6 7 8 9 10 11
| timer1 = time.After(time.Second) <-timer1 timer2 = time.NewTimer(time.Second) <-timer2.C ticker1 = time.Tick(time.Second) <-ticker1 ticker2 = time.NewTicker(time.Second) <-ticker2.C
|
异常处理
自己定义一个异常比较简单,就是实现接口:
1 2 3
| type error interface { Error() string }
|
这里上一个A Tour of Go的练习的例子说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package main import ( "fmt" ) type ErrNegativeSqrt float64 func (e ErrNegativeSqrt) Error() string { return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e)) } func Sqrt(x float64) (float64, error) { if (x < 0) { return 0, ErrNegativeSqrt(x) } return 0, nil } func main() { fmt.Println(Sqrt(2)) fmt.Println(Sqrt(-2)) }
|
注意,实现Error
方法时float64(e)
是必须的,否则就会导致无限循环,因为Sprintf会调用error接口的Error方法,而这里Error方法又调用Sprintf。
Panic
panic和linux kernel的panic意思差不多,就是出现fatal级错误,程序要挂了。和异常不一样,panic挂的非常彻底,所以一般的库中尽量少用panic,panic一般用在初始化阶段:
1 2 3 4 5 6 7
| var user = os.Getenv("USER") func init() { if user == "" { panic("no value for $USER") } }
|
Recover
recover一般用在defer函数中,当发生panic时可以捕获错误。一般的模块设计中可以用recover把panic和调用者隔离,一个recover的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| type Error string func (e Error) Error() string { return string(e) } func (regexp *Regexp) error(err string) { panic(Error(err)) } func Compile(str string) (regexp *Regexp, err error) { regexp = new(Regexp) defer func() { if e := recover(); e != nil { regexp = nil err = e.(Error) } }() return regexp.doParse(str), nil }
|
Reference
- Go 语言简介(上)— 语法
- Go 语言简介(下)— 特性
- A Tour of Go(FuckGFW)
- Go by Example
- Package Documentation(FuckGFW)
- Effective Go(FuckGFW)
- 学习Go语言
- The Go Programming Language Specification(FuckGFW)