Golang中如何简化一系列连续的错误检查

Golang中如何简化一系列连续的错误检查 你好,

我是Go语言的新手,目前正在尝试用Go编写一个小型Web应用的后端API。我有一些代码如下所示。有没有办法简化它,例如,像Java那样使用try-catch来覆盖所有的错误检查?

以下是我的代码块:

func GetContextUser(c *gin.Context) (*dto.UserInfoDto, error) {
	token, err := uses.GetRequestToken(c.Request)
	if err != nil {
		return nil, err
	}
	user, err := dao.GetUserByToken(token)
	if err != nil {
		return nil, err
	}
	addresss, err := dao.GetUserAddress(user.Id)
	if err != nil {
		return nil, err
	}
	estates, err := dao.GetAllEstates()
	if err != nil {
		return nil, err
	}
	var res = dto.UserInfoDto{
		Info:      *user,
		Addresses: addresss,
		Config: dto.Config{
			Estates: estates,
		},
	}
	return &res, nil
}

更多关于Golang中如何简化一系列连续的错误检查的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

Go(谢天谢地)没有异常这个概念。 您展示的代码在 Go 中是地道的写法。

目前有一些讨论,希望创建一种简化的语法来减少代码的冗余,但目前这就是标准做法。

提示:您可以通过“包装”现有错误来丰富返回的错误信息(例如使用 fmt.Errorf())。

func main() {
    fmt.Println("hello world")
}

更多关于Golang中如何简化一系列连续的错误检查的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


err 

实际上,这是两个问题。如果Go返回一个错误,这意味着你调用的方法中的异常已经被提前捕获,这里并没有发生异常,就像在Java中,当try catch捕获到异常并返回null一样。如果你想以类似try catch的方式包含所有方法,那么你调用的方法就不应该捕获异常,或者不应该返回错误,而是应该直接panic()抛出异常,这取决于Func的设计。

作为开发者,你有责任思考代码中可能出错的一切情况,并决定采取正确的应对措施。

Go语言通过要求你单独处理每个错误,使这一点变得非常容易。虽然对于一个小型示例项目来说,这可能看起来有些繁琐,但对于其他任何情况来说,这绝对是必要的。

当你只是将错误向上抛出调用链时,用户会收到一个通用的错误信息,例如“错误:值不能为空”——没有任何指示说明该怎么做,也没有任何关于错误具体发生位置的上下文信息。

典型错误原因

根据我的经验,大多数错误都属于以下三类之一,具体取决于需要采取什么措施来修复它:

  1. 用户/输入错误:用户尝试了某些操作,除非他做出某些更改,否则这些操作无法成功。他们需要采取特定的行动(例如,登录、更改查询、选择其他产品,因为此产品缺货……)——在这种情况下,你需要向用户提供明确的信息,说明他们需要做什么才能继续(最好附带链接)。

  2. 临时条件错误:用户只需要重试,很可能就会成功(例如,网络请求失败、由于服务器繁忙导致操作超时、乐观锁异常……)——如果有可能成功,你的代码理想情况下应该立即重试。或者向用户提供明确的信息——例如“请几分钟后再试”。

  3. 永久性/代码错误:某些东西损坏了,只有技术人员修复后才能再次正常工作(数据库损坏或代码中存在错误)——在这种情况下,用户需要联系支持人员(或者监控服务会自动提醒支持人员)——这些错误需要记录详细信息,以便轻松地重现、分析和修复错误。

实用错误处理示例

token, err := uses.GetRequestToken(c.Request)
	if err != nil || token == nil {
        // 没有令牌?用户未登录
		return nil, ErrNoLoginToken // 重定向到登录页面
	}
	user, err := dao.GetUserByToken(token)
	if err != nil {
        // 找不到该令牌对应的用户:会话已过期或存在黑客攻击尝试
		return nil, ErrSessionInvalid // 重定向到带有错误信息的登录页面
	}
	addresss, err := dao.GetUserAddress(user.Id)
    if errors.Is(err, sql.ErrNotFound) {
        // 显示链接到用户可以向其账户添加地址的页面
        return nil, ErrNoAddress
    } else if err != nil {
        // 通用数据库错误,为错误排查提供上下文
		return nil, fmt.Errorf("could not load address for user %+v: %w", user, err)
	}
	estates, err := dao.GetAllEstates()
	if err != nil {
        // 通用数据库错误,为错误排查提供上下文
		return nil, fmt.Errorf("could not load estates for user: %+v %w", user, err)
	}

