在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。
这让程序员调用方法或函数时,能够检查它是否像预期那样执行完毕。
在Go语言中,错误是一个值。标准库声明了接口error,如下所示。
type error interface{
Error() string
}
这个接口只有一个方法——Error,它返回一个字符串。
标准库中的errors包支持创建和操作错误。下例演示了如何创建并打印错误。
package main
import (
"fmt"
"errors"
)
func main() {
err := errors.New("Something wrong")
if err != nil{
fmt.Println(err)
}
}
除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)
}
}
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语言错误处理方式的一个优点:错误处理不是在函数中,而是在调用函数的地方进行的。这在错误处理方面提供了极大的灵活性,而不是简单地一刀切。
除从技术角度考虑Go语言的错误处理方式和错误生成方式外,还需从以用户为中心的角度考虑错误。编写供他人使用的库或包时,您编写和使用错误的方式将极大地影响可用性。
如果库用户相信错误会以一致的方式返回,且包含有用的错误消息,则用户能够从错误中恢复的可能性将高得多。他们很可能也会认为您编写的库不仅很有用,而且值得信任。
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可能是正确的选择。
在最简单的计算机程序中,操作是依次执行的,执行顺序与出现顺序相同。
另一种理念是不必等到一个操作执行完毕后再执行下一个,编程任务和编程环境越复杂,这种理念就越重要。提出这种理念旨在让程序能够应对更复杂的情形,避免执行完一行代码后再执行下一行,从而提高程序的执行速度。
同时烤多个蛋挞被称为并发;而将烤蛋挞的任务分为两部分,由两家分别烤,烤好后再放在一起,这被称为并行。
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在幕后使用线程来管理并发,但Goroutine让程序员无须直接管理线程,它消除了这样做的痛苦。创建一个Goroutine只需占用几KB的内存,因此即便创建数千个Goroutine也不会耗尽内存。另外,创建和销毁Goroutine的效率也非常高。
Goroutine是一个并发抽象,因此开发人员通常无须准确地知道操作系统中发生的情况。
如果说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)
}
说明:
通常,通道收到消息后就可将其发送给接收者,但有时候可能没有接收者。在这种情况下,可使用缓冲通道。缓冲意味着可将数据存储在通道中,等接收者准备就绪再交给它。要创建缓冲通道,可向内置函数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或需要限制调度的工作量时,缓冲通道很有效。
给通道指定消息接收者是一个阻塞操作,因为它将阻止函数返回,直到收到一条消息为止。
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
说明:
可将通道作为参数传递给函数,并在函数中向通道发送消息。要进一步指定在函数中如何使用传入的通道,可在传递通道时将其指定为只读、只写或读写的。指定通道是只读、只写、读写的语法差别不大。
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右边时,表示通道在函数内是只写的;没有指定<-时,表示通道是可读写的。
假设有多个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.")
}
在已知需要停止执行的时间的情况下,使用超时时间是不错的选择,但在有些情况下,不确定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
问:关闭通道时会导致缓冲的消息丢失吗?
答:关闭缓冲通道意味着不能再向它发送消息。缓冲的消息会被保留,可供接收者读取。