golang 错误返回

golang错误处理机制

error

go语言中没有传统的异常处理机制,而是通过返回错误值来处理错误

常见的错误处理方式有:

  • 返回错误值:函数在执行过程中如果遇到错误,会返回一个非nil的错误对象。调用者需要检测返回的错误值,并进行相应的处理。

  • 使用 deferpanicrecover: defer 用于延迟执行函数,通常用于资源释放等操作。panic 用于触发一个运行时错误,使程序进入恐慌状态。recover 用于从恐慌状态中恢复,通常在 defer 函数中使用。

package main

import "fmt"

func riskyFunction() {
	// 模拟错误
	panic("Something went wrong!")
}

func handleError() {
	// 使用 defer,但没有 recover
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered from panic:", r)
		}//如果将recover去掉,程序就会非正常退出
	}()
	// 触发 panic
	riskyFunction()
}

func main() {
	handleError()

	// 这一行代码不会执行,因为 panic 使程序崩溃
	fmt.Println("Program continues after panic (this won't be printed)")
}

如果微服务架构的程序 其中的go程序出现panic会怎样?整个微服务会挂掉吗

当一个Goroutine发生panic时,通常会触发所谓的defer机制,通过defer可以注册一些函数,这些函数会在当前函数返回时执行,这样可以用来处理一些清理工具或者捕获panic。另外,go语言的recover函数也可以用来在发生panic时恢复程序的运行,这样可以使得程序在某些异常情况下不会完全挂掉

一边来说是不会的 可以熔断和降级

错误处理

大道至简

大道至简就是对于整个过程开发者知道自己在干啥,不要企图依赖错误处理来兜底自己的开发错误。

当然并不能说go的错误处理方式更优,但对于业务处理而已至少比try catch更方便,毕竟相当于编译器开发者帮我们做了很多处理工作,大多数时候我们只想梭哈一把,只要代码写得快,bug就追不上我们,这也是为什么go用于业务开发常被抱怨的原因。

最佳事件

事实上,关于错误处理,go官方也一直在迭代,也给出了一些最佳实践。总的原则就是,go的错误应该被当做值,既然是值,那么错误处理的方式完全取决于开发者,推荐一些最佳实践的套路,躺平得更理直气壮。

常见处理方式

常用处理方式足以赢多大多数简单开发场景,比如应该简单的cli工具等等。

简单处理

函数中常用errors.New和fmt.Error,上层 if err!= nil 判断是否发生错误,对于简单的封装调用,这种方式即可。

func funcA() error {
    // do something
    return errors.New("funcA error")
    // return fmt.Errorf("funcB error %d", 1)
}

func TestSimple(t *testing.T) {
    err := funcA()
    if err != nil {
        t.Logf("err %v", err)
        return
    }
}

标准错误匹配判断

类似 try catch,提前定义好不同的标准Error,统一调用可能返回不同的错误,上游调用针对不同类型不同处理 简单的可直接判断类型

// 分支判断
var (
    ErrA = errors.New("A error")
)

func funcA2() error {
    // do something
    if true {
        return ErrA
    }

    // do something
    return nil
}

func TestSimple2(t *testing.T) {
    err := funcA2()
    if err == ErrA {
        t.Logf("err %v", err)
        return
    }
}

处理多类型分钟用switch case 判断下

func main(){
	err := test()
	if err != nil{
		swith err{
		case t1:
			//...
			return
		case t2:
			//...
		default:
			//...
			return
		}
	}
}

自定义

const(
	Success = 200
	UserNotExist = 1001
	UserExist=1002
	UserCreateFailed=1003
	PassWordError=1004
)

var CodeMessage = map[int]string{
	Success:"success",
	UserNotExist:"user not exist",
	UserExist:"user exist",
	UserCreateFailed:"user create failed",
	PassWordError:"password error",
}

func GetMsg(code int) string {
	return CodeMessage[code]
}

error定义的包依赖

无论是标准的error的列表,还是自定义的define声明,检测错误导致了两个包之间产生代码级的依赖。比如,检查某个错误是否是io.EOF(预定义error),不得不依赖io包,检查某个错误是否是自定义err1,不得不依赖err1的定义。

事实上最理想的情况是:代码实现上最好可以不用import定义该错误的包,从而导致的耦合,毕竟调用方只关心错误的行为,并不关心底层实现细节,这样就算所谓的透明类型错误。

