您现在的位置是:首页 >学无止境 >【Go语言Web开发】Todolist 项目重构网站首页学无止境

【Go语言Web开发】Todolist 项目重构

小生凡一 2023-07-08 00:00:03
简介【Go语言Web开发】Todolist 项目重构

写在前面

这篇文章我们来重构一下之前写的Todolist项目,包括项目结构,代码逻辑
项目地址:https://github.com/CocaineCong/TodoList

1. 项目结构的改变

1.1 改变前的项目架构

TodoList/
├── api
├── cache
├── conf
├── middleware
├── model
├── pkg
│  ├── e
│  └── util
├── routes
├── serializer
└── service

这个项目结构看起来没啥问题,但实际使用的过程中问题很大!

  1. cache 和 model 都是有关于存储类的,但是没有统一一个文件归纳这些,现在项目小,只有mysql,redis,但是如果加上es,mongodb等待其他存储类的,那么显得非常臃肿了。所以我们这里应该用一个文件统一放置这些东西
  2. serializer非常python,没有go味。如果接触过python的django,大家应该知道dj中有一个序列化的功能,就是这个serializer,作用功能是给整合数据返回给前端的。但是在go中,不需要也没必要用这个serializer,而是用一个types,统一管理定义的结构体,包括 reqresp 等待所需要的结构体。

1.2 改变后的项目架构

  • 针对于上面的第一种情况,我们可以用一个 repository 来存储,包括 redis,mysql 等等。与之相关可以加到 repository 文件中,包括 es,mongodb 等等…
  • 对于第二个 serializer 就是将这个新建一个types,用来统一管理定义的结构体就行了,不过注意一点,就是types的resp结构体和db层的联动。

注意一点:types,consts包都是只能单方面引入,也就是这两个包只能被引用,不能引用其他包的代码,否则容易变成循环导包的情况

TodoList/
├── api
├── cmd
├── conf
├── consts
├── docs
├── middleware
├── pkg
│  ├── e
│  └── util
├── routes
├── repository
│  ├── cache
│  └── db
│     ├── dao
│     └── model
├── routes
├── service
└── types

2. 代码逻辑的改变

这部分我们以Task模块举例子

2.1 controller层

2.1.1 改造前

先看代码:

func CreateTask(c *gin.Context) {
	createService := service.CreateTaskService{}
	chaim, _ := util.ParseToken(c.GetHeader("Authorization"))
	if err := c.ShouldBind(&createService); err == nil {
		res := createService.Create(chaim.Id)
		c.JSON(200, res)
	} else {
		c.JSON(400, ErrorResponse(err))
		util.LogrusObj.Info(err)
	}
}

这个代码咋一看,就很多问题。

  1. 第三行的解析token,不应该放在这里进行解析,而是放在中间件中统一解析,然后存到context中,进行上下游的流动存储
  2. 第五行的Create函数中,没有穿进去context,这是很危险的行为,这意味着这条链路没法跟踪了
  3. 第五行的返回值,像这种函数方法,应该需要返回error值,这是go语言的特色( 也不知道go为啥这种设计,一堆if err != nil )
  4. 第六,第八行中的200,400,应该用常量代替,http包中的常量

2.1.2 改造后

修改之后的controller层代码如下所示:

func CreateTaskHandler() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		var req types.CreateTaskReq
		if err := ctx.ShouldBind(&req); err == nil {
			// 参数校验
			l := service.GetTaskSrv()
			resp, err := l.CreateTask(ctx.Request.Context(), &req)
			if err != nil {
				ctx.JSON(http.StatusInternalServerError, ErrorResponse(err))
				return
			}
			ctx.JSON(http.StatusOK, resp)
		} else {
			util.LogrusObj.Infoln(err)
			ctx.JSON(http.StatusBadRequest, ErrorResponse(err))
		}
	}
}
  • 解析token的操作放在middleware层,通过context进行上下游传递
  • 第七行,传进去了context,也保证了返回值中有err的返回进行处理
  • 原来的 200,400 也用http包中的常量代替

2.2 service层

2.2.1 改造前

// 创建任务的服务
type CreateTaskService struct {
	Title   string `form:"title" json:"title" binding:"required,min=2,max=100"`
	Content string `form:"content" json:"content" binding:"max=1000"`
	Status  int    `form:"status" json:"status"` // 0 待办   1已完成
}

func (service *CreateTaskService) Create(id uint) serializer.Response {
	var user model.User
	model.DB.First(&user, id)
	task := model.Task{
		User:      user,
		Uid:       user.ID,
		Title:     service.Title,
		Content:   service.Content,
		Status:    0,
		StartTime: time.Now().Unix(),
	}
	code := e.SUCCESS
	err := model.DB.Create(&task).Error
	if err != nil {
		util.LogrusObj.Info(err)
		code = e.ErrorDatabase
		return serializer.Response{
			Status: code,
			Msg:    e.GetMsg(code),
			Error:  err.Error(),
		}
	}
	return serializer.Response{
		Status: code,
		Data:   serializer.BuildTask(task),
		Msg:    e.GetMsg(code),
	}
}
  1. 这个代码咋一眼觉得没啥问题,但是写的没有go味。最明显的是没有返回error,虽然返回值中包含error,但这是不太有go味!没内味!!
  2. 其次就是这个对db的操作,如create,不应该在service层实现,而是应该在dao层实现。

