分析golong—Web后端的三大层次 | 青训营
分析golong—Web后端的三大层次 | 青训营
业务描述如下:
查询自己(一个用户)的评论。数据格式为json
我们猜想:每一个用户存在一个或多个评论,必然在每个评论中数据格式必须包含自己的唯一id,而数据在cup上以map集合的方式,key:用户id,value:评论数据
类似Java的mvc三层架构
回顾一下Javaweb经典三层架构:
- dao 数据持久层
- service 业务逻辑层
- controller 视图层
1. 数据持久层 repository
在此之前先看一下数据的存在形式,我们以文本存储代替数据库存储
1.1. post(评论)
{"id":1,"parent_id":1,"content":"小姐姐快来1","create_time":1650437616}
{"id":2,"parent_id":1,"content":"小姐姐快来2","create_time":1650437617}
{"id":3,"parent_id":1,"content":"小姐姐快来3","create_time":1650437618}
{"id":4,"parent_id":1,"content":"小姐姐快来4","create_time":1650437619}
{"id":5,"parent_id":1,"content":"小姐姐快来5","create_time":1650437620}
{"id":6,"parent_id":2,"content":"小哥哥快来1","create_time":1650437621}
{"id":7,"parent_id":2,"content":"小哥哥快来2","create_time":1650437622}
{"id":8,"parent_id":2,"content":"小哥哥快来3","create_time":1650437623}
{"id":9,"parent_id":2,"content":"小哥哥快来4","create_time":1650437624}
{"id":10,"parent_id":2,"content":"小哥哥快来5","create_time":1650437625}
1.2. topic(用户或话题)看自己理解
{"id":1,"title":"青训营来啦!","content":"小姐姐,快到碗里来~","create_time":1650437625}
{"id":2,"title":"青训营来啦!","content":"小哥哥,快到碗里来~","create_time":1650437640}
看这里:
对上述数据格式进行存储定义,注意:go中以struct代替Java中的类class
type Post struct {
id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
//----注意:两个结构体存在自己的.go文件中,为的是代码可读性
type Post struct {
id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
我们在后续的查询要求必须只有一次查询,在这里我们创造dao,这里的dao作用只是为了限制查询次数,这里使用空结构体,作用在下面会说的
type PostDao struct {
}
type TopicDao struct {
}
限制次数可以使用package sync下once.go中的Do方法
var (
postDao *PostDao
PostOnce sync.Once
)
func NewPostDaoInstance() *PostDao {
PostOnce.Do(
func() {
postDao = &PostDao{}
})
return postDao
}
func (*PostDao) QueryPostsByParentId(parentId int64) []*Post {
return postIndexMap[parentId]
}
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]
}
欧克,直接回到主方法,之前,讲一下上面为什么使用空的结构体。上述代码New*函数返回的是空的结构体,使用sync.Once中的Do方法,保证每次都是使用最开始创建的结构体。这也是和Java不同的区别,使用的是指针,传递的是地址。OK!讲到这里我想你们也有自己的思路了,在Java中做到这一点也非常简单,我们可以实现单例模式。上我们回顾一下Java的单例模式,使用私有属性执行自身,提供共有的函数给这个私有属性赋值。话不多说直接上代码:
public class Singleton {
// 1.防止外部直接 new 对象破坏单例模式
private Singleton() {}
// 2.通过私有变量保存单例对象【添加了 volatile 修饰】
private static volatile Singleton instance = null;
// 3.提供公共获取单例对象的方法
public static Singleton getInstance() {
if (instance == null) {
// 第 1 次效验
synchronized (Singleton.class) {
if (instance == null) {
// 第 2 次效验
instance = new Singleton();
}
}
}
return instance;
}
}
2. 主函数
func main() {
if err := Init("./src/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
}
ok,一目了然,主函数首先调用持久层的Init方法,进行数据持久化,即,对数据进行装载
上述也讲过两种数据在内存中的存在形式
var (
topicIndexMap map[int64]*Topic
postIndexMap map[int64][]*Post
)
ok 介绍Init
方法
func Init(filePath string) error {
if err := initTopicIndexMap(filePath); err != nil {
return err
}
if err := initPostIndexMap(filePath); err != nil {
return err
}
return nil
}
Init
方法中调用了两个方法initTopicIndexMap(param string),initPostIndexMap(param string)
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
}
//==========================================================
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 scanner.Scan() {
text := scanner.Text()
//json化post
var post Post
if err := json.Unmarshal([]byte(text), &post); err != nil {
return err
}
//初始化postIndexMap
posts, ok := postTmpMap[post.ParentId]
//获取结果第一次为false
if !ok {
//真正更新postIndexMap
postTmpMap[post.ParentId] = []*Post{&post}
continue
}
posts = append(posts, &post) //将json化结果进行拼接
postTmpMap[post.ParentId] = posts
}
postIndexMap = postTmpMap
return nil
}
json.Unmarshal([]byte(text), &topic)
:将一个json格式的数据反序列化为topic
request := DictRequest{TransType: "en2zh", Source: word}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewBuffer(buf)
ok,回到主函数,在数据初始化成功后调用视图层
3. controller视图层
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(),
}
}
//无错误,进行调用service层进行数据存储
pageInfo, err := service.QueryPageInfo(topicId)
//调用时候存在错误,返回错误
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
//无错误,返回数据
return &PageData{
Code: 0,
Msg: "success",
Data: pageInfo,
}
}
代码我就不一一介绍
controller调用service层
4. service业务逻辑层
type PageInfo struct {
Topic *repository.Topic
PostList []*repository.Post
}
对外接口,下面是一个查询观点的函数,见名知意
func QueryPageInfo(topicId int64) (*PageInfo, error) {
return NewQueryPageInfoFlow(topicId).Do()
}
ok!, 代码时候调用另一个函数。这里我也有一点不太理解,语法。。。
该代码定义了一个名为Do()
的方法,它属于QueryPageInfoFlow
结构体。该方法不接受任何参数,并返回两个值:指向PageInfo
对象的指针和一个错误。
type QueryPageInfoFlow struct {
topicId int64
pageInfo *PageInfo
topic *repository.Topic
posts []*repository.Post
}
//==============
func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
return &QueryPageInfoFlow{
topicId: topId,
}
}
//==============
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) Do() (*PageInfo, error){}
*QueryPageInfoFlow 是一个指针类型,指向*QueryPageInfoFlow类型的值。 可以被*QueryPageInfoFlow类型的变量调用
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 {
//获取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
}
欧克,欧克,到这里了,下面我介绍一下golang特性,在后面文章中也会对其特性在具体案例中进行解释,喜欢可以点点关注
!!!
Go语言是一种开源的静态类型编程语言,具有以下特性:
- 简洁易读:Go语言的语法设计简洁清晰,注重可读性,减少了冗余的语法元素,使得代码更易于理解和维护。
- 高效编译:Go语言的编译速度非常快,可以快速生成可执行文件,加快开发和部署的速度。
- 并发支持:Go语言内置了轻量级的协程(goroutine)和通信机制(channel),使得并发编程更加简单和高效。通过goroutine和channel,可以实现并发任务的编排和通信,提高程序的性能和响应能力。
- 内存安全:Go语言通过内置的垃圾回收机制(garbage collector)和严格的内存访问规则,保证了程序的内存安全性,减少了常见的内存错误,如空指针引用和内存泄漏。
- 静态类型和类型推断:Go语言是静态类型语言,变量需要在编译时声明类型。但是,Go语言也支持类型推断,可以根据变量的赋值自动推导出变量的类型,简化了类型声明的过程。
- 内置并发模型:Go语言提供了内置的并发模型,包括goroutine和channel,使得编写并发代码变得更加简单和直观。同时,标准库中还提供了丰富的并发相关的包和工具,如sync、atomic、context等,方便开发者进行并发编程。
- 内置网络编程支持:Go语言标准库中提供了强大的网络编程支持,包括HTTP、TCP、UDP等协议的封装和实现,使得开发网络应用变得更加便捷和高效。
- 跨平台支持:Go语言的编译器可以生成不同平台的可执行文件,因此可以轻松地在不同操作系统和硬件架构上部署和运行Go程序。
- 强调工程化:Go语言鼓励使用模块化和规范化的代码组织方式,提供了包管理工具(go mod)和标准的代码风格指南,便于团队协作和项目维护。
- 丰富的标准库:Go语言的标准库提供了许多常用的功能和工具,包括字符串处理、文件操作、加密解密、正则表达式、测试框架等,开发者可以直接使用这些库来加速开发过程。
这些特性使得Go语言成为一种适用于构建高效
、可靠
和并发
的应用程序的编程语言。