Skip to content

Latest commit

 

History

History
1062 lines (782 loc) · 34.3 KB

File metadata and controls

1062 lines (782 loc) · 34.3 KB

四、Go 中的错误处理

即使是最基本的 Go 程序,错误处理也很重要。Go 中的错误实现了Error接口,必须在代码的每一层进行处理。Go 错误不像异常那样工作,未经处理的错误可能会导致巨大的问题。无论何时发生错误,你都应该努力处理和考虑错误。

本章还介绍了日志记录,因为每当发生实际错误时都会记录日志。我们还将研究包装错误,以便给定的错误在返回函数堆栈时提供额外的上下文,以便更容易确定某些错误的实际原因。

本章将介绍以下配方:

  • 处理错误和错误接口
  • 使用 pkg/errors 包和包装错误
  • 使用日志包并了解何时记录错误
  • 带 apex 和 logrus 包的结构化日志记录
  • 使用上下文包进行日志记录
  • 使用包级全局变量
  • 捕捉长期运行进程的恐慌

技术要求

为了继续本章中的所有配方,请按照以下步骤配置您的环境:

  1. 在您的操作系统上下载并安装 Go 1.12.6 或更高版本 https://golang.org/doc/install

  2. 打开终端/控制台应用;创建并导航到项目目录,如~/projects/go-programming-cookbook。所有代码都将从此目录运行和修改。

  3. 将最新代码克隆到~/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 中有许多方法可以产生错误;本食谱将探索基本错误、具有赋值或类型的错误以及使用结构的自定义错误的创建。

怎么做。。。

以下步骤包括应用的编写和运行:

  1. 从终端/控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter4/basicerrors的新目录,并导航到此目录。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter4/basicerrors复制测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为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)

}
  1. 创建一个名为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
}
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的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)
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ 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
  1. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

无论您是使用errors.Newfmt.Errorf还是自定义错误,最重要的一点是,您永远不应该在代码中留下未经处理的错误。这些定义错误的不同方法提供了很大的灵活性。例如,您可以在结构中添加额外的函数以进一步查询错误,并将接口转换为调用函数中的错误类型以获得一些附加功能。

接口本身非常简单,唯一的要求是返回一个有效字符串。将其连接到一个结构对于一些高级应用可能很有用,这些应用在整个过程中都有一致的错误处理,但希望与其他应用很好地协作。

使用 pkg/errors 包和包装错误

位于github.com/pkg/errorserrors包装是标准 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包在整个代码中为错误添加注释。

怎么做。。。

以下步骤包括应用的编写和运行:

  1. 从终端/控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter4/errwrap的新目录,并导航到此目录。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter4/errwrap复制测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为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))

        }
  1. 创建一个名为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)
        }
  1. 创建一个名为example的新目录并导航到它。

  2. 创建一个包含以下内容的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()
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ 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
  1. go.mod文件应该更新,go.sum文件现在应该出现在顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行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包的使用和一些有用的选项,并展示可能出现日志的时间。

怎么做。。。

以下步骤包括应用的编写和运行:

  1. 从终端/控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter4/log的新目录,并导航到此目录。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter4/log复制测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为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())
        }
  1. 创建一个名为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
            }
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的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()
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ 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
  1. go.mod文件得到更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

您可以使用log.NewLogger()初始化记录器并传递它,或者使用log包级记录器记录消息。此配方中的log.go文件执行前者,error.go执行后者。它还显示了错误到达最终目的地后日志记录何时有意义;否则,您可能会为一个事件记录多次。

这种方法存在一些问题。首先,您可能在其中一个中间函数中有额外的上下文,例如要记录的变量。其次,记录一堆变量可能会变得混乱,使其变得混乱和难以阅读。下一个方法探索结构化日志记录,它提供了日志记录变量的灵活性,在后面的方法中,我们还将探索实现全局包级日志记录程序。

带 apex 和 logrus 包的结构化日志记录

记录信息的主要原因是在事件发生或过去发生时检查系统的状态。当您有大量正在记录的微服务时,基本日志消息很难梳理。

如果您可以将日志转换为他们理解的数据格式,那么可以使用各种第三方软件包对日志进行梳理。这些软件包提供索引功能、可搜索性等。sirupsen/logrusapex/log包提供了一种进行结构化日志记录的方法,您可以在其中记录许多字段,这些字段可以重新格式化以适合这些第三方日志读取器。例如,以 JSON 格式发送日志以供各种服务解析非常简单。

怎么做。。。

