A Bite of GoLang (3)

叁叁肆2018-09-19 11:09


本文来自网易云社区


作者:盛国存


4.3、接口的组合

1、定义

什么叫接口的组合?当然这就是它的字面上的意思,接口可以组合其他的接口。这种方式等效于在接口中添加其他的接口的方法。在系统函数中就有很多这样的组合,比如:ReadWriter

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
    Reader
    Writer
}

在常见的读写文件的时候,网络相关以及一些底层的东西经常会遇到 Reader 、Writer

2、实例演示

为了更好的理解接口的组合的概念,下面用一个简单的例子来进一步了解

// 定义Reader接口
type Reader interface {
    read()
}
// 定义Writer接口
type Writer interface {
    write()
}
// 实现上述两个接口
type myReaderWriter struct {
}

func (mrw *myReaderWriter) read()  {
    fmt.Println("myReaderWriter read func...")
}

func (mrw *myReaderWriter) write() {
    fmt.Println("myReadWriter writer func...")
}
// 定义一个接口,组合上述两个接口
type ReaderWriterV1 interface {
    Reader
    Writer
}
// 等价于
type ReaderWriterV2 interface {
    read()
    write()
}

func main() {
    mrw := &myReaderWriter{}
    //mrw对象实现了read()方法和write()方法,因此可以赋值给ReaderWriterV1和ReaderWriterV2
    var rwv1 ReaderWriterV1 = mrw
    rwv1.read()
    rwv1.write()

    var rwv2 ReaderWriterV2 = mrw
    rwv2.write()
    rwv2.read()

    //同时,ReaderWriterV1和ReaderWriterV2两个接口对象可以相互赋值
    rwv1 = rwv2
    rwv2 = rwv1
}


4.4、常用的系统接口

1、Stringer

这个就是常见的 toString 的功能,

// Stringer is implemented by any value that has a String method,
// which defines the ``native'' format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
    String() string
}

Stringer接口定义在fmt包中,该接口包含String()函数。任何类型只要定义了String()函数,进行Print输出时,就可以得到定制输出。比如:

package main

import "fmt"

type Person struct{
    age int
    gender string
    name string
}

func (p Person) String() string {
    return fmt.Sprintf("age:%d, gender:%s, name:%s", p.age, p.gender, p.name)
}

func main() {
    var i Person = Person{
        age: 25,
        gender: "male",
        name: "sheng.guocun",
    }
    fmt.Printf("%s\n", i)
    fmt.Println(i)
    fmt.Printf("%v", i)
}

结果输出为:

age:25, gender:male, name:sheng.guocun
age:25, gender:male, name:sheng.guocun
age:25, gender:male, name:sheng.guocun

2、Reader、Writer

Reader Writer 上面有提到过,就是常见的读写文件的时候经常会用到,就是对文件的一个抽象,但是不仅这些,比如常见的

// NewScanner returns a new Scanner to read from r.
// The split function defaults to ScanLines.
func NewScanner(r io.Reader) *Scanner {
    return &Scanner{
        r:            r,
        split:        ScanLines,
        maxTokenSize: MaxScanTokenSize,
    }
}

这的参数也是一个Reader,还有很多的底层的代码都是基于 Reader Writer 的,这里就不一一举例了。


5. 函数式编程

5.0、函数式编程

Go语言作为一个通用型语言,它对函数式编程主要体现在闭包上面。

1、函数式编程 VS 函数指针

  • 函数是一等公民:参数、变量、返回值都可以是函数,在别的语言中大多不是这样的,比如在C++里面只有函数指针,在Java里面我们只能调用,不能把函数传给别人。

  • 高阶函数:参数可以是函数,1.6.3里面的apply函数就是一个高阶函数。

  • 函数 --> 闭包:首先用个例子来了解一下闭包的用法

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(v int) int {
        sum += v
        return sum
    }
}

func main() {

    a := adder()
    for i := 0; i < 10; i ++  {
        fmt.Printf("0 + 1 + 2 + ... + %d = %d\n", i, a(i))
    }
}

结果输出为

