程序员

《Go语言入门经典》10~12章读书笔记

2020-06-24  本文已影响0人  跑马溜溜的球

第10章处理错误

10.1 错误处理及Go语言的独特之处

在Go语言中,一种约定是在调用可能出现问题的方法或函数时,返回一个类型为错误的值。这意味着如果出现问题,函数通常不会引发异常,而让调用者决定如何处理错误。

package main

import (
    "fmt"
    "io/ioutil"
)


func main() {
    file, err := ioutil.ReadFile("foo.txt");
    if err != nil{
        fmt.Println(err)
        return
    }

    fmt.Printf("%s", file)
} 

在没有文件foo.txt的系统中运行这个程序时,将触发错误。

要理解Go语言处理错误的方式,最重要的是明白函数ReadFile接受一个字符串参数,并返回一个字节切片和一个错误值。这个函数的定义如下。

func ReadFile(filename string) ([]byte, error)

这意味着函数ReadFile总是会返回一个错误值,可对其进行检查。

在Go语言中,有一种约定是,如果没有发生错误,返回的错误值将为nil。
这让程序员调用方法或函数时,能够检查它是否像预期那样执行完毕。

10.2 理解错误类型

在Go语言中,错误是一个值。标准库声明了接口error,如下所示。

type error interface{
    Error() string
}

这个接口只有一个方法——Error,它返回一个字符串。

10.3 创建错误

标准库中的errors包支持创建和操作错误。下例演示了如何创建并打印错误。

package main

import (
    "fmt"
    "errors"
)


func main() {
    err := errors.New("Something wrong")
    if err != nil{
        fmt.Println(err)
    }
}

10.4 设置错误的格式

除errors包外,标准库中的fmt包还提供了方法Errorf,可用于设置返回的错误字符串的格式。

package main

import (
    "fmt"
)

func main() {
    name, role := "yuxi", "dancer"
    err := fmt.Errorf("%v %v", name, role)
    if err != nil {
        fmt.Println(err)
    }   
}

10.5 从函数返回错误

Go语言的做法是从函数和方法中返回一个错误值。下面是一个示例。

package main

import (
    "fmt"
)

func Half(num int)(int, error){
    if num % 2 != 0{
        return -1, fmt.Errorf("Cannot half %v", num)
    }

    return num / 2, nil
}

func main() {
    n, err := Half(9)
    if err != nil{
        fmt.Println(err)
        return
    }

    fmt.Println(n)
}

这个示例演示了Go语言错误处理方式的一个优点:错误处理不是在函数中,而是在调用函数的地方进行的。这在错误处理方面提供了极大的灵活性,而不是简单地一刀切。

10.6 错误和可用性

除从技术角度考虑Go语言的错误处理方式和错误生成方式外,还需从以用户为中心的角度考虑错误。编写供他人使用的库或包时,您编写和使用错误的方式将极大地影响可用性。

如果库用户相信错误会以一致的方式返回,且包含有用的错误消息,则用户能够从错误中恢复的可能性将高得多。他们很可能也会认为您编写的库不仅很有用,而且值得信任。

10.7 慎用panic

panic是Go语言中的一个内置函数,它终止正常的控制流程并引发恐慌(panicking),导致程序停止执行。出现普通错误时,并不提倡这种做法,因为程序将停止执行,并且没有任何回旋余地。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("This is executed")
    panic("oh no. i can do no more")
    fmt.Println("This is not executed")
}

运行这个示例将引发panic,导致程序崩溃。

This is executed
panic: oh no. i can do no more

goroutine 1 [running]:
main.main()
        /home/go/panic.go:10 +0x79
exit status 2

调用panic后,程序将停止执行,因此打印This is not executed的代码行根本没有机会执行。

在下面的情形下,使用panic可能是正确的选择。

第11章使用Goroutine

11.1 理解并发

在最简单的计算机程序中,操作是依次执行的,执行顺序与出现顺序相同。

另一种理念是不必等到一个操作执行完毕后再执行下一个,编程任务和编程环境越复杂,这种理念就越重要。提出这种理念旨在让程序能够应对更复杂的情形,避免执行完一行代码后再执行下一行,从而提高程序的执行速度。

11.2 并发和并行

同时烤多个蛋挞被称为并发;而将烤蛋挞的任务分为两部分,由两家分别烤,烤好后再放在一起,这被称为并行。