透明类型错误检测

这里参考Dave Cheney的方法,要想上层不依赖下层错误定义,最简单的就是上层只判断是否出错,压根不关心具体信息。如下

func fn() error {
        x, err := bar.Foo()
        if err != nil {
                return err
        }
        // use x
}

为了支持不同错误的具体信息判断怎么做呢?Golang的接口是鸭子类型,只需要通过预定义接口,在上层调用中判断是否实现了某个接口而即可。看起来能解决问题,但是实际中增加使用复杂度,很少用。

type temporary interface {
        Temporary() bool
}

// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
        te, ok := err.(temporary)
        return ok && te.Temporary()
}

独立定义和声明

实际上,实际业务中前一种方式用得很少。 目前最广泛使用的,特别是微服务中,就是单独定义一个错误包,项目组统一维护,其它相关使用方引入该包使用即可。

业务上下文输出

实际上,实际业务中前一种方式用得很少。 目前最广泛使用的,特别是微服务中,就是单独定义一个错误包,项目组统一维护,其它相关使用方引入该包使用即可。

  • 定义业务相关信息,这里简单定义错误码和消息,一般单独模块维护

const (
    ErrSuccess   = 0
    ErrInvalid   = 1
    ErrWrongUser = 2
)

var code2msg = map[int]string{
    ErrSuccess:   "Success",
    ErrInvalid:   "Invalid Param",
    ErrWrongUser: "Wrong User",
}

// 独立的自定义error
type MyError struct {
    mod  string
    code int
    msg  string
}
  • 然后自定义MyError,自定义业务信息输出和记录,可以在此基础上增加信息和方法实现
// 独立的自定义error
type MyError struct {
    mod  string
    code int
    msg  string
}

func NewMyError(mod string, code int) *MyError {
    return &MyError{
        mod,
        code,
        code2msg[code],
    }
}

func (e *MyError) Error() string {
    return fmt.Sprintf("Error detail: %+v %+v %+v", e.mod, e.code, e.msg)
}

func (e *MyError) Code() int {
    return e.code
}

func (e *MyError) Msg() string {
    return e.msg
}

func doSomethingA() error {
    return NewMyError("A", ErrSuccess)
}

func doSomethingB() error {
    return NewMyError("B", ErrInvalid)
}
  • 最后实际使用中记录和输出业务相关
func TestDoSomething(t *testing.T) {
    err := doSomethingA()

    if err != nil {
        // 分错误处理
        switch err.(type) {
        case *MyError:

            // 分业务处理
            myerr := err.(*MyError)
            switch myerr.Code() {
            case ErrSuccess:
                // ...
                t.Logf("MyError - %v", err)
            case ErrInvalid:
                // ...
            case ErrWrongUser:
                // ...
            }

        default:
            t.Log("Other error")
        }
    }
}

跟踪错误堆栈

在Go应用里,一个逻辑往往要经多多层函数的调用才能完成,那在程序里我们的建议Error Handling 尽量留给上层的调用函数做,中间和底层的函数通过错误包装把自己要记的错误信息附加再原始错误上再返回给外层函数。

期望功能:

  • 错误包装(Wrap)和解包装(UnWarp),返回的是一个error堆栈(Stack)
  • 可以打印error堆栈(Stack)
  • 被包装的error无法直接=或type判断是否为具体错误类型,需要支持error堆栈中查找判断

重复的错误日志输出

很多开发者实际开发过程中,函数里遇到error,可能会先打印error,同时把error也返回给上层调用方,结果日志出现大量重复error,影响排查不说,还可能降低性能。

这里提供两种常见的最佳实践思路

  • 利用错误堆栈跟踪错误,上层统一处理和打印

  • 最原始位置调用日志包记录函数,打印错误信息,其他位置直接返回

事实上,目前更多看到的是这种方式。实际工程中,很多开发者并不习惯把错误打印全部放在一个地方,甚至很多时候只是按需打印error日志,这时候结合Wrap功能折中处理会更好。

一般来说当错误发生时,也借助 log 包定位到错误发生的位置。最好如下操作

  • 只在错误产生的最初位置打印日志,其他地方直接返回错误,一般不需要再对错误进行封装。
  • 当代码调用第三方包的函数时,第三方包函数出错时打印错误信息。
posted @ 2025-06-28 22:14  夏尾草  阅读(18)  评论(0)    收藏  举报