0 + 1 + 2 + ... + 0 = 0
0 + 1 + 2 + ... + 1 = 1
0 + 1 + 2 + ... + 2 = 3
0 + 1 + 2 + ... + 3 = 6
0 + 1 + 2 + ... + 4 = 10
0 + 1 + 2 + ... + 5 = 15
0 + 1 + 2 + ... + 6 = 21
0 + 1 + 2 + ... + 7 = 28
0 + 1 + 2 + ... + 8 = 36
0 + 1 + 2 + ... + 9 = 45

上述的 v 就称为局部变量, sum 称为自由变量,func(v int) int { sum += v return sum } 称为函数体,整个就叫做一个闭包。用一张图来概括就是:

2、“正统”函数式编程

  • 不可变性:不能有状态,只有常量和函数;当然这和平常的函数不一样,连变量都没有,甚至连选择语句、循环语句都没有。

  • 函数只能有一个参数

要是上面的累加想做一个稍微正统函数怎么做呢?

type iAdder func(int) (int, iAdder)

func adderV2(base int) iAdder {
    return func(v int) (int, iAdder) {
        return base + v, adderV2(base + v)
    }
}

func main() {

    a := adderV2(0)
    for i := 0; i < 10; i ++ {
        var s int
        s, a = a(i)
        fmt.Printf("0 + 1 + 2 + ... + %d = %d\n", i, s)
    }
}

当然正统的不一定是最好的,正统式的写法经常导致代码的可读性变得不是很好。


5.1、函数式编程实例演示

1、斐波那契数列

Go语言的官方案列中,对闭包的讲解通过一个常见的例子:斐波那契数列,为了更好的理解闭包的感念,那这里我们就将这个例子再来演示一遍

