本文首发于稀土掘金。该平台的作者 逐光而行 也是本人。
参考资料
- 字节跳动第三届青训营后端专场课程公开资料
https://juejin.cn/post/7093721879462019102
- 一位大佬写的青训营笔记
https://juejin.cn/post/7096064144180248612
项目要求
- 为社区话题页面实现一个web服务(不涉及前端),要求可展示话题(标题、文字描述)以及其下的回帖列表
- 不涉及数据库,仅涉及本地文件存储
- 可对话题进行回帖,回帖id唯一
- 注意Map并发性问题
!!!注:以下是对参考代码的学习!!!
参考代码为github项目中的v0.1分支
E-R图设计及类字段描述
- 每个Topic有一个独特的id,每个属于该Topic的Post都关联该id字段;每条Post的id也是唯一的(topic_id);
- Topic有标题、内容、创建时间;Post也有自己的内容、创建时间
分层结构设计
后端不止是对数据进行增删查改,从文件(数据库)到客户,其实还分为以下几层:
- 数据层:数据模型,对数据进行增删查改。
- 逻辑层:业务实体,处理核心业务逻辑输出。
- 控制层,展现视图,处理和外部的交互逻辑。
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" 来完成这一功能。
示例代码的意思应该是:初始值为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及其原理、应用之后再回过头看,应该会有新的体悟。