这是我参与「第五届青训营 」伴学笔记创作活动的第3天
Go 经典分层结构及实践
分层结构
- 什么是分层结构,为什么用?
分层架构是运用最为广泛的架构模式, 几乎每个软件系统都需要通过层 来隔离不同的关注点,以此应对不同需求的变化, 使得这种变化可以独立进行 用的原因是因为优点有:
- 降低复杂度,上层不需要关注下层细节。
- 提高灵活性,可以灵活替换某层的实现。
- 减小耦合度,将层次间的依赖减到最低。
- 经典分层为三层:
自顶向下由视图层(View)
负责处理和外部的交互逻辑
逻辑层(Entity )
负责处理核心业务逻辑输出
与数据层(Model)组成
负责外部数据的增删改查
实践
- 在字节青训营里有一个这样的需求
我们先分析一下得出ER图
然后定义数据
- 话题:首先一个话题必须有一个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层
流程👇
先定义数据
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
}
这里一共有三步
- 参数校验
func (f *QueryPageInfoFlow) checkParam() error { if f.topicId <= 0 { return errors.New("topic id must be larger than 0") } return nil } - 数据准备
这里是通过并行来加速,通过wait进行阻塞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 } - 实体组装
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里,然后就能得到我们想要的了