func Fibonacci() func() int {

    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

比如我们要打印这一串斐波那契数列,我们就需要不停的调用上面的斐波那契数列的生成器。

f := Fibonacci()

f() // 1
f() // 1
f() // 2
f() // 3
f() // 5
f() // 8

2、为函数实现接口

这个斐波那契数列的调用的生成器跟文件有点像,我们可以把它包装成一个 io.Reader 这样就跟打印一个文件一样生成这个斐波那契数列。

首先我们先定义我们的类型 func() int ,就取名Generate好了

type Generate func() int

同时需要将 Fibonacci() 函数的类型改掉

func Fibonacci() Generate {

    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

是一个类型就可以实现接口,这就是Go语言灵活的地方,下一步我们实现这个Reader接口

func (gen Generate) Read(p []byte) (n int, err error) {

    nextNum := gen()
    numString := fmt.Sprintf("%d \n", nextNum)

    return strings.NewReader(numString).Read(p)
}

这里我们会发现函数也可以实现接口,这就是Go语言的不一样的地方,因为函数是一等公民,它既可以作为参数,也可以作为接收者。首先我们要先取到下一个元素 nextNum ,然后将下一个元素写进 p 。然后我们直接用一个写好的文件打印的函数

func printFileContents(reader io.Reader) {
    scanner := bufio.NewScanner(reader)

    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

最后我们就可以直接调用了

func main() {

    f := Fibonacci()
    printFileContents(f)
}

当然,上述的代码是存在瑕疵的,比如这个 printFileContents 函数会一直读下去,就变成一个死循环了,我们需要设置其终止条件。比如上面的 p 太小的话,只读了一半,当然这边就留给读者后期拓展了。


6. 错误处理和资源管理

我们实际的代码不止 Hello World ,我们的代码是要运行在服务器上的,要和各种各样的用户进行交互,所以我们这里就要了解一下Go语言的资源管理和出错处理。

6.0、defer调用

1、确保在函数结束时调用

比如一个常见的代码

package main

import "fmt"

func main() {

    fmt.Println(1)
    fmt.Println(2)
}

我要是想要让1在2后面输出该如何做呢?你说调换一下顺序呗,道理我都懂,但是我们今天要介绍的不是这个,我们只需要在打印1之前加一个 defer 就可以了

package main

import "fmt"

func main() {

    defer fmt.Println(1)
    fmt.Println(2)
}

要是有多个defer呢?它的输出顺序又是什么样的呢?

package main

import "fmt"

func main() {

    defer fmt.Println(1)
    defer fmt.Println(2)
    fmt.Println(3)
}

上面这段代码,输出的结果又是什么?

3
2
1

这里我们可以发现 defer 的调用实际是一个栈,先进后出。当然 defer 的最大的好处是什么呢?就是当程序中间有return返回甚至panic的时候,依然不影响 defer 后面的代码的执行。

package main

import "fmt"

func main() {

    defer fmt.Println(1)
    defer fmt.Println(2)
    fmt.Println(3)
    panic("whoops, something went wrong....")
    fmt.Println(4)
}

上述的代码在panic之后,1 2 依然能够正常输出。

2、场景演示

当然说了这么多,我们在代码中常见的使用defer的场景有哪些呢?比如我们创建文件,写文件这些,过去我们用别的语言经常会在处理文件的最后释放句柄,因为中间隔了很多的文件操作,经常可能会忘记释放句柄。那Go语言就针对这样的场景做了非常好的优化,通过defer关键字实现,下面我们就通过一个简单的写文件事例来演示一下:

package main

import (
    "os"
    "bufio"
    "fmt"
)

func main() {

    filename := "demo.txt"
    file, err := os.Create(filename)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    writer := bufio.NewWriter(file)
    defer writer.Flush()

    fmt.Fprintln(writer, "你好,杭州")
}

一个完整的事例就演示到这边,比如常见的 Open/Close、Lock/Unlock这些成对出现的都可以使用 defer


6.1、错误处理概念

因为在我们实际的程序中,有错直接panic中断程序执行,这时非常不友好的,通常我们会对其出错处理。比如上面的事例中 os.Create 函数返回的 err 不为 nil 的时候,我们需要做一个出错处理,

filename := "demo.txt"
file, err := os.Create(filename)
if err != nil {
    fmt.Println(err.Error())
    return
}

我们可以直接打印出相关的错误信息,然后直接返回。这就是常见的错误处理方式之一,当然在函数内部也可以将错误信息直接作为结果返回。


6.2、panic和recover

1、panic

  • 停止当前函数执行

panic和我们其他语言的throw exception很像

  • 一直向上返回,执行每一层的defer

当然相对还是友好的,每层的defer还是会用到,一层一层的返回,返回到最后程序就会自动退出了

  • 如果没有遇见recover,程序退出

2、recover

  • 仅在defer调用中使用

  • 获取panic的值

  • 如果无法处理,可充新panic

主要的特性就可以用上述几句话概括,为了更好的理解上述的概念,下面用一个简短的代码来学以致用

func tryDefer() {
    for i := 0; i < 100; i++ {
        defer fmt.Println(i)
        if i == 10 {
            panic("就打印到这吧")
        }
    }
}

上面就是一个 panic 和 defer 的结合使用,他的输出结果会是什么样的呢?

10
9
8
7
6
5
4
3
2
1
0
panic: 就打印到这吧

goroutine 1 [running]:
main.tryDefer()
    /Users/verton/GoLangProject/src/shengguocun.com/web/web.go:11 +0x11d
main.main()
    /Users/verton/GoLangProject/src/shengguocun.com/web/web.go:18 +0x20

从上述输出结果我们可以看到panic的前两个特性,那结合recover又会是什么样的呢?

// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
func recover() interface{}
func tryRecover() {
    defer func() {
        r := recover()
        if err, ok := r.(error); ok {
            fmt.Println("错误信息: ", err)
        } else {
            panic(r)
        }
    }()

    panic(errors.New("这是一个 error"))
}

从上面我们可以看到 recover 是一个interface, 所以在判断的时候需要判断 r 是否是一个 error,结果自然会是输出

错误信息:  这是一个 error

那我们再用一个实际一点的例子来测试一下,比如除数为0的例子

func tryRecover() {
    defer func() {
        r := recover()
        if err, ok := r.(error); ok {
            fmt.Println("错误信息: ", err)
        } else {
            panic(r)
        }
    }()

    b := 0
    a := 5 / b
    fmt.Println(a)
}

结果输出

错误信息:  runtime error: integer divide by zero

上面的两个例子简单介绍了panic、recover的基本使用,下面通过一个稍微实际一点的例子来综合讲述一下一般的项目中是如何统一处理错误的。



相关阅读:A Bite of GoLang (1)

A Bite of GoLang (2)

A Bite of GoLang (3)

A Bite of GoLang (4)

A Bite of GoLang (5)

A Bite of GoLang (6)

A Bite of GoLang (7)

A Bite of GoLang (8)


网易云免费体验馆0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区