资源管理与出错处理
Golang 通过 defer 调用来确保调用在函数结束或 panic 时发生,从而进行资源管理。
对语句使用 defer 关键字,则这条语句将会在函数结束时才运行,比如:
go
package main
import "fmt"
func tryDefer() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
}
func main() {
tryDefer()
}
输出的结果是:
go
3
2
1
一个更典型的例子如下,在该例当中我们实现了一个函数,用于向给定文件当中写入前二十个 Fibonacci 数:
go
func writeFile(filename string) {
file, err := os.Create(filename)
if err != nil {
panic(err)
}
defer file.Close() // 打开的 file 使用 defer 进行 close
// 直接读写文件比较慢, 使用 bufio 来对文件进行读写
writer := bufio.NewWriter(file)
defer writer.Flush() // 将 buffer 当中的内容写入到文件
f := fib.Fibonacci()
for i := 0; i < 20; i++ {
fmt.Fprintln(writer, f())
}
// 函数结束时, 将会首先运行 writer.Flush(), 再运行 file.Close()
}
何使使用 defer 调用?
- Open/Close
- Lock/Unlock
- PrintHeader/PrintFooter
错误处理
现在我们对之前的 writeFile 函数进行修改,修改的结果如下:
go
func writeFile(filename string) {
file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)
if err != nil {
panic(err)
}
defer file.Close() // 打开的 file 使用 defer 进行 close
// 直接读写文件比较慢, 使用 bufio 来对文件进行读写
writer := bufio.NewWriter(file)
defer writer.Flush() // 将 buffer 当中的内容写入到文件
f := fib.Fibonacci()
for i := 0; i < 20; i++ {
fmt.Fprintln(writer, f())
}
}
👆 如果此时要写入的目标文件存在,那么程序会在第一个 panic 挂掉。但是我们不希望程序只给出出错的信息,我们希望在函数执行的过程中给出错误信息,并试图对错误进行修正,或是将函数返回以避免函数进一步执行得到不理想的结果:
go
if err != nil {
// fmt.Println("file already exists")
fmt.Println("Error:", err.Error())
return
}
现在我们关心的是,err 当中本身包含哪些内容。根据 Golang 提供的文档,得知 err 本质上是一个 *PathError 类型。可以通过下述方式得到 err 当中包含哪些内容:
go
if err != nil {
//fmt.Println("file already exists")
if pathError, ok := err.(*os.PathError); !ok {
panic(err)
} else {
fmt.Println(pathError.Op, pathError.Path, pathError.Err)
}
return
}
得到的结果如下:
go
open fib.txt The file exists.
其中 open 是 pathError.Op,fib.txt 是 pathError.Path,The file exists 是 PathError.Err。
我们自己也可以新建一些 error,比如:
go
err = errors.New("ths is a custom error")
error 本身是一个 interface,实现 interface 的方法即可定义我们自己的 error:
go
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
如何实现统一的错误处理逻辑?
现在我们想要实现一个统一的错误处理逻辑,上面的向文件中写内容的例子过于简单,现在我们试图实现一个文件显示服务器,它能够打开给定的 url 并将内容写入到文件当中:
go
package main
import (
"io"
"net/http"
"os"
)
func main() {
http.HandleFunc("/list/",
func(writer http.ResponseWriter, request *http.Request) {
path := request.URL.Path[len("/list/"):] // /list/fib.txt
file, err := os.Open(path)
if err != nil {
panic(err)
}
defer file.Close()
all, err := io.ReadAll(file)
if err != nil {
panic(err)
}
writer.Write(all)
})
err := http.ListenAndServe(":8888", nil)
if err != nil {
panic(err)
}
}
在浏览器打开 localhost:8888/list/fib.txt,即可显示:
但是如果给定一个错误的文件地址,程序仍然会 panic,我们希望对上面的错误 err 进行封装:
go
if err != nil {
http.Error(writer,
err.Error(),
http.StatusInternalServerError)
return
}
这种情况下,会将错误显示到浏览器,即直接展示给用户,这还不够好,我们希望对 err 的处理进行进一步的封装。
首先,我们对文件的目录进行组织。将服务器运行的代码放在 web.go 当中,将 web.go 放在 filelistingserver 目录下。在此基础上,在 filelistingserver 目录下新建 filelisting 目录,将 handler.go 放在 filelisting 目录下,handler 当中的内容是:
go
package filelisting
import (
"io"
"net/http"
"os"
)
func HandleFileList(writer http.ResponseWriter, request *http.Request) error {
path := request.URL.Path[len("/list/"):] // /list/fib.txt
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
all, err := io.ReadAll(file)
if err != nil {
return err
}
writer.Write(all)
return nil
}
👆 可以看到,它是对 http.HandleFunc(即最开始代码当中 main 函数体的第一行)第二个参数的抽象,在 HandleFileList 当中完成了服务的主要业务逻辑,但是在这个函数当中不对错误信息进行处理,而是统一地将错误信息进行返回,在其它地方对错误进行处理。
再来看目前的 web.go 文件的代码:
go
package main
import (
"learngo/errhandling/filelistingserver/filelisting"
"net/http"
"os"
)
type appHandler func(writer http.ResponseWriter, request *http.Request) error
func errWrapper(handler appHandler) func(http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
err := handler(writer, request)
if err != nil {
code := http.StatusOK
switch {
case os.IsNotExist(err):
code = http.StatusNotFound
default:
code = http.StatusInternalServerError
}
http.Error(writer, http.StatusText(code), code)
}
}
}
func main() {
http.HandleFunc("/list/", errWrapper(filelisting.HandleFileList))
err := http.ListenAndServe(":8888", nil)
if err != nil {
panic(err)
}
}
web.go 当中新定义了一个名为 appHandler 的类型,它是func(writer http.ResponseWriter, request *http.Request) error
的别名。而函数 errWrapper 试图对错误信息进行打包,它的输入就是一个函数,这个函数的别名正是 appHandler,它返回的是函数func(http.ResponseWriter, *http.Request)
,在它的返回值当中定义了一个匿名函数,这个匿名函数会对每一个错误类型进行详细的处理,并返回状态码,比如 StatusNotFound 对应的状态码是 404。通过对 web.go 进行错误包装处理,此时在 localhost:8888 错误的给定一个文件路径,将不会返回系统内部 panic 的错误,而是直接显示 Not Found,即:将错误进行封装,返回的错误信息是开发者希望用户看到的,而不是直接返回系统内部的错误信息。
errWrapper 是对函数式编程的典型应用,它的输入是一个函数,输出也是一个函数,相当于把输入的函数包装成了输出函数,听起来与 Python 的装饰器非常的相似。
error vs. panic
尽可能地不要使用 panic(意料之外的错误使用 panic,比如数组越界),意料之中的错误使用 error(比如:文件无法打开)。