即使是最基本的 Go 程序,错误处理也很重要。Go 中的错误实现了Error
接口,必须在代码的每一层进行处理。Go 错误不像异常那样工作,未经处理的错误可能会导致巨大的问题。无论何时发生错误,你都应该努力处理和考虑错误。
本章还介绍了日志记录,因为每当发生实际错误时都会记录日志。我们还将研究包装错误,以便给定的错误在返回函数堆栈时提供额外的上下文,以便更容易确定某些错误的实际原因。
本章将介绍以下配方:
- 处理错误和错误接口
- 使用 pkg/errors 包和包装错误
- 使用日志包并了解何时记录错误
- 带 apex 和 logrus 包的结构化日志记录
- 使用上下文包进行日志记录
- 使用包级全局变量
- 捕捉长期运行进程的恐慌
为了继续本章中的所有配方,请按照以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本 https://golang.org/doc/install 。
-
打开终端/控制台应用;创建并导航到项目目录,如
~/projects/go-programming-cookbook
。所有代码都将从此目录运行和修改。 -
将最新代码克隆到
~/projects/go-programming-cookbook-original
中,并且可以选择从该目录进行操作,而不是手动键入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
Error
接口是一个非常小和简单的接口:
type Error interface{
Error() string
}
这个界面很优雅,因为它很容易制作任何东西来满足它。不幸的是,这也给需要根据收到的错误采取某些操作的包带来了混乱。
在 Go 中有许多方法可以产生错误;本食谱将探索基本错误、具有赋值或类型的错误以及使用结构的自定义错误的创建。
以下步骤包括应用的编写和运行:
- 从终端/控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter4/basicerrors
的新目录,并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/basicerrors
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/basicerrors
- 从
~/projects/go-programming-cookbook-original/chapter4/basicerrors
复制测试,或者将其用作编写自己代码的练习! - 创建一个名为
basicerrors.go
的文件,其内容如下:
package basicerrors
import (
"errors"
"fmt"
)
// ErrorValue is a way to make a package level
// error to check against. I.e. if err == ErrorValue
var ErrorValue = errors.New("this is a typed error")
// TypedError is a way to make an error type
// you can do err.(type) == ErrorValue
type TypedError struct {
error
}
//BasicErrors demonstrates some ways to create errors
func BasicErrors() {
err := errors.New("this is a quick and easy way to create an error")
fmt.Println("errors.New: ", err)
err = fmt.Errorf("an error occurred: %s", "something")
fmt.Println("fmt.Errorf: ", err)
err = ErrorValue
fmt.Println("value error: ", err)
err = TypedError{errors.New("typed error")}
fmt.Println("typed error: ", err)
}
- 创建一个名为
custom.go
的文件,其内容如下:
package basicerrors
import (
"fmt"
)
// CustomError is a struct that will implement
// the Error() interface
type CustomError struct {
Result string
}
func (c CustomError) Error() string {
return fmt.Sprintf("there was an error; %s was the result", c.Result)
}
// SomeFunc returns an error
func SomeFunc() error {
c := CustomError{Result: "this"}
return c
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
main.go
文件:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/basicerrors"
)
func main() {
basicerrors.BasicErrors()
err := basicerrors.SomeFunc()
fmt.Println("custom error: ", err)
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
errors.New: this is a quick and easy way to create an error
fmt.Errorf: an error occurred: something
typed error: this is a typed error
custom error: there was an error; this was the result
- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
无论您是使用errors.New
、fmt.Errorf
还是自定义错误,最重要的一点是,您永远不应该在代码中留下未经处理的错误。这些定义错误的不同方法提供了很大的灵活性。例如,您可以在结构中添加额外的函数以进一步查询错误,并将接口转换为调用函数中的错误类型以获得一些附加功能。
接口本身非常简单,唯一的要求是返回一个有效字符串。将其连接到一个结构对于一些高级应用可能很有用,这些应用在整个过程中都有一致的错误处理,但希望与其他应用很好地协作。
位于github.com/pkg/errors
的errors
包装是标准 Goerrors
包装的替代品。此外,它还提供了一些非常有用的包装和处理错误的功能。前面配方中的类型化错误和声明的错误是一个很好的例子,它们可以用于向错误添加其他信息,但以标准方式包装它将更改其类型并破坏类型断言:
// this wont work if you wrapped it
// in a standard way. that is,
// fmt.Errorf("custom error: %s", err.Error())
if err == Package.ErrorNamed{
//handle this error in a specific way
}
此配方将演示如何使用pkg/errors
包在整个代码中为错误添加注释。
以下步骤包括应用的编写和运行:
- 从终端/控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter4/errwrap
的新目录,并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/errwrap
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/errwrap
- 从
~/projects/go-programming-cookbook-original/chapter4/errwrap
复制测试,或者将其用作编写自己代码的练习! - 创建一个名为
errwrap.go
的文件,其内容如下:
package errwrap
import (
"fmt"
"github.com/pkg/errors"
)
// WrappedError demonstrates error wrapping and
// annotating an error
func WrappedError(e error) error {
return errors.Wrap(e, "An error occurred in WrappedError")
}
// ErrorTyped is a error we can check against
type ErrorTyped struct{
error
}
// Wrap shows what happens when we wrap an error
func Wrap() {
e := errors.New("standard error")
fmt.Println("Regular Error - ", WrappedError(e))
fmt.Println("Typed Error - ",
WrappedError(ErrorTyped{errors.New("typed error")}))
fmt.Println("Nil -", WrappedError(nil))
}
- 创建一个名为
unwrap.go
的文件,其内容如下:
package errwrap
import (
"fmt"
"github.com/pkg/errors"
)
// Unwrap will unwrap an error and do
// type assertion to it
func Unwrap() {
err := error(ErrorTyped{errors.New("an error occurred")})
err = errors.Wrap(err, "wrapped")
fmt.Println("wrapped error: ", err)
// we can handle many error types
switch errors.Cause(err).(type) {
case ErrorTyped:
fmt.Println("a typed error occurred: ", err)
default:
fmt.Println("an unknown error occurred")
}
}
// StackTrace will print all the stack for
// the error
func StackTrace() {
err := error(ErrorTyped{errors.New("an error occurred")})
err = errors.Wrap(err, "wrapped")
fmt.Printf("%+v\n", err)
}
-
创建一个名为
example
的新目录并导航到它。 -
创建一个包含以下内容的
main.go
文件:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/errwrap"
)
func main() {
errwrap.Wrap()
fmt.Println()
errwrap.Unwrap()
fmt.Println()
errwrap.StackTrace()
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
Regular Error - An error occurred in WrappedError: standard
error
Typed Error - An error occurred in WrappedError: typed error
Nil - <nil>
wrapped error: wrapped: an error occurred
a typed error occurred: wrapped: an error occurred
an error occurred
github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter4/errwrap.StackTrace
/Users/lothamer/go/src/github.com/agtorre/go-
cookbook/chapter4/errwrap/unwrap.go:30
main.main
/tmp/go/src/github.com/agtorre/go-
cookbook/chapter4/errwrap/example/main.go:14
go.mod
文件应该更新,go.sum
文件现在应该出现在顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
pkg/errors
包是一个非常有用的工具。使用此包包装每个返回的错误,以便在日志记录和错误调试中提供额外的上下文,这是有意义的。它足够灵活,可以在发生错误时打印整个堆栈跟踪,也可以在打印错误时为错误添加前缀。它还可以清理代码,因为包装的 nil 返回一个nil
值;例如,考虑下面的代码:
func RetError() error{
err := ThisReturnsAnError()
return errors.Wrap(err, "This only does something if err != nil")
}
在某些情况下,这可以避免您在返回错误之前先检查错误是否为nil
。此配方演示如何使用包包装和展开错误,以及基本的堆栈跟踪功能。该包的文档还提供了一些其他有用的示例,例如打印部分堆栈。该图书馆的作者戴夫·切尼(Dave Cheney)也写了许多有用的博客,并就此主题进行了演讲;你可以去https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully 了解更多。
当错误是最终结果时,通常应进行日志记录。换句话说,当发生异常或意外情况时,记录日志是很有用的。如果您使用提供日志级别的日志,在代码的关键部分散布 debug 或 info 语句,以便在开发过程中快速调试问题,那么这也可能是合适的。过多的日志记录会使您很难找到任何有用的内容,但如果日志记录不足,则会导致系统损坏,而无法深入了解根本原因。此配方将演示默认 Golog
包的使用和一些有用的选项,并展示可能出现日志的时间。
以下步骤包括应用的编写和运行:
- 从终端/控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter4/log
的新目录,并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/log
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/log
- 从
~/projects/go-programming-cookbook-original/chapter4/log
复制测试,或者将其用作编写自己代码的练习! - 创建一个名为
log.go
的文件,其内容如下:
package log
import (
"bytes"
"fmt"
"log"
)
// Log uses the setup logger
func Log() {
// we'll configure the logger to write
// to a bytes.Buffer
buf := bytes.Buffer{}
// second argument is the prefix last argument is about
// options you combine them with a logical or.
logger := log.New(&buf, "logger: ",
log.Lshortfile|log.Ldate)
logger.Println("test")
logger.SetPrefix("new logger: ")
logger.Printf("you can also add args(%v) and use Fatalln to
log and crash", true)
fmt.Println(buf.String())
}
- 创建一个名为
error.go
的文件,其内容如下:
package log
import "github.com/pkg/errors"
import "log"
// OriginalError returns the error original error
func OriginalError() error {
return errors.New("error occurred")
}
// PassThroughError calls OriginalError and
// forwards the error along after wrapping.
func PassThroughError() error {
err := OriginalError()
// no need to check error
// since this works with nil
return errors.Wrap(err, "in passthrougherror")
}
// FinalDestination deals with the error
// and doesn't forward it
func FinalDestination() {
err := PassThroughError()
if err != nil {
// we log because an unexpected error occurred!
log.Printf("an error occurred: %s\n", err.Error())
return
}
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
main.go
文件:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/log"
)
func main() {
fmt.Println("basic logging and modification of logger:")
log.Log()
fmt.Println("logging 'handled' errors:")
log.FinalDestination()
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
basic logging and modification of logger:
logger: 2017/02/05 log.go:19: test
new logger: 2017/02/05 log.go:23: you can also add args(true)
and use Fataln to log and crash
logging 'handled' errors:
2017/02/05 18:36:11 an error occurred: in passthrougherror:
error occurred
go.mod
文件得到更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
您可以使用log.NewLogger()
初始化记录器并传递它,或者使用log
包级记录器记录消息。此配方中的log.go
文件执行前者,error.go
执行后者。它还显示了错误到达最终目的地后日志记录何时有意义;否则,您可能会为一个事件记录多次。
这种方法存在一些问题。首先,您可能在其中一个中间函数中有额外的上下文,例如要记录的变量。其次,记录一堆变量可能会变得混乱,使其变得混乱和难以阅读。下一个方法探索结构化日志记录,它提供了日志记录变量的灵活性,在后面的方法中,我们还将探索实现全局包级日志记录程序。
记录信息的主要原因是在事件发生或过去发生时检查系统的状态。当您有大量正在记录的微服务时,基本日志消息很难梳理。
如果您可以将日志转换为他们理解的数据格式,那么可以使用各种第三方软件包对日志进行梳理。这些软件包提供索引功能、可搜索性等。sirupsen/logrus
和apex/log
包提供了一种进行结构化日志记录的方法,您可以在其中记录许多字段,这些字段可以重新格式化以适合这些第三方日志读取器。例如,以 JSON 格式发送日志以供各种服务解析非常简单。
以下步骤包括应用的编写和运行:
- 从终端/控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter4/structured
的新目录,并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/structured
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/structured
- 从
~/projects/go-programming-cookbook-original/chapter4/structured
复制测试,或者将其用作编写自己代码的练习! - 创建一个名为
logrus.go
的文件,其内容如下:
package structured
import "github.com/sirupsen/logrus"
// Hook will implement the logrus
// hook interface
type Hook struct {
id string
}
// Fire will trigger whenever you log
func (hook *Hook) Fire(entry *logrus.Entry) error {
entry.Data["id"] = hook.id
return nil
}
// Levels is what levels this hook will fire on
func (hook *Hook) Levels() []logrus.Level {
return logrus.AllLevels
}
// Logrus demonstrates some basic logrus functionality
func Logrus() {
// we're emitting in json format
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetLevel(logrus.InfoLevel)
logrus.AddHook(&Hook{"123"})
fields := logrus.Fields{}
fields["success"] = true
fields["complex_struct"] = struct {
Event string
When string
}{"Something happened", "Just now"}
x := logrus.WithFields(fields)
x.Warn("warning!")
x.Error("error!")
}
- 创建一个名为
apex.go
的文件,其内容如下:
package structured
import (
"errors"
"os"
"github.com/apex/log"
"github.com/apex/log/handlers/text"
)
// ThrowError throws an error that we'll trace
func ThrowError() error {
err := errors.New("a crazy failure")
log.WithField("id", "123").Trace("ThrowError").Stop(&err)
return err
}
// CustomHandler splits to two streams
type CustomHandler struct {
id string
handler log.Handler
}
// HandleLog adds a hook and does the emitting
func (h *CustomHandler) HandleLog(e *log.Entry) error {
e.WithField("id", h.id)
return h.handler.HandleLog(e)
}
// Apex has a number of useful tricks
func Apex() {
log.SetHandler(&CustomHandler{"123", text.New(os.Stdout)})
err := ThrowError()
//With error convenience function
log.WithError(err).Error("an error occurred")
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
main.go
文件:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/structured"
)
func main() {
fmt.Println("Logrus:")
structured.Logrus()
fmt.Println()
fmt.Println("Apex:")
structured.Apex()
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
Logrus:
WARN[0000] warning! complex_struct={Something happened Just now}
id=123 success=true
ERRO[0000] error! complex_struct={Something happened Just now}
id=123 success=true
Apex:
INFO[0000] ThrowError id=123
ERROR[0000] ThrowError duration=133ns error=a crazy failure
id=123
ERROR[0000] an error occurred error=a crazy failure
go.mod
文件应该更新,go.sum
文件现在应该出现在顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
sirupsen/logrus
和apex/log
包都是优秀的结构化记录器。两者都提供钩子,用于向多个事件发出消息或向日志条目添加额外字段。例如,使用logrus
钩子或apex
自定义处理程序将行号以及服务名称添加到所有日志中会相对简单。钩子的另一个用途可能包括traceID
,以便跨不同的服务跟踪请求。
当logrus
拆分钩子和格式化程序时,apex
将它们合并。除此之外,apex
还增加了一些方便的功能,如WithError
来添加error
字段以及跟踪,这两个功能都在本配方中演示。将挂钩从logrus
调整到apex
处理程序也相对简单。对于这两种解决方案,转换为 JSON 格式(而不是 ANSI 彩色文本)将是一个简单的更改。
此配方将演示在各种函数之间传递日志字段的方法。Gopkg/context
包是在函数之间传递附加变量和取消的极好方法。本食谱将探讨如何使用此功能在函数之间分配变量,以便记录日志。
此样式可根据上一配方的logrus
或apex
进行调整。我们将使用apex
来制作这个食谱。
以下步骤包括应用的编写和运行:
- 从终端/控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter4/context
的新目录,并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/context
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/context
- 从
~/projects/go-programming-cookbook-original/chapter4/context
复制测试,或者将其用作编写自己代码的练习! - 创建一个名为
log.go
的文件,其内容如下:
package context
import (
"context"
"github.com/apex/log"
)
type key int
// logFields is a key we use
// for our context logging
const logFields key = 0
func getFields(ctx context.Context) *log.Fields {
fields, ok := ctx.Value(logFields).(*log.Fields)
if !ok {
f := make(log.Fields)
fields = &f
}
return fields
}
// FromContext takes an entry and a context
// then returns an entry populated from the context object
func FromContext(ctx context.Context, l log.Interface)
(context.Context, *log.Entry) {
fields := getFields(ctx)
e := l.WithFields(fields)
ctx = context.WithValue(ctx, logFields, fields)
return ctx, e
}
// WithField adds a log field to the context
func WithField(ctx context.Context, key string, value
interface{}) context.Context {
return WithFields(ctx, log.Fields{key: value})
}
// WithFields adds many log fields to the context
func WithFields(ctx context.Context, fields log.Fielder)
context.Context {
f := getFields(ctx)
for key, val := range fields.Fields() {
(*f)[key] = val
}
ctx = context.WithValue(ctx, logFields, f)
return ctx
}
- 创建一个名为
collect.go
的文件,其内容如下:
package context
import (
"context"
"os"
"github.com/apex/log"
"github.com/apex/log/handlers/text"
)
// Initialize calls 3 functions to set up, then
// logs before terminating
func Initialize() {
// set basic log up
log.SetHandler(text.New(os.Stdout))
// initialize our context
ctx := context.Background()
// create a logger and link it to
// the context
ctx, e := FromContext(ctx, log.Log)
// set a field
ctx = WithField(ctx, "id", "123")
e.Info("starting")
gatherName(ctx)
e.Info("after gatherName")
gatherLocation(ctx)
e.Info("after gatherLocation")
}
func gatherName(ctx context.Context) {
ctx = WithField(ctx, "name", "Go Cookbook")
}
func gatherLocation(ctx context.Context) {
ctx = WithFields(ctx, log.Fields{"city": "Seattle",
"state": "WA"})
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
main.go
文件:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/context"
func main() {
context.Initialize()
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
INFO[0000] starting id=123
INFO[0000] after gatherName id=123 name=Go Cookbook
INFO[0000] after gatherLocation city=Seattle id=123 name=Go
Cookbook state=WA
go.mod
文件得到更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
context
包现在出现在各种包中,包括数据库和 HTTP 包。此方法允许您将日志字段附加到上下文,并将其用于日志记录目的。其思想是,单独的方法可以在传递上下文时将更多字段附加到上下文,然后最终的调用站点可以执行日志记录和聚合变量。
此配方模拟了前一配方中日志记录包中的WithField
和WithFields
方法。这些修改了存储在上下文中的单个值,还提供了使用上下文的其他好处:取消、超时和线程安全。
前面示例中的apex
和logrus
包都使用包级别的全局变量。有时,构建库以支持具有各种方法和顶级函数的两种结构是很有用的,这样您就可以直接使用它们,而无需传递它们。
此配方还演示了如何使用sync.Once
确保全局记录器只初始化一次。也可以通过Set
方法绕过。配方仅导出WithField
和Debug
,但您可以想象导出附加到log
对象的每个方法。
以下步骤包括应用的编写和运行:
- 从终端/控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter4/global
的新目录,并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/global
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/global
- 从
~/projects/go-programming-cookbook-original/chapter4/global
复制测试,或者将其用作编写自己代码的练习! - 创建一个名为
global.go
的文件,其内容如下:
package global
import (
"errors"
"os"
"sync"
"github.com/sirupsen/logrus"
)
// we make our global package level
// variable lower case
var (
log *logrus.Logger
initLog sync.Once
)
// Init sets up the logger initially
// if run multiple times, it returns
// an error
func Init() error {
err := errors.New("already initialized")
initLog.Do(func() {
err = nil
log = logrus.New()
log.Formatter = &logrus.JSONFormatter{}
log.Out = os.Stdout
log.Level = logrus.DebugLevel
})
return err
}
// SetLog sets the log
func SetLog(l *logrus.Logger) {
log = l
}
// WithField exports the logs withfield connected
// to our global log
func WithField(key string, value interface{}) *logrus.Entry {
return log.WithField(key, value)
}
// Debug exports the logs Debug connected
// to our global log
func Debug(args ...interface{}) {
log.Debug(args...)
}
- 创建一个名为
log.go
的文件,其内容如下:
package global
// UseLog demonstrates using our global
// log
func UseLog() error {
if err := Init(); err != nil {
return err
}
// if we were in another package these would be
// global.WithField and
// global.Debug
WithField("key", "value").Debug("hello")
Debug("test")
return nil
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
main.go
文件:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/global"
func main() {
if err := global.UseLog(); err != nil {
panic(err)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
{"key":"value","level":"debug","msg":"hello","time":"2017-02-
12T19:22:50-08:00"}
{"level":"debug","msg":"test","time":"2017-02-12T19:22:50-
08:00"}
go.mod
文件得到更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
这些global
包级对象的一种常见模式是保持global
变量未报告,并通过方法仅公开所需的功能。通常,您还可以包含一个方法,用于为需要logger
对象的包返回global
记录器的副本。
sync.Once
型是一种新引入的结构。此结构与Do
方法一起,只在代码中执行一次。我们在初始化代码中使用了这一点,如果多次调用Init
,则Init
函数将抛出错误。如果我们想将参数传递到global
日志中,我们使用自定义Init
函数而不是内置init()
函数
尽管本例使用了日志,但您也可以想象这样的情况:在数据库连接、数据流和许多其他用例中,日志可能很有用。
在实现长时间运行的流程时,某些代码路径可能会导致死机。这通常适用于未初始化的映射和指针,以及验证较差的用户输入情况下的零除问题。
在这些情况下,程序完全崩溃通常比恐慌本身严重得多,因此捕捉和处理恐慌是有帮助的。
以下步骤包括应用的编写和运行:
- 从终端/控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter4/panic
的新目录,并导航到此目录。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/panic
您应该看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter4/panic
- 从
~/projects/go-programming-cookbook-original/chapter4/panic
复制测试,或者将其用作编写自己代码的练习! - 创建一个名为
panic.go
的文件,其内容如下:
package panic
import (
"fmt"
"strconv"
)
// Panic panics with a divide by zero
func Panic() {
zero, err := strconv.ParseInt("0", 10, 64)
if err != nil {
panic(err)
}
a := 1 / zero
fmt.Println("we'll never get here", a)
}
// Catcher calls Panic
func Catcher() {
defer func() {
if r := recover(); r != nil {
fmt.Println("panic occurred:", r)
}
}()
Panic()
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
main.go
文件:
package main
import (
"fmt"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter4/panic"
)
func main() {
fmt.Println("before panic")
panic.Catcher()
fmt.Println("after panic")
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
before panic
panic occurred: runtime error: integer divide by zero
after panic
- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
这个食谱是如何抓住恐慌的一个非常基本的例子。您可以想象,使用更复杂的中间件,如何在运行许多嵌套函数后延迟恢复并捕获它。在 recover 中,您基本上可以做任何您想做的事情,尽管发出日志是很常见的。
在大多数 web 应用中,常见的情况是捕捉恐慌并在恐慌发生时发出http.InternalServerError
消息。