Go学习笔记(三)项目实战——完成社区话题的后端逻辑

131 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天,点击查看活动详情

参考资料

项目要求

  1. 为社区话题页面实现一个web服务(不涉及前端),要求可展示话题(标题、文字描述)以及其下的回帖列表
  2. 不涉及数据库,仅涉及本地文件存储
  3. 可对话题进行回帖,回帖id唯一
  4. 注意Map并发性问题 项目地址

!!!注:以下是对参考代码的学习!!!

参考代码为github项目中的v0.1分支

E-R图设计及类字段描述

image.png

  • 每个Topic有一个独特的id,每个属于该Topic的Post都关联该id字段;每条Post的id也是唯一的(topic_id);
  • Topic有标题、内容、创建时间;Post也有自己的内容、创建时间

分层结构设计

后端不止是对数据进行增删查改,从文件(数据库)到客户,其实还分为以下几层:

  • 数据层:数据模型,对数据进行增删查改。
  • 逻辑层:业务实体,处理核心业务逻辑输出。
  • 控制层,展现视图,处理和外部的交互逻辑。

image.png

在代码中,这三层分别对应于:dal、service、handler。

Web框架Gin

web 框架:Gin - github.com/gin-gonic/g…

用于写一个发帖接口。 Gin示例代码如下:

func main() {
  // Creates a gin router with default middleware:
  // logger and recovery (crash-free) middleware
  router := gin.Default()

  router.GET("/someGet", getting)
  router.POST("/somePost", posting)
  router.PUT("/somePut", putting)
  router.DELETE("/someDelete", deleting)
  router.PATCH("/somePatch", patching)
  router.HEAD("/someHead", head)
  router.OPTIONS("/someOptions", options)

  // By default it serves on :8080 unless a
  // PORT environment variable was defined.
  router.Run()
  // router.Run(":3000") for a hard coded port
}

接口代码如下: server.go是项目的入口函数,接口写在其中的main函数中,注意要import Gin的库

import "gopkg.in/gin-gonic/gin.v1"
r.POST("/community/post/do", func(c *gin.Context) {
   topicId, _ := c.GetPostForm("topic_id")
   content, _ := c.GetPostForm("content")
   data := cotroller.PublishPost(topicId, content)
   c.JSON(200, data)
})

同时,在上述提到的三个层也添加对应代码:

Reposity

Reposity里面的db_init.go中添加一个变量:

var
{ rwMutex       sync.RWMutex }

由词猜意,这是Go语言定义的一个读写互斥锁,用于解决并发安全问题。

Service

service里面新增一个publish_post.go:

package service

import (
   "errors"
   "time"
   "unicode/utf16"

   "github.com/Moonlight-Zhao/go-project-example/repository"
   idworker "github.com/gitstliu/go-id-worker"
)

var idGen *idworker.IdWorker

func init() {
   idGen = &idworker.IdWorker{}
   idGen.InitIdWorker(1, 1)
}

func PublishPost(topicId int64, content string) (int64, error) {
   return NewPublishPostFlow(topicId, content).Do()
}

func NewPublishPostFlow(topicId int64, content string) *PublishPostFlow {
   return &PublishPostFlow{
      content: content,
      topicId: topicId,
   }
}

type PublishPostFlow struct {
   content string
   topicId int64

   postId int64
}

// 返回postId
func (f *PublishPostFlow) Do() (int64, error) {
   if err := f.checkParam(); err != nil {
      return 0, err
   }
   if err := f.publish(); err != nil {
      return 0, err
   }
   return f.postId, nil
}

// 限制帖子内容字数
func (f *PublishPostFlow) checkParam() error {
   if len(utf16.Encode([]rune(f.content))) >= 500 {
      return errors.New("content length must be less than 500")
   }
   return nil
}

// 调用下一层(Reposity)的InsertPost,实现发布功能
func (f *PublishPostFlow) publish() error {
   post := &repository.Post{
      ParentId:   f.topicId,
      Content:    f.content,
      CreateTime: time.Now().Unix(),
   }
   id, err := idGen.NextId()
   if err != nil {
      return err
   }
   post.Id = id
   if err := repository.NewPostDaoInstance().InsertPost(post); err != nil {
      return err
   }
   f.postId = post.Id
   return nil
}
  • 项目要求生成的id唯一,参考代码使用开源项目"github.com/gitstliu/go-id-worker" 来完成这一功能。

image.png 示例代码的意思应该是:初始值为1000,每次递增1。 换成本项目中,于是变成了从 1开始,每次递增1。

cotroller里面新增一个publish_post.go:

package cotroller

import (
   "github.com/Moonlight-Zhao/go-project-example/service"
   "strconv"
)

func PublishPost(topicIdStr, content string) *PageData {
   //参数转换
   topicId, _ := strconv.ParseInt(topicIdStr, 10, 64)
   //获取service层结果
   postId, err := service.PublishPost(topicId, content)
   if err != nil {
      return &PageData{
         Code: -1,
         Msg:  err.Error(),
      }
   }
   return &PageData{
      Code: 0,
      Msg:  "success",
      Data: map[string]int64{
         "post_id": postId,
      },
   }
}

这里就是写接口的参数返回信息。

结语

至此,对该项目代码的学习就结束了。虽然我只是大概看懂了逻辑,对很多细节为什么要这么实现还不理解、对一些实现原理还是很懵,但是至少对如何用Go写项目有了一个初步的了解,还是非常有收获的。

至于我还未能理解的地方,等我再更深入地学习Go及其原理、应用之后再回过头看,应该会有新的体悟。