《Go语言入门经典》10~12章读书笔记
第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 问与答
问:关闭通道时会导致缓冲的消息丢失吗?
答:关闭缓冲通道意味着不能再向它发送消息。缓冲的消息会被保留,可供接收者读取。