在Go语言中,可以通过几种方式简化连续的错误检查。以下是几种实用的方法:

1. 使用错误包装器(推荐)

创建一个辅助函数来封装错误检查逻辑:

func checkErr[T any](value T, err error) (T, error) {
    if err != nil {
        return value, err
    }
    return value, nil
}

func GetContextUser(c *gin.Context) (*dto.UserInfoDto, error) {
    token, err := checkErr(uses.GetRequestToken(c.Request))
    if err != nil {
        return nil, err
    }
    
    user, err := checkErr(dao.GetUserByToken(token))
    if err != nil {
        return nil, err
    }
    
    addresses, err := checkErr(dao.GetUserAddress(user.Id))
    if err != nil {
        return nil, err
    }
    
    estates, err := checkErr(dao.GetAllEstates())
    if err != nil {
        return nil, err
    }
    
    return &dto.UserInfoDto{
        Info:      *user,
        Addresses: addresses,
        Config: dto.Config{
            Estates: estates,
        },
    }, nil
}

2. 使用闭包和延迟错误处理

func GetContextUser(c *gin.Context) (res *dto.UserInfoDto, err error) {
    var token string
    var user *model.User
    var addresses []model.Address
    var estates []model.Estate
    
    defer func() {
        if err != nil {
            // 可以在这里添加日志记录或其他清理操作
        }
    }()
    
    token, err = uses.GetRequestToken(c.Request)
    if err != nil {
        return
    }
    
    user, err = dao.GetUserByToken(token)
    if err != nil {
        return
    }
    
    addresses, err = dao.GetUserAddress(user.Id)
    if err != nil {
        return
    }
    
    estates, err = dao.GetAllEstates()
    if err != nil {
        return
    }
    
    res = &dto.UserInfoDto{
        Info:      *user,
        Addresses: addresses,
        Config: dto.Config{
            Estates: estates,
        },
    }
    return
}

3. 使用自定义错误类型和链式调用

type Result struct {
    err error
}

func (r *Result) Check(fn func() error) *Result {
    if r.err == nil {
        r.err = fn()
    }
    return r
}

func (r *Result) Error() error {
    return r.err
}

func GetContextUser(c *gin.Context) (*dto.UserInfoDto, error) {
    var token string
    var user *model.User
    var addresses []model.Address
    var estates []model.Estate
    
    result := &Result{}
    
    result.Check(func() error {
        var err error
        token, err = uses.GetRequestToken(c.Request)
        return err
    }).Check(func() error {
        var err error
        user, err = dao.GetUserByToken(token)
        return err
    }).Check(func() error {
        var err error
        addresses, err = dao.GetUserAddress(user.Id)
        return err
    }).Check(func() error {
        var err error
        estates, err = dao.GetAllEstates()
        return err
    })
    
    if err := result.Error(); err != nil {
        return nil, err
    }
    
    return &dto.UserInfoDto{
        Info:      *user,
        Addresses: addresses,
        Config: dto.Config{
            Estates: estates,
        },
    }, nil
}

4. 使用第三方库

如果项目允许使用第三方库,可以考虑使用类似 github.com/pkg/errorsgolang.org/x/sync/errgroup

import "golang.org/x/sync/errgroup"

func GetContextUser(c *gin.Context) (*dto.UserInfoDto, error) {
    var (
        token      string
        user       *model.User
        addresses  []model.Address
        estates    []model.Estate
    )
    
    g := new(errgroup.Group)
    
    g.Go(func() error {
        var err error
        token, err = uses.GetRequestToken(c.Request)
        return err
    })
    
    g.Go(func() error {
        var err error
        user, err = dao.GetUserByToken(token)
        return err
    })
    
    g.Go(func() error {
        var err error
        addresses, err = dao.GetUserAddress(user.Id)
        return err
    })
    
    g.Go(func() error {
        var err error
        estates, err = dao.GetAllEstates()
        return err
    })
    
    if err := g.Wait(); err != nil {
        return nil, err
    }
    
    return &dto.UserInfoDto{
        Info:      *user,
        Addresses: addresses,
        Config: dto.Config{
            Estates: estates,
        },
    }, nil
}

第一种方法(错误包装器)是最简洁且符合Go语言习惯的方式,它保持了代码的可读性同时减少了重复的错误检查代码。

回到顶部