11.5 使用Goroutine处理并发操作

Go语言提供了Goroutine,让您能够处理并发操作。下例中,通过使用Goroutine,可在调用函数slowFunc后立即执行main函数中的第二行代码。在这种情况下,函数slowFunc依然会执行,但不会阻塞程序中其他代码行的执行。Goroutine使用起来非常简单,只需在要让Goroutine执行的函数或方法前加上关键字go即可。

package main

import (
    "fmt"
    "time"
)

func slowFunc(){
    time.Sleep(time.Second * 2)
    fmt.Println("sleeper() finished")
}

func main() {
    go slowFunc()
    fmt.Println("i am now shown straightway")

    time.Sleep(time.Second * 3)
}

运行结果

i am now shown straightway
sleeper() finished

说明:

11.6 定义Goroutine

Go在幕后使用线程来管理并发,但Goroutine让程序员无须直接管理线程,它消除了这样做的痛苦。创建一个Goroutine只需占用几KB的内存,因此即便创建数千个Goroutine也不会耗尽内存。另外,创建和销毁Goroutine的效率也非常高。

Goroutine是一个并发抽象,因此开发人员通常无须准确地知道操作系统中发生的情况。

第12章通道简介

12.1 使用通道

如果说Goroutine是一种支持并发编程的方式,那么通道就是一种与Goroutine通信的方式。通道让数据能够进入和离开Goroutine,可方便Goroutine之间进行通信。

通道的创建语法如下。

c := make(chan string)

向通道发送消息的语法如下。

c <- "hello"

请注意其中的<-,这表示将右边的字符串发送给左边的通道。如果通道被指定为收发字符串,则只能向它发送字符串消息,如果向它发送其他类型的消息将导致错误。

从通道那里接收消息的语法如下。

msg := <-c

现在可对程序清单11.5节中的代码进行修改以使用通道,如程序清单如下。

package main

import (
    "fmt"
    "time"
)

func slowFunc(c chan string){
    time.Sleep(time.Second * 2)
    c <- "showFunc() finished"
}

func main() {
    c := make(chan string)
    go slowFunc(c)

    fmt.Println("i am now shown straightway")
    
    msg := <-c
    fmt.Println(msg)
}

说明:

12.2 使用缓冲通道

通常,通道收到消息后就可将其发送给接收者,但有时候可能没有接收者。在这种情况下,可使用缓冲通道。缓冲意味着可将数据存储在通道中,等接收者准备就绪再交给它。要创建缓冲通道,可向内置函数make传递另一个表示缓冲区长度的参数。

message := make(chan string, 2)

这些代码创建一个可存储两条消息的缓冲通道。缓冲通道最多只能存储指定数量的消息,如果向它发送更多的消息将导致错误。

package main

import (
    "fmt"
)

func receiver(c chan string){
    for msg := range c{
        fmt.Println(msg)
    }
}

func main() {
    msg := make(chan string, 2)
    msg <- "hello"
    msg <- "world"
    
    close(msg)
    
    receiver(msg)
}

解读如下

在知道需要启动多少个Goroutine或需要限制调度的工作量时,缓冲通道很有效。

12.3 阻塞和流程控制

给通道指定消息接收者是一个阻塞操作,因为它将阻止函数返回,直到收到一条消息为止。

package main

import (
    "fmt"
    "time"
)

func pinger(c chan string){
    t := time.NewTicker(1 * time.Second)
    for {
        c <- "ping"
        <-t.C
    }
}

func main() {
    msgs := make(chan string)
    
    go pinger(msgs)
    
    for {
       msg  := <-msgs
       fmt.Println(msg)
    }   
}

运行结果是每秒输出一个ping

说明:

12.4 将通道用作函数参数

可将通道作为参数传递给函数,并在函数中向通道发送消息。要进一步指定在函数中如何使用传入的通道,可在传递通道时将其指定为只读、只写或读写的。指定通道是只读、只写、读写的语法差别不大。

func channelReader(messages <-chan string){
    msg := <-messages
    fmt.Println(msg)
}

func channelWriter(messages chan<- string){
    messages <- "Hello world"
}

func channelReaderAndWriter(messages chan string){
    msg := <-messages
    fmt.Println(msg)
    messages <- "Hello world"
}

<-位于关键字chan左边时,表示通道在函数内是只读的;<-位于关键字chan右边时,表示通道在函数内是只写的;没有指定<-时,表示通道是可读写的。

