【Golang】Go语言编程思想(三):资源管理和出错处理

资源管理与出错处理

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(比如:文件无法打开)。

相关推荐
五味香8 分钟前
Java学习,字符串搜索
java·c语言·开发语言·python·学习·golang·kotlin
大卡尔3 小时前
Reviewbot 开源 | 这些写 Go 代码的小技巧,你都知道吗?
golang·go·静态检查·reviewbot
闲人怪喵7 小时前
/usr/local/go/bin/go: cannot execute binary file: Exec format error
开发语言·后端·golang
免檒13 小时前
GO并发编程
开发语言·golang
MavenTalk17 小时前
新手上路,学Go还是Python
开发语言·python·微服务·golang
007php00720 小时前
php项目的sdk封装成composer包的创建与发版
运维·开发语言·nginx·golang·github·php·composer
007php00721 小时前
go语言zero框架对接阿里云消息队列MQ的rabbit的配置与调用
开发语言·后端·python·gpt·阿里云·golang·github
YGGP21 小时前
【Golang】Go语言编程思想(六):Channel,第三节,使用Channel实现树的遍历
golang
Ai 编码助手21 小时前
Go 协程上下文切换的代价
服务器·golang