Go 经典分层结构及实践| 青训营笔记

127 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第3天

Go 经典分层结构及实践

分层结构

  • 什么是分层结构,为什么用?

分层架构是运用最为广泛的架构模式, 几乎每个软件系统都需要通过层 来隔离不同的关注点,以此应对不同需求的变化, 使得这种变化可以独立进行 用的原因是因为优点有:

  1. 降低复杂度,上层不需要关注下层细节。
  2. 提高灵活性,可以灵活替换某层的实现。
  3. 减小耦合度,将层次间的依赖减到最低。
  • 经典分层为三层: image.png

自顶向下由视图层(View)

负责处理和外部的交互逻辑

逻辑层(Entity )

负责处理核心业务逻辑输出

与数据层(Model)组成

负责外部数据的增删改查

实践

  • 在字节青训营里有一个这样的需求

image.png

我们先分析一下得出ER图

image.png 然后定义数据

  • 话题:首先一个话题必须有一个id作为主键方便我们管理;
    其次,一个帖子应该有标题和内容吧?再然后我们得加创建时间
    type Topic struct {
        Id         int64  `json:"id"`
        Title      string `json:"title"`
        Content    string `json:"content"`
        CreateTime int64  `json:"create_time"`
    }
    
  • 帖子:首先一个帖子必须有一个id作为主键方便我们管理;
    其次,一个帖子应该有内容吧?
    再然后一个帖子是不是得在某个话题下面?所以加一个话题id;(和话题关联上了)
    最后我们得加创建时间(如果做软删除的话再加一个删除时间)
    type Post struct {
        Id         int64  `json:"id"`
        ParentId   int64  `json:"parent_id"`
        Content    string `json:"content"`
        CreateTime int64  `json:"create_time"`
    }
    

为了能尽快地找到目标,我们通过map来进行索引

var (
   topicIndexMap map[int64]*Topic
   postIndexMap  map[int64][]*Post
)

然后我们进行初始化(以topic为例)

func initTopicIndexMap(filePath string) error {
   open, err := os.Open(filePath + "topic")
   if err != nil {
      return err
   }
   scanner := bufio.NewScanner(open)
   topicTmpMap := make(map[int64]*Topic)
   for scanner.Scan() {
      text := scanner.Text()
      var topic Topic
      if err := json.Unmarshal([]byte(text), &topic); err != nil {
         return err
      }
      topicTmpMap[topic.Id] = &topic
   }
   topicIndexMap = topicTmpMap
   return nil
}

在这里我们打开储存数据的文件,然后挨行扫描,把扫出来的json数据转换为我们go里的结构体

接着我们定义dao

type TopicDao struct {}
var (
   topicDao  *TopicDao
   topicOnce sync.Once
)

这里的topicDao我们不需要给它加成员参数,因为我们只需要用它作为一个接口,来实现一些方法 sync.Once使函数只执行一次的实现,常应用于单例模式,就是说用通过它的话某个函数就只能执行一次,不懂一会看下面例子即可❤

接着我们创建一个函数一个方法

func NewTopicDaoInstance() *TopicDao {
   topicOnce.Do(
      func() {
         topicDao = &TopicDao{}
      })
   return topicDao
}
func (*TopicDao) QueryTopicById(id int64) *Topic {
   return topicIndexMap[id]
}

先说方法,很简单就是通过id查询话题,里面的内容也很简单,这里不多说
函数的话,这里用到了工厂模式(设计模式中比较常见),topicOnce.Do()就是在在括号里给一个函数,然后就会调用一次,不会出现再调用

2. service层

流程👇 image.png 先定义数据

type QueryPageInfoFlow struct {
   topicId  int64
   pageInfo *PageInfo

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

然后这里就是流程

func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
   if err := f.checkParam(); err != nil {
      return nil, err
   }
   if err := f.prepareInfo(); err != nil {
      return nil, err
   }
   if err := f.packPageInfo(); err != nil {
      return nil, err
   }
   return f.pageInfo, nil
}

这里一共有三步

  1. 参数校验
    func (f *QueryPageInfoFlow) checkParam() error {
        if f.topicId <= 0 {
            return errors.New("topic id must be larger than 0")
        }
        return nil
    }
    
  2. 数据准备
    func (f *QueryPageInfoFlow) prepareInfo() error {
        //获取topic信息
        var wg sync.WaitGroup
        wg.Add(2)
        go func() {
            defer wg.Done()
            topic := Repository.NewTopicDaoInstance().QueryTopicById(f.topicId)
            f.topic = topic
        }()
        //获取post列表
        go func() {
            defer wg.Done()
            posts := Repository.NewPostDaoInstance().QueryPostsByParentId(f.topicId)
            f.posts = posts
        }()
        wg.Wait()
        return nil
    }
    
    这里是通过并行来加速,通过wait进行阻塞
  3. 实体组装
    func (f *QueryPageInfoFlow) packPageInfo() error {
        f.pageInfo = &PageInfo{
            Topic:    f.topic,
            PostList: f.posts,
        }
        return nil
    }
    

Controller层

定义数据

type PageData struct {
Code int64       `json:"code"`
Msg  string      `json:"msg"`
Data interface{} `json:"data"`
}

这里的Data建议用空接口来接收,因为后期数据再有任何变化这里也都不用改

func QueryPageInfo(topicIdStr string) *PageData {
   topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
   if err != nil {
      return &PageData{
         Code: -1,
         Msg:  err.Error(),
      }
   }
   pageInfo, err := Service.QueryPageInfo(topicId)
   if err != nil {
      return &PageData{
         Code: -1,
         Msg:  err.Error(),
      }
   }
   return &PageData{
      Code: 0,
      Msg:  "success",
      Data: pageInfo,
   }

}

然后就是查询,controller起到了一个承上启下的作用,即接收 api层的数据,然后调用service层处理和返回api层的东西(这里api层就是repository里定义的)

at last

主函数

func main() {
   if err := Init("./data/"); err != nil {
      os.Exit(-1)
   }
   r := gin.Default()
   r.GET("/community/page/get/:id", func(c *gin.Context) {
      topicId := c.Param("id")
      data := controller.QueryPageInfo(topicId)
      c.JSON(200, data)
   })
   err := r.Run()
   if err != nil {
      return
   }
}

每一个数据传到controller里,然后就能得到我们想要的了