以下步骤包括应用的编写和运行:

  1. 从终端/控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter4/structured的新目录,并导航到此目录。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter4/structured复制测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为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!")
        }
  1. 创建一个名为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")
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的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()
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ 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
  1. go.mod文件应该更新,go.sum文件现在应该出现在顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

sirupsen/logrusapex/log包都是优秀的结构化记录器。两者都提供钩子,用于向多个事件发出消息或向日志条目添加额外字段。例如,使用logrus钩子或apex自定义处理程序将行号以及服务名称添加到所有日志中会相对简单。钩子的另一个用途可能包括traceID,以便跨不同的服务跟踪请求。

logrus拆分钩子和格式化程序时,apex将它们合并。除此之外,apex还增加了一些方便的功能,如WithError来添加error字段以及跟踪,这两个功能都在本配方中演示。将挂钩从logrus调整到apex处理程序也相对简单。对于这两种解决方案,转换为 JSON 格式(而不是 ANSI 彩色文本)将是一个简单的更改。

使用上下文包进行日志记录

此配方将演示在各种函数之间传递日志字段的方法。Gopkg/context包是在函数之间传递附加变量和取消的极好方法。本食谱将探讨如何使用此功能在函数之间分配变量,以便记录日志。

此样式可根据上一配方的logrusapex进行调整。我们将使用apex来制作这个食谱。

怎么做。。。

以下步骤包括应用的编写和运行:

  1. 从终端/控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter4/context的新目录,并导航到此目录。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter4/context复制测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为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
        }
  1. 创建一个名为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"})
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的main.go文件:
        package main

        import "github.com/PacktPublishing/
                Go-Programming-Cookbook-Second-Edition/
                chapter4/context"

        func main() {
            context.Initialize()
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ 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
  1. go.mod文件得到更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

context包现在出现在各种包中,包括数据库和 HTTP 包。此方法允许您将日志字段附加到上下文,并将其用于日志记录目的。其思想是,单独的方法可以在传递上下文时将更多字段附加到上下文,然后最终的调用站点可以执行日志记录和聚合变量。

此配方模拟了前一配方中日志记录包中的WithFieldWithFields方法。这些修改了存储在上下文中的单个值,还提供了使用上下文的其他好处:取消、超时和线程安全。

使用包级全局变量

前面示例中的apexlogrus包都使用包级别的全局变量。有时,构建库以支持具有各种方法和顶级函数的两种结构是很有用的,这样您就可以直接使用它们,而无需传递它们。

此配方还演示了如何使用sync.Once确保全局记录器只初始化一次。也可以通过Set方法绕过。配方仅导出WithFieldDebug,但您可以想象导出附加到log对象的每个方法。

怎么做。。。

以下步骤包括应用的编写和运行:

  1. 从终端/控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter4/global的新目录,并导航到此目录。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter4/global复制测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为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...)
        }
  1. 创建一个名为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
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的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)
            }
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ 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"}
  1. go.mod文件得到更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

这些global包级对象的一种常见模式是保持global变量未报告,并通过方法仅公开所需的功能。通常,您还可以包含一个方法,用于为需要logger对象的包返回global记录器的副本。

sync.Once型是一种新引入的结构。此结构与Do方法一起,只在代码中执行一次。我们在初始化代码中使用了这一点,如果多次调用Init,则Init函数将抛出错误。如果我们想将参数传递到global日志中,我们使用自定义Init函数而不是内置init()函数

尽管本例使用了日志,但您也可以想象这样的情况:在数据库连接、数据流和许多其他用例中,日志可能很有用。

捕捉长期运行进程的恐慌

在实现长时间运行的流程时,某些代码路径可能会导致死机。这通常适用于未初始化的映射和指针,以及验证较差的用户输入情况下的零除问题。

在这些情况下,程序完全崩溃通常比恐慌本身严重得多,因此捕捉和处理恐慌是有帮助的。

怎么做。。。

以下步骤包括应用的编写和运行:

  1. 从终端/控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter4/panic的新目录,并导航到此目录。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter4/panic复制测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为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()
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的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")
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您应该看到以下输出:

$ go run main.go
before panic
panic occurred: runtime error: integer divide by zero
after panic
  1. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

这个食谱是如何抓住恐慌的一个非常基本的例子。您可以想象,使用更复杂的中间件,如何在运行许多嵌套函数后延迟恢复并捕获它。在 recover 中,您基本上可以做任何您想做的事情,尽管发出日志是很常见的。

在大多数 web 应用中,常见的情况是捕捉恐慌并在恐慌发生时发出http.InternalServerError消息。