那么我们应该怎么做改呢?

2.2.2 改造后

首先我们应该改造我们的对象模式,使用sync.Once来进行对象的创建,原本是饿汉式的单例模式,现在是懒汉式的单例模式

懒汉式指的是在第一次访问单例对象时才进行实例化,而不是在程序启动时就进行实例化。
在下面的这个示例中,当第一次调用GetTaskSrv函数时,才会执行once.Do方法并初始化单例对象,从而实现了懒加载的效果

var TaskSrvIns *TaskSrv
var TaskSrvOnce sync.Once

type TaskSrv struct {
}

func GetTaskSrv() *TaskSrv {
	TaskSrvOnce.Do(func() {
		TaskSrvIns = &TaskSrv{}
	})
	return TaskSrvIns
}

在上面的代码中,使用了一个名为TaskSrvIns的全局变量来存储单例实例,once变量则用于确保GetTaskSrv函数只会被执行一次。

GetTaskSrv函数中,调用了once.Do方法,并传入一个匿名函数,用于初始化TaskSrvIns变量。这样,在第一次调用GetTaskSrv函数时,匿名函数会被执行,创建一个新的TaskSrv实例,并将其赋值给TaskSrvIns变量。之后,再次调用GetTaskSrv函数时,直接返回已经创建的TaskSrvIns变量,从而保证整个应用程序中只存在一个TaskSrv实例。

使用该实现方式可以有效地避免因为多线程操作而导致的线程安全问题,同时又能保证单例对象只会被创建一次

那么我们的函数就变成了这样:
传入context,定义的req结构体,返回了resp和error

func (s *TaskSrv) CreateTask(ctx context.Context, req *types.CreateTaskReq) (resp interface{}, err error) {}

之后我们抽离出dao层专门处理db的操作。

user, err := dao.NewUserDao(ctx).FindUserByUserId(u.Id)
task := &model.Task{
	User:      *user,
	Uid:       user.ID,
	Title:     req.Title,
	Content:   req.Content,
	Status:    0,
	StartTime: time.Now().Unix(),
}
err = dao.NewTaskDao(ctx).CreateTask(task)

然后再抽离出返回值

return ctl.RespSuccess(), nil

抽离获取用户的部分,我们后面再说。

完整代码:

var TaskSrvIns *TaskSrv
var TaskSrvOnce sync.Once

type TaskSrv struct {
}

func GetTaskSrv() *TaskSrv {
	TaskSrvOnce.Do(func() {
		TaskSrvIns = &TaskSrv{}
	})
	return TaskSrvIns
}

func (s *TaskSrv) CreateTask(ctx context.Context, req *types.CreateTaskReq) (resp interface{}, err error) {
	u, err := ctl.GetUserInfo(ctx)
	if err != nil {
		util.LogrusObj.Info(err)
		return
	}
	user, err := dao.NewUserDao(ctx).FindUserByUserId(u.Id)
	if err != nil {
		util.LogrusObj.Info(err)
		return
	}
	task := &model.Task{
		User:      *user,
		Uid:       user.ID,
		Title:     req.Title,
		Content:   req.Content,
		Status:    0,
		StartTime: time.Now().Unix(),
	}
	err = dao.NewTaskDao(ctx).CreateTask(task)
	if err != nil {
		util.LogrusObj.Info(err)
		return
	}
	return ctl.RespSuccess(), nil
}

2.3 middleware层

我们先定义一下所需要的用户信息,这里我们只需要用户的id,所以定义这个id就可以了,后续如果需要用户名字或是其他信息,都是可以加上去的。

type UserInfo struct {
	Id uint `json:"id"`
}

新建一个context,以便后续插入到context中,进行上下游的传递。

type key int

var userKey key

func NewContext(ctx context.Context, u *UserInfo) context.Context {
	return context.WithValue(ctx, userKey, u)
}

从context中获取信息,进行返回

func FromContext(ctx context.Context) (*UserInfo, bool) {
	u, ok := ctx.Value(userKey).(*UserInfo)
	return u, ok
}

func GetUserInfo(ctx context.Context) (*UserInfo, error) {
	user, ok := FromContext(ctx)
	if !ok {
		return nil, errors.New("获取用户信息错误")
	}
	return user, nil
}

所以我们在middleware层中可以把我们的new context并携带我们解析token的信息,加入到context流中进行上下游的流动。

c.Request = c.Request.WithContext(ctl.NewContext(c.Request.Context(), &ctl.UserInfo{Id: claims.Id}))

完整代码:

type key int

var userKey key

type UserInfo struct {
	Id uint `json:"id"`
}

func GetUserInfo(ctx context.Context) (*UserInfo, error) {
	user, ok := FromContext(ctx)
	if !ok {
		return nil, errors.New("获取用户信息错误")
	}
	return user, nil
}

func NewContext(ctx context.Context, u *UserInfo) context.Context {
	return context.WithValue(ctx, userKey, u)
}

func FromContext(ctx context.Context) (*UserInfo, bool) {
	u, ok := ctx.Value(userKey).(*UserInfo)
	return u, ok
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。