美文网首页程序员
《Go语言入门经典》10~12章读书笔记

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

作者: 跑马溜溜的球 | 来源:发表于2020-06-24 11:47 被阅读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
    

    说明:

    • go 使得slowFunc()不会阻塞下面的代码
    • time.Sleep(time.Second * 3)是为了让main可以等到slowFunc执行完成,不然将无法看到sleeper() finished的输出。

    11.6 定义Goroutine

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

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

    第12章通道简介

    12.1 使用通道

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

    通道的创建语法如下。

    c := make(chan string)
    
    • 使用简短变量赋值,将变量c初始化为:=右边的值。
    • 使用内置函数make创建一个通道,这是使用关键字chan指定的。
    • 关键字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)
    }
    

    说明:

    • 创建一个存储字符串数据的通道,并将其赋给变量c。
    • 函数slowFunc将通道当作参数。
    • slowFunc函数的单个参数指定了一个通道和一个字符串的数据类型。
    • 声明变量msg,用于接收来自通道c的消息。这将阻塞进程直到收到消息为止,从而避免进程过早退出。
    • 函数slowFunc执行完毕后向通道c发送一条消息。
    • 接收并打印这条消息。
    • 由于没有其他的语句,因此程序就此退出。

    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)
    }
    

    解读如下

    • 创建一个长度为2的缓冲通道。
    • 向通道发送两条消息。此时没有可用的接收者,因此消息被缓冲。
    • 关闭通道(close),这意味着不能再向它发送消息。
    • 将通道作为参数传递给函数receiver。
    • 函数receiver使用range迭代通道,并将通道中缓冲的消息打印到控制台。

    在知道需要启动多少个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

    说明:

    • time.NewTicker每秒触发一次,效果和使用time.Sleep(time.Second * 1)一样。如果你用过js, 这两者可以分别类比为setInterval和setTimeout。

    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 问与答

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

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

    相关文章

      网友评论

        本文标题:《Go语言入门经典》10~12章读书笔记

        本文链接:https://www.haomeiwen.com/subject/obkrfktx.html