Kratos单体应用实战三


前言

在使用kratos框架进行开发时往往会遇到一些让人纠结的问题,比如命名规范?service层,biz层,data层业务逻辑所占的比重?多个http服务如何引入?文件上传如何接入到框架中?下面我们一个个来实践。


一、规范问题

关于命名规范一直是开发中让人纠结的问题,但是只要团队之间约定好,并严格按照约定来(有时候很容易出错,所以在时间允许的情况下最好对代码进行Review),这样就减少开发过程中纠结命名规范的问题。

1 包的命名规范

包的命名go语言建议使用简洁短小并且全部小写,但是业务开发中一般很难做到,可以使用下划线命名或小驼峰命名。
而文件的命名方式就采用下划线命名即可。

2 变量的命名规范

一般采用驼峰命名法,根据访问情况控制首字母大写或小写。
其中bool类型,一般以Has, Is, Can, Allow开头

3 常量命名

使用全部大写,单词之间使用下划线区分。
或者使用大驼峰也是可以的,看个人习惯。
若为枚举类型的常量,则需要先创建相应类型。如下

type UserType string

const (
	UserTypeAdmin   UserType = "admin"
	UserTypeTeacher UserType = "teacher"
	UserTypeStudent UserType = "student"
)

4 错误处理

原则上全部的err都需要处理,不能选择丢弃,并且用log记录下来。
某些错误的出现使得程序执行不了,则使用painc处理,例如程序启动的时候读取配置文件等严重的错误。其他情况不使用painc。
err的定义一般以Err开头,例如ErrUserNotFound。

5 其他

  • 切片或字典初始化时,建议设置容量。
  • 不能在循环中使用defer
  • 协程链之间要使用context控制,及时退出
  • 方法中的每小段逻辑处理之间用空格分开,增加可读性
  • 持续补充。。。。

二 、service, biz, data权重问题

在开发中,由于拆分了层次,造成业务逻辑重点放在哪个地方,估计是每个程序员在使用kratos过程中经常纠结的问题。下面说下我个人使用过程中权衡的做法。

业务开发中核心的业务逻辑主要放在biz层,数据访问与操作的轻业务逻辑放在data层,而service层仅仅是为了转换为api接口文档定义的结构体的适配作用。整体权重大约是:

  • services层占比 10%
  • biz层占比 60%
  • data层占比 30%

为什么会这么权衡呢?

  1. 我们是单体服务,基本没有什么数据需要通过data层访问另外一个服务获取数据,然后整合。
  2. biz层拥有自己的实体,调用repo也是通过接口的形式,data层仅仅是实现了biz层定义的接口,由此biz层非常独立干净,很方便的去做单元测试。
  3. 当我们切换数据库的时候,比如从mysql换成mongodb时候,核心逻辑基本不需要动,只需要重写下data层,非常方便。

相关示例可以查看 kratos-use

三、事务问题

当我们把核心逻辑放在biz层时,事务的处理就变得很麻烦,因为数据库对象是放在data层的,而开启事务需要有事务上下文对象tx才行。所以我们需要加点东西,实现依赖倒置。

  1. 在biz层定义一个事务接口
// Transaction 新增事务接口方法
type Transaction interface {
	ExecTx(ctx context.Context, fn func(ctx context.Context) error) error
}
  1. 让data层的Data对象实现这个接口,因为不同的数据库开启事务的形式不一样,我用的mysql,为了以后的扩展,在ent/schema/dbo/mysql 实现了ExecTx接口,而Data对象就可以继承BaseDBData
type contextTxKey struct{}

type BaseDBData struct {
	db *ent.Client
}

func NewBaseDBData(db *ent.Client) *BaseDBData {
	return &BaseDBData{
		db: db,
	}
}

// 事务接口实现
func (b *BaseDBData) ExecTx(ctx context.Context, fn func(ctx context.Context) error) error {
	log.Info("begin transaction")

	var (
		err error
		tx  *ent.Tx
	)
	tx, err = b.db.Tx(ctx)
	if err != nil {
		return err
	}

	ctx = context.WithValue(ctx, contextTxKey{}, tx)

	defer func() {
		if v := recover(); v != nil {
			log.Warnw("msg", "transaction panic", "recover error", err)

			err1 := tx.Rollback().Error
			if err1 != nil {
				log.Warnw("msg", "rollback transaction failed", "err", err, "err1", err1)
			} else {
				log.Info("rollback transaction successfully")
			}
		}
	}()

	if err := fn(ctx); err != nil {
		log.Errorw("msg", "func execute failed", "err", err)

		if rerr := tx.Rollback(); rerr != nil {
			log.Errorw("msg", "rolling back transaction error", "err", err)
		} else {
			log.Info("rolling back transaction successfully")
		}

		return err
	}

	if err := tx.Commit(); err != nil {
		return fmt.Errorf("committing transaction: %w", err)
	}

	log.Info("commit transaction successfully")

	return nil
}
  1. 为了保证在事务上下文用一个tx,为BaseDBData新增一个方法
func (b *BaseDBData) DB(ctx context.Context) *ent.Client {
	tx, ok := ctx.Value(contextTxKey{}).(*ent.Tx)
	if ok {
		return tx.Client()
	}

	return b.db
}

  1. data层中加入如下代码,并注册到wire.NewSet中
func NewTransaction(d *Data) biz.Transaction {
	return d
}
  1. biz层中接收tx
// DiaryUsecase is a Diary usecase.
type DiaryUsecase struct {
	repo DiaryRepo
	log  *log.Helper
	tx   Transaction
}
  1. 使用方式如下,用起来也非常丝滑
err = uc.tx.ExecTx(ctx, func(ctx context.Context) error {
	// 更新订单数据
	err := uc.userRepo.UpdateOrder....
	
	// 更新用户积分点
	err := uc.userRepo.UpdateUserPoint....

	return nil
})
if err != nil {
	uc.log.WithContext(ctx).Errorw("msg", "事务执行失败")
	return err
}

这地方需要自己看kratos-use,细细品味。当然也可以按照更好的思路进行重构。

三、文件上传

proto接口文件的形式不支持文件上传和下载的代码生成工作,所以需要自己实现,如下

func RegisterFileServiceHTTPServer(srv *kratoshttp.Server, service *service.FileService) {
	// 文件上传相关的接口
	route := srv.Route("/")
	route.POST("/api/v1/common/resource/upload", _UploadResourceFileHandler(service))
}
func _UploadResourceFileHandler(fileService *service.FileService) func(ctx kratoshttp.Context) error {
	return func(ctx kratoshttp.Context) error {
		req := ctx.Request()
		// 获取resourceFile的文件流
		resourceFile, resourceHeader, err := req.FormFile("file")
		if err != nil {
			if errors.Is(err, http.ErrMissingFile) {
				return httpx.ErrBadRequestWithMsg("resourceFile不能为空")
			}
			return err
		}
		defer resourceFile.Close()
		// 文件大小校验
		if resourceHeader.Size > 5*1024*1024 || resourceHeader.Size <= 0 {			
			return httpx.ErrBadRequestWithMsg("上传文件的大小超过了限定值")
		}

		h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
			return fileService.UploadResourceFile(ctx, req)
		})
		
		out, err := h(ctx, &in)
		if err != nil {
			return err
		}
		return ctx.Result(200, out)
	}
}

上面是文件上传的其中一种方式,由后端完成文件的处理,保存到本地服务器或者上传到存储桶。如果是上传到存储桶中,可以采用前端上传文件的方式,可以减少服务端的资源和压力。这部分可以看第三方存储桶提供的文档。大致流程就是后端负责提供一个临时的文件上传的签名地址,前端直传到存储桶即可。

总结

本篇文章主要将一些规范问题,代码权重问题,以及事务和文件的处理方式,仅供参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值