Go 语言(实战项目笔记) | 青训营

73 阅读6分钟

实战目标:

  • 展示话题(标题,文字描述)和回帖列表
  • 暂不考虑前端页面实现,仅实现一个本地的web服务
  • 话题和回帖数据用文件存储

项目分层结构:

  1. 数据层:Repository 数据Model,封装外部数据的增删改查,并将数据初步反序列化,且需要直接与底层的数据存储形式打交道,比如存储形式是文件,还是数据库,还是微服务等等。
  2. 逻辑层:Service 业务Entity,这里会利用数据层得到封装好的数据再次封装得到更贴近客户端请求的数据,同样也需要写好增删改查,但这里的增删改查并不会与真正的外部数据打交道,也就是说Service层不关心底层数据的存储形式,只关心核心业务输出。
  3. 视图层:Controller 视图View,处理和外部的交互逻辑,也就是说,这个层级也是依赖于上一个层级的数据,它负责真正和客户端交互的过程,只关心返回什么样的数据给客户端,而前面两个层级都是为这个层级做的铺垫。

Repository层实现

主要实现底层存储数据序列化到具体的结构体上,以及对应的增删改查。

一般经过以下过程:

graph LR
a(初始化)
b(底层存储的交互)
a-->b
  • 初始化:主要是对数据的准备,或者时数据库的连接的初始化。
  • 底层存储的交互:如果数据库,那么就是对数据库发起请求得到对应的Model,如果是文件存储,那么数据应该已经初始化到内存,直接进行取值即可。
数据映射

由于本次的存储实现采取的是文件存储,故需要每次一次性把文件读取好并完成数据的反序列化。这里用到的map进行映射数据方便查询。

如果是数据库,这时应该通过一些orm框架直接进行数据的增删改查映射,但在此之前还是得连接数据库(初始化过程

数据的增删改查

topic.go

实现对话题的增删改查,这里用到了一个结构体+方法的方式去实现,且用sync.Once实现单例,我觉得好处在于:

  1. 防止重名。
  2. 方便记忆,方便调用时进行对应的语法补全(比如想要对Topic进行操作,只需要想到TopicDao这个即可补全后续的操作

和前面的实现类似,这里我完成了homework,添加了AddPost方法以及对应的将数据插入到文件的方法,由于可能出现多个客户端同时发起post请求,这时我们需要对数据进行并发安全的保护,这里我使用的Mutex加锁的方式。

Service层实现

主要是对Repository层的Modle进行进一步的封装成更上层需要的Entity。

一般经过以下流程:

graph LR
a(参数校验)
b(准备数据)
c(组装实体)
a-->b-->c
  • 参数校验:由于是和上层通信的层,上层调用得到数据时,首先**需要传入对应的参数,那么我们需要对这个参数进行校验,**不同的方法需要的参数是不同的,需要进行的校验也是不同的,比如本项目查询的方法和插入的方法,需要的参数就不同,所以对应的也是走的这三个流程。
  • 准备数据:在正式组装得到整个实体之前,我们应该先进行数据的准备,也就是需要把零件得到,当然,不一次性组装好的原因,我认为更重要的是这样可以减少代码的耦合,这样一来准备每个数据的过程可以独立开,且可以进行针对性的优化,或者进行局部的修改,也不会直接对组装代码造成影响。
  • 组装实体:把准备好的数据返回即可。

为了实现上述过程,我们建立一个结构体,保存准备的数据,且把整个组装实体的过程流程化。

结构体如下:

// PageInfo 一个页面的信息包括,topic和它上面的post言论
type PageInfo struct {
	Topic    *repository.Topic
	PostList []*repository.Post
}

// QueryPageInfoFlow 为了防止高耦合度的构造PageInfo,可以构造如下结构体实现流式处理
type QueryPageInfoFlow struct {
	topicId  int64
	pageInfo *PageInfo

	topic *repository.Topic
	posts []*repository.Post
}

整个组装过程:

// Do 整个组装过程
func (q *QueryPageInfoFlow) Do() (*PageInfo, error) {
	//对id进行合法性验证
	if err := q.checkNum(); err != nil {
		return nil, err
	}
	//准备好生成PageInfo的数据
	if err := q.prepareInfo(); err != nil {
		return nil, err
	}
	//打包最终的PageInfo
	if err := q.packPageInfo(); err != nil {
		return nil, err
	}
	return q.pageInfo, nil
}
参数校验

由于这个查询过程暂时只需要校验这一个参数

func (q *QueryPageInfoFlow) checkNum() error {
	if q.topicId <= 0 {
		return errors.New("topic must larger than 0")
	}
	return nil
}
准备数据

由于两个数据的查询毫无关联,可以通过并行处理。

graph LR
a[话题信息]
b[回帖信息]
c[查询]
d[结束]
c-->a
c-->b
a-->d
b-->d
//这两个过程,由于是毫无关联的,可以用go协程进行并发处理
func (q *QueryPageInfoFlow) prepareInfo() error {
	var wg sync.WaitGroup
	wg.Add(2)
	//获取Topic
	go func() {
		defer wg.Done()
		q.topic = repository.NewTopicDao().QueryTopicFromId(q.topicId)
	}()
	//获取Posts
	go func() {
		defer wg.Done()
		q.posts = repository.NewPostDao().QueryPostsFromParentId(q.topicId)
	}()

	wg.Wait()
	return nil
}
组装实体
//更新最终的PageInfo
func (q *QueryPageInfoFlow) packPageInfo() error {
	q.pageInfo = &PageInfo{
		Topic:    q.topic,
		PostList: q.posts,
	}
	return nil
}

这样的话实现整个QueryPageInfo函数就只需要调用这个结构体的方法即可。

如下:

func QueryPageInfo(id int64) (*PageInfo, error) {
	return NewQueryPageInfoFlow(id).Do()
}

Controller层实现

这个层级是真正对客户端发来的请求进行直接响应的层级,直接与客户端交互。

一般经过以下过程:

graph LR
a[参数解析]
b[构造数据]
c[返回数据]
a-->b-->c
  • 参数解析:由于对接的数据直接是上层收到的信息,所以大概率是纯字符串,所以需要先对参数进行解析。
  • 构造数据:也就是构造响应的数据,一般来说除了直接的数据外,还需要提供一个错误码和错误信息给前端。
  • 返回数据:根据不同情况构造的不同数据直接返回即可。
具体代码
// PageData 最终发送给客户端的json数据对应的结构体,我们需要错误码,以及对应错误码对应的消息,最后再是数据(用空接口实现泛型
type PageData struct {
	Code int64       `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}

// QueryPageINfo 真正和客户端进行交互的函数,需要注意客户端发来的流量都是字符串形式
func QueryPageINfo(topicIdStr string) *PageData {
	pageId, err := strconv.Atoi(topicIdStr)
	if err != nil {
		return &PageData{Code: 1, Msg: err.Error(), Data: nil}
	}
	pageInfo, err := service.QueryPageInfo(int64(pageId))
	if err != nil {
		return &PageData{Code: 2, Msg: err.Error(), Data: nil}
	}
	return &PageData{Code: 0, Msg: "success", Data: pageInfo}
}