kevinyan815/gocookbook

Go代码更优雅地错误处理

Opened this issue · 0 comments

Go 优雅处理错误的几种方案

在 Go 语言官方库 bufioScanner对象的错处处理的实现方式可以给我们一点启发,它大概是这么实现的。

scanner := bufio.NewScanner(input)

for scanner.Scan() {
    token := scanner.Text()
    // process token
}

if err := scanner.Err(); err != nil {
    // process the error
}

上面的代码我们可以看到,scanner在操作底层的I/O的时候,那个for-loop中没有任何的 if err !=nil 的情况,退出循环后有一个 scanner.Err() 的检查。看来使用了结构体的方式。

我们来看一下 Scanner类型的定义

type Scanner struct {
	r            io.Reader
  ...//其他字段省略
	err          error    
}

这个类型内部持有一个error 在迭代执行 Scan 方法时,遇到错误后会往这个 error 中记录错误。

func (s *Scanner) Scan() bool {
  ...// 其余代码省略
	for {
			if err != nil {
				s.setErr(err)
				return false
			}
}
  
func (s *Scanner) Err() error {
	if s.err == io.EOF {
		return nil
	}
	return s.err
}

所以我们可以参考这个思路继续搞下去。比如来一个读取业务对象的

package main

import (
	"fmt"
	"log"
)

type Person struct {
	name string
	age int
	err error
}

func (p *Person) ReadName() string {
	if p.err != nil {
		return ""
	}
	return p.name
}
func (p *Person) ReadAge() int {
	//p.err = errors.New("故意的搞个错误")
	if p.err != nil {
		return 0
	}
	return p.age
}

func (p *Person) Err() error {
	return p.err
}

func main() {
	p := Person{"小kk", 20, nil}
	name := p.ReadName()
	age :=p.ReadAge()

	if err := p.Err(); err != nil {
		log.Println(err)
		return
	}

	fmt.Printf("name:%s , age:%d", name, age)
}

上面这个示例相信大家很容易看懂,不过,其使用场景也就只能在对于同一个业务对象的不断操作下可以简化错误处理,对于多个业务对象的话,还是得需要各种 if err != nil的方式。

更容易落地的方案

通过应用服务可以协调多个业务对象执行任务,同时我们上面业务对象加的那些错误处理抽离到应用服务层里,让业务对象更专注自己的职责。这样的话,你的服务层代码,可能就得变成了这样

package applicationservice

import "errors"

type TenantService struct {
	err error
}

func GetTenantService() *TenantService {
	return new(TenantService)
}

func (t *TenantService)Err() error {
	return t.err
}

type Tenant struct {
	TenantId string
}

func (t *TenantService) ActiveTenant(userId int64) *Tenant {
	if t.err != nil {
		return nil
	}

	// 这里调用Tenant业务对象,给用户开启租户


	// TODO: 服务层错误不用处理,抛给外层,但是要记下关键信息
	//log.Error("这里记下关键信息,比如入参什么的," +
	//	"不用记错误,错误可以抛给上层处理")

	return &Tenant{TenantId: "1"}
}

func (t *TenantService) InitBilling(tenant *Tenant) bool {
	if t.err != nil {
		return false
	}
	// 这里调用Billing业务对象给租户开启
	// 账单中心等等需要在开租户时就要有的功能模块

	// 注意 Service里记日志只
	t.err = errors.New("错误")

	return true
}

然后我们的控制层呢,调用应用服务层拿到结果,并且在这个时候判断整个需求任务执行的过程中有没有错误,有的话记录错误,返回错误响应给客户端。

// 假设这是一个路由绑定的控制器方法
func ActivateTenantForUser()  {
	tenantService := GetTenantService()
	tenant := tenantService.ActiveTenant(1)
	tenantService.InitBilling(tenant)

	if err := tenantService.Err();err != nil {
		log.Error()
    // 返回错误响应给客户端
	}
  
  // 返回正常响应给客户端
}