Golang学习笔记——高级特性


这个坑好像坑了很久,终于填好了……

高级特性

接口与多态

这里有一篇不错的文章How to use interfaces in Go
go的接口有点Trait的意思,并且不需要显式实现,这点不是很爽,虽然结果上没啥区别,但是总觉得看到定义时知道它实现了哪些接口比较方便。实际上interface除了是一组方法外,还是一种类型。

作为接口

一个简单的接口的例子:

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 *有点像。前文提到的那篇文章中有一个有趣的错例

//注意,这是一个错误的例子!!!
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接口时,一般的:

if _, ok := val.(json.Marshaler); ok {
	//do something
}

但运行时接口检查比较麻烦,假如我们知道某类型需要实现某接口时,可以定义:

var _ json.Marshaler = (*RawMessage)(nil)

这样,可以在编译时确保*RawMessage实现了json.Marshaler接口。

Embedding

Go没啥继承的概念,所以发明了一个“嵌入”的概念,例如:

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就等于实现了ReaderWriter。这里需要主要接口里只能embedding接口。类似的,struct也可以embedding:

// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

注意这里不用写成员名,不写它是一个嵌入,写了就是一个成员了,嵌入用在struct里有点多重继承的味道。在struct的方法中可以直接以struct名调用这个“成员”:

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的例子:

func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

goroutine

Go的并发模型是协程的,因此goroutine相当于轻量级线程,用法上和线程大同小异,只是协程间切换成本更低。Go中创建一个goroutine也非常简单:

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队列,默认是阻塞的。存取数据均使用符号<-,这里存变量时为毛不用->,真是太不形象了。

//创建一个channel
ch := make(chan int)
//把v push到队列里
ch <- v
//取出来
v := <-ch

如果创建channel时指定了size,那么将创建一个buffered channel,buffered channel只有在channel满的时候存数据才会阻塞,而在空的时候读数据会阻塞。(好像废话一样)

//buffered channles
ch := make(chan int, 20)

range&close

channel的生产者可以显式close,这时消费者可以接收到这一状态,一般来说消费者不主动关闭channel:

v, ok := <-ch

一个例子:

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分句可以执行时,随机选择一个,一个例子如下:

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, 他们在使用上有一些差别:

// 直接创造一个channel
timer1 = time.After(time.Second)
<-timer1
// 创造一个timer对象,它的C成员是我们需要的channel
timer2 = time.NewTimer(time.Second)
<-timer2.C
//同上	
ticker1 = time.Tick(time.Second)
<-ticker1
ticker2 = time.NewTicker(time.Second)
<-ticker2.C

异常处理

自己定义一个异常比较简单,就是实现接口:

type error interface {
    Error() string
}

这里上一个A Tour of Go的练习的例子说明:

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一般用在初始化阶段:

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

Recover

recover一般用在defer函数中,当发生panic时可以捕获错误。一般的模块设计中可以用recover把panic和调用者隔离,一个recover的例子:

// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}

Reference

  1. Go 语言简介(上)— 语法
  2. Go 语言简介(下)— 特性
  3. A Tour of Go(FuckGFW)
  4. Go by Example
  5. Package Documentation(FuckGFW)
  6. Effective Go(FuckGFW)
  7. 学习Go语言
  8. The Go Programming Language Specification(FuckGFW)

文章作者: Odin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Odin !
  目录