Go语言项目实战web开发 | 青训营

177 阅读5分钟

社区话题页面后端web开发

随着互联网发展,web开发成为一个重要领域,本文将介绍如何使用go语言进行web开发。以实现一个社区话题页面后端web服务的项目为例。

我们选用Gin轻量级的web框架,简单易用,适合快速构建API和Web应用程序。

需求描述

image.png

需求用例

设计主要涉及两个功能点:用户浏览消费,涉及页面的展示,包括话题内容和回帖的列表。

从图中抽出两个实体:话题和帖子。我们可以设计一下他们的结构体内该有的属性,如下:

  • Topic话题
    • id
    • title
    • content内容
    • create_time创建时间
  • PostList帖子
    • id
    • topic_id每个帖子需要关联到一个话题上
    • content
    • create_time

ER图

下面ER图可以用来描述现实世界的概念模型。有了模型实体,属性以及之间的联系,对我们后续做开发就提供了比较清晰的思路。

image.png

回到需求,两个实体主要包括:实体的属性、实体的联系。 Topic和Post属于一对多的关系。

下一步就是要思考代码结构设计。 我们采用典型的分层结构设计。

分层结构

image.png

上图是一个比较常用的结构图,以后在项目中也可以用。

  • 数据层:关注数据Model,封装外部数据的增删改查。

    在此次实战中,我们的数据会存储在本地文件,通过文件操作拉取话题,帖子数据。

    数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。

    servoce的接口不关心底层数据的存储。只需要拿到repsitory返回的一个model数据就可以了。

  • 逻辑层:业务Entity,处理核心业务逻辑输出。

    service逻辑层处理核心业务逻辑,计算打包业务实体entity,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层。

  • 视图层:视图View,处理和外部的交互逻辑。

    controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问。

在实际生活应用中,要根据实际项目的成本和复杂度来修改结构。

组件工具

image.png

下面介绍开发涉及的基础组件和工具。

首先Gin,高性能开源的go web框架,我们基于Gin搭建web服务器,主要涉及路由分发。

因为我们引入web框架,所以就涉及go module依赖管理,我们首先通过go mod初始化管理配置文件,然后go get下载gin依赖。

image.png

有了框架依赖,我们只需要关注业务本身的实现,从数据层->逻辑层->视图层,一步步实现。

一、Respository数据层

image.png

主要包括以下两个查询:

  • 通过话题ID查话题
  • 通过话题ID查所有与此话题关联的帖子

Repository-index

这里引出索引的概念,可以引导我们快速查找定位我们需要的结果。

这里我们使用map实现内存索引,在服务对外暴露前,利用文件元数据初始化全局内存索引,这样可以实现O(1)的时间复杂度查找工作。这样很快定位到具体的数据。

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

Repository-初始化init

//初始化话题数据索引
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
}

Repository-查询

topic.go中利用ID查询话题

  • 索引:话题ID
  • 数据:话题
var (
	topicDao  *TopicDao
	topicOnce sync.Once//适合高并发情况下运行一次
)

func NewTopicDaoInstance() *TopicDao {
	topicOnce.Do(
		func() {
			topicDao = &TopicDao{}
		})
	return topicDao
}
//根据ID查询话题
func (*TopicDao) QueryTopicById(id int64) *Topic {
	return topicIndexMap[id]
}

同样可以写出post.go中,根据id查帖子列表

  • 索引:话题ID
  • 数据:帖子列表
func NewPostDaoInstance() *PostDao {
	postOnce.Do(
		func() {
			postDao = &PostDao{}
		})
	return postDao
}
//根据话题ID查帖子列表
func (*PostDao) QueryPostsByParentId(parentId int64) []*Post {
	return postIndexMap[parentId]
}

有了上述两个函数就可以上述给逻辑层,在逻辑层实现实体案例的封装

二、Service逻辑层

构建实体

type PageInfo struct {
	Topic    *repository.Topic
	PostList []*repository.Post
}

整个逻辑层实现流程为:参数校验->准备数据->组装实体

  • 对传入的topicID做一个非法校验
  • 通过Repository层查询函数拿数据
  • 组装实体

代码如下:

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
}
//id校验
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 {
	//两个没有先后顺序,所以并行实现
	//两个并行任务 add(2)
	var wg sync.WaitGroup 
	wg.Add(2)
	//获取topic信息
	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"` //code==0代表成功
	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,
	}

}

Router 搭建框架

路由是Web应用程序接收请求的入口。

搭建框架:

  • 初始化数据索引
  • 初始化引擎配置
  • 构建路由
  • 启动服务

通过gin把我们的服务通过http的形式暴露出去,id通过path变量做一个绑定。

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") //拿到path变量做解析
		data := cotroller.QueryPageInfo(topicId) //转化
		c.JSON(200, data) //JSON化返回数据
	})
	err := r.Run()  //启动服务
	if err != nil {
		return
	}
}