12.5 使用select语句

假设有多个Goroutine,而程序将根据最先返回的Goroutine执行相应的操作,此时可使用select语句。它为通道创建一系列接收者,并执行最先收到消息的接收者。

select语句看起来和switch语句很像。

    ch1 := make (chan string)
    ch2 := make (chan string)

    select{
        case msg1 := <- ch1:
            fmt.Println("recv msg1", msg1)
        case msg2 := <- ch2:
            fmt.Println("recv msg2", msg2)
    }
}

如果从通道ch1那里收到了消息,将执行第一条case语句;如果从通道ch2那里收到了消息,将执行第二条case语句。具体执行哪条case语句,取决于消息到达的时间,哪条消息最先到达决定了将执行哪条case语句。通常,接下来收到的其他消息将被丢弃。收到一条消息后,select语句将不再阻塞。

下面的程序演示了select语句

package main

import (
    "fmt"
    "time"
)

func ping1(c chan string){
    time.Sleep(time.Second * 1)
    c <- "ping on channel1"
}

func ping2(c chan string){
    time.Sleep(time.Second * 2)
    c <- "ping on channel2"
}

func main() {
    ch1 := make (chan string)
    ch2 := make (chan string)
    
    go ping1(ch1)
    go ping2(ch2)

    select{
        case msg1 := <- ch1:
            fmt.Println("recv msg1", msg1)
        case msg2 := <- ch2:
            fmt.Println("recv msg2", msg2)
    }
}

要根据最先收到的消息采取相应的措施,select语句是一个不错的选择。但如果没有收到消息呢?为此可使用超时时间。这让select语句在指定时间后不再阻塞,以便接着往下执行。

下面的程序添加了一个超时case语句,指定在0.5s内没有收到消息时将采取的措施。

select{
        case msg1 := <- ch1:
            fmt.Println("recv msg1", msg1)
        case msg2 := <- ch2:
            fmt.Println("recv msg2", msg2)
        case <-time.After(500 * time.Millisecond):
            fmt.Println("no msg recv. give up.")
}

12.6 退出通道

在已知需要停止执行的时间的情况下,使用超时时间是不错的选择,但在有些情况下,不确定select语句该在何时返回,因此不能使用定时器。在这种情况下,可使用退出通道。这种技术并非语言规范的组成部分,但可通过向通道发送消息来理解退出阻塞的select语句。

来看这样一种情形:程序需要使用select语句实现无限制地阻塞,但同时要求能够随时返回。通过在select语句中添加一个退出通道,可向退出通道发送消息来结束该语句,从而停止阻塞。可将退出通道视为阻塞式select语句的开关。对于退出通道,可随便命名,但通常将其命名为stop或quit。在下面的示例中,在for循环中使用了一条select语句,这意味着它将无限制地阻塞,并不断地接收消息。通过向通道stop发送消息,可让select语句停止阻塞:从for循环中返回,并继续往下执行。

    messages := make (chan string)
    stop := make(chan bool)

    for {
        select{
            case <-stop:
                return
            case msg := <- messages:
                fmt.Println(msg)
        }
    }

在应用程序的某部分向通道发送消息,并要在未来的某个位置时点终止时,这种技术是很有效的。

下面是一个完整的退出通道使用示例。在这个示例中,等待一定的时间后向退出通道发送了消息。但在实际工作中,具体等待多长时间可能取决于程序其他地方的未知事件何时发生。

package main

import (
    "fmt"
    "time"
)

func sender(c chan string){
    t := time.NewTicker(time.Second * 1)
    for{
        c <- "i'm sending a msg"
        <-t.C
    }
}

func main() {
    messages := make (chan string)
    stop := make(chan bool)
    
    go sender(messages)
    
    go func(){
        time.Sleep(time.Second * 2)
        fmt.Println("time is up")
        stop <- true
    }()

    for {
        select{
            case <-stop:
                return
            case msg := <- messages:
                fmt.Println(msg)
        }
    }
}

运行结果

i'm sending a msg
i'm sending a msg
time is up

12.8 问与答

问:关闭通道时会导致缓冲的消息丢失吗?

答:关闭缓冲通道意味着不能再向它发送消息。缓冲的消息会被保留,可供接收者读取。

上一篇下一篇

猜你喜欢

热点阅读