这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
此次课程主要呈现了一个项目的开发思路与基本流程。
需求描述
构建社区话题页面,实现功能为:展示话题(标题,文字描述)和回帖列表,实现一个本地的web服务。话题和回帖数据用文件存储。
需求设计
ER图(Entity Relationship Diagram) 设计了概念模型。基于ER图可以有效描述现实世界的概念模型,包括实体的属性和实体的联系。话题页抽取出实体topic,postList,可以显示Topic和Post的属性以及两者的一对多关系。
结构设计
常用分层模型包括repository数据层,service逻辑层和controller视图层。
- 数据层:主要关联数据模型,封装外部数据的增删查改,拉取数据,面向逻辑层并屏蔽数据差异
- 逻辑层:负责处理核心业务逻辑,计算打包业务实体entity,处理核心业务逻辑输出。
- 视图层:包装Json格式化的请求结果,以API方式处理和外部的交互逻辑
代码开发
组件工具上主要基于高性能web框架gin。
data
创建data文件夹,定义topic和post的数据模型结构,以Json格式保存在文件内:
data/topic:
{
"id":1,
"title":"青训营来啦!",
"content":"新同学冲冲!",
"create_time":1650437625
}
{
"id":2,
"title":"青训营来啦!",
"content":"老同学冲冲!",
"create_time":1650437627
}
data/post:
Post:
{
"id":1,
"parent_id":1,
"content":"好期待呀!",
"create_time":1650437616
}
{
"id":2,
"parent_id":1,
"content":"666!",
"create_time":1650437617
}
{
"id":3,
"parent_id":2,
"content":"真不错!",
"create_time":1650437618
}
{
"id":4,
"parent_id":2,
"content":"下次继续!",
"create_time":1650437619
}
repository
repository部分,以topic为例,其主要功能包括初始化和查询。查询功能主要基于索引,按照topic的id查找对应结构体返回。为实现O(1)的时间复杂度,采用Map的数据结构,对Topic数据建立内存Map作为索引,这是基于data初始化索引的工作,Post部分同理,初始化工作如下:
创建索引,其中Topic和Post的数据结构在各自的文件中定义,列举如下:
//topic.go
type Topic struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
//init_db.go
var(
topicIndexMap map[int64]*Topic
postIndexMap map[int64][]*Post
rwMutex sync.RWMutex//
)
初始化话题数据索引的函数
//init_db.go
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 scannner.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
}
初始化回帖列表内存索引的函数同理,通过查询回帖对应的话题id(parent_id),返回一组回帖数组,每个数组的结构为指向post的指针。
//init_db.go
func initPostIndexMap(filePath string) error{
open, err := os.Open(filePath + "post")
if err != nil{
return err
}
scanner := bufio.NewScanner(open)
postTmpMap := make(map[int64][]*Post)
for scannner.Scan() {
text:=scanner.Text()
var post Post
if err:= json.Unmarshal([]byte(text), &post); err != nil{
return err
}
posts,ok := postTmpMap[post.PartentId]//对键值为切片的操作方法
if !ok{
postTmpMap[post.ParentId] = []*Post{&post}
continue
}
posts = append(posts, &post)
postTmpMap[post.ParentId] = posts
}
postIndexMap = postTmpMap
return nil
}
查询功能部分,其中sync.once主要适用于高并发场景下只执行一次的场景,once即为单例模式。以topic为例,设计了topic的json格式,并保证只初始化一次topicDao,然后基于索引实现查询函数QueryTopicById。
//topic.go
type Topic struct{
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"Content"`
CreateTime int64 `json:"create_time"`
}
type TopicDao struct{
}
var (
topicDao *TopicDao
topicOnce sync.Once
)
func NewTopicDaoInstance()* TopicDao{
topicOnce.Do(
func(){
topicDao = &TopicDao{}
}
)
return topicDao
}
func (*TopicDao) QueryTopicById(id int64) * Topic{
return topicIndexMap[id]
}
service
service层负责组建实体话题页信息,内容主要为Topic和PostList部分:
type PageInfo struct{
Topic *repository.Topic
PostList []*repository.Post
}
service中执行函数的过程包括参数校验,准备数据和组装实体,其中参数校验checkParam部分是保证服务安全性的,准备数据prepareInfo,通过调用repository层的因为话题和回帖信息的获取都依赖topic id,而且可以并行执行来提高执行效率,通过流程合并可以降低接口耗时,充分利用CPU资源。而组装实体packPageInfo将信息准备为合适的格式并发送给controller层。
func QueryPageInfo(topicId int64) (*PageInfo, error) {
return NewQueryPageInfoFlow(topicId).Do()
}
func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
return &QueryPageInfoFlow{
topicId: topId,
}
}
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
if err:=f.checkParam(); err != nil{//id校验,保证服务
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
}
//准备信息
func (f *QueryPageInfoFlow) prepareInfo() error{
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
在QueryPageInfo部分,基于topic的id信息,调用service层的 QueryPageInfo函数,并返回PageInfo,再封装成PageData格式的响应信息。其包含响应代码Code,显示信息Msg以及数据接口Data。
//query_page_info.go
type PageData struct{
Code int64 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"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,
}
}
主函数
主函数的实现主要包括基于gin框架的初始化数据索引,初始化引擎配置,构建路由以及启动服务。
package main
import (
"github.com/Moonlight-Zhao/go-project-example/cotroller"
"github.com/Moonlight-Zhao/go-project-example/repository"
"gopkg.in/gin-gonic/gin.v1"
"os"
)
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 := cotroller.QueryPageInfo(topicId)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
func Init(filePath string) error {
if err := repository.Init(filePath); err != nil {
return err
}
return nil
}
以上即为创建可查询话题下回帖信息的接口的实现过程。
参考资料
字节青训营课程:juejin.cn/course/byte…