项目优化目标:
- 支持发布帖子。
- 本地Id生成保证不重复。
- Append文件,更新索引,注意并发安全问题。
我发现一个特点,这种分Controller、Service、Repository层的情况,
当你上层调用查询接口的时候,数据是自下往上的,也就是数据是从下往上依次封装。
而如果是实现添加操作接口的时候,数据是自上往下的,则数据是从上往下依次封装。
具体实现
思路:
- Id生成唯一性,是用的一个lastIndexId保存整个post中最大的id,之后每次添加post都继续增加这个lastIndexId来得到新的id。
- 并发安全问题,用到Mutex加锁临界区即可。
Repository层
AddPost提供是提供给Service层的接口。
需要实现把数据添加到map里以及append到文件中(对应fileDataInsertPost函数)
func (d *PostDao) AddPost(post *Post) error {
//加锁保证同时请求的并发安全
lock := sync.Mutex{}
lock.Lock()
posts, ok := postIndexMap[post.ParentId]
if !ok {
return errors.New("post invalid,not exist parent id")
}
//注意更新map里的数据,go切片并不像C++里的Vector,可能append后操作的就不是同一片 底层数组了
postIndexMap[post.ParentId] = append(posts, post)
err := fileDataInsertPost("./lesson2/homework/data/", post)
if err != nil {
return err
}
lock.Unlock()
return nil
}
func fileDataInsertPost(filePath string, post *Post) error {
open, err := os.OpenFile(filePath+"post", os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
writer := bufio.NewWriter(open)
data, err := json.Marshal(*post)
if err != nil {
return err
}
writer.WriteString("\r\n")
writer.Write(data)
writer.Flush()
return nil
}
Service层实现
之前实现的流程基本一致,先校验上层传来的参数,数据准备过程换成数据的发布(publish)过程,将得到的数据封装好后再传给下层(我们发现这个数据的组织过程和查询是反着的)
package service
import (
"errors"
"github.com/ACking-you/TraningCamp/lesson2/homework/repository"
"time"
"unicode/utf8"
)
func PublishPost(topicId, userId int64, content string) (int64, error) {
return NewPublishPostFlow(topicId, userId, content).Do()
}
func NewPublishPostFlow(topicId, userId int64, content string) *PublishPostFlow {
return &PublishPostFlow{
userId: userId,
content: content,
topicId: topicId,
}
}
type PublishPostFlow struct {
userId int64
content string
topicId int64
postId int64
}
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 f.userId <= 0 {
return errors.New("userId id must be larger than 0")
}
if utf8.RuneCountInString(f.content) >= 500 {
return errors.New("content length must be less than 500")
}
return nil
}
func (f *PublishPostFlow) publish() error {
post := &repository.Post{
ParentId: f.topicId,
UserId: f.userId,
Content: f.content,
CreateTime: time.Now().Unix(),
}
repository.LastPostId++
post.Id = repository.LastPostId
if err := repository.NewPostDao().AddPost(post); err != nil {
return err
}
f.postId = post.Id
return nil
}
Controller层
和之前的Query处理过程是完全一致的,解析参数-->构造内容-->返回内容
package controller
import (
"strconv"
"github.com/ACking-you/TraningCamp/lesson2/homework/service"
)
func PublishPost(uidStr, topicIdStr, content string) *PageData {
//参数转换
uid, _ := strconv.ParseInt(uidStr, 10, 64)
topic, _ := strconv.ParseInt(topicIdStr, 10, 64)
//获取service层结果
postId, err := service.PublishPost(topic, uid, 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 test 会执行这个软件包里面所有的测试。如果需要执行特定的测试在后面跟上这个测试的go文件名以及对应的测试文件名。
服务端代码
server.go
package main
import (
"github.com/ACking-you/TraningCamp/lesson2/homework/controller"
"github.com/ACking-you/TraningCamp/lesson2/homework/repository"
"gopkg.in/gin-gonic/gin.v1"
"os"
"strings"
)
//最后再通过gin框架搭建服务器
func main() {
//准备数据
if err := Init("./lesson2/homework/data/"); err != nil {
os.Exit(-1)
}
//注册路由
r := gin.Default()
r.GET("me:id", func(c *gin.Context) {
topicId := c.Param("id")
topicId = strings.TrimLeft(topicId, ":,")
println(topicId)
data := controller.QueryPageINfo(topicId)
c.JSONP(200, data)
})
r.POST("/post/do", func(c *gin.Context) {
uid, _ := c.GetPostForm("uid")
println(uid)
topicId, _ := c.GetPostForm("topic_id")
println(topicId)
content, _ := c.GetPostForm("content")
println(content)
data := controller.PublishPost(uid, topicId, content)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
func Init(filepath string) error {
err := repository.Init(filepath)
if err != nil {
return err
}
return nil
}
请求结果
使用的是goland里面的http请求工具进行的。
HTTP 定义了一组请求方法,以表明要对给定资源执行的操作。指示针对给定资源要执行的期望动作。虽然它们也可以是名词,但这些请求方法有时被称为 HTTP 动词。每一个请求方法都实现了不同的语义,但一些共同的特征由一组共享:例如一个请求方法可以是安全的、幂等的或可缓存的。
-
GET方法请求一个指定资源的表示形式,使用GET的请求应该只被用于获取数据。 -
HEAD方法请求一个与GET请求的响应相同的响应,但没有响应体。 -
POST方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或副作用。 -
PUT方法用有效载荷请求替换目标资源的所有当前表示。 -
DELETE方法删除指定的资源。 -
CONNECT方法建立一个到由目标资源标识的服务器的隧道。 -
OPTIONS方法用于描述目标资源的通信选项。 -
TRACE方法沿着到目标资源的路径执行一个消息环回测试。 -
PATCH方法用于对资源应用部分修改。
GET请求测试(成功)
请求报文如下:
GET http://localhost:8080/me:1
Accept: application/json
返回报文如下:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 09 May 2022 05:17:28 GMT
Content-Length: 426
{
"code": 0,
"msg": "success",
"data": {
"Topic": {
"id": 1,
"title": "青训营来啦!",
"content": "小姐姐,快到碗里来~",
"create_time": 1650437625
},
"PostList": [
{
"id": 1,
"parent_id": 1,
"content": "小姐姐快来1",
"create_time": 1650437616,
"user_id": 1
},
{
"id": 2,
"parent_id": 1,
"content": "小姐姐快来2",
"create_time": 1650437617,
"user_id": 2
},
{
"id": 3,
"parent_id": 1,
"content": "小姐姐快来3",
"create_time": 1650437618,
"user_id": 13
}
]
}
}
Response code: 200 (OK); Time: 174ms; Content length: 368 bytes
POST请求测试(成功)
请求报文:
POST http://localhost:8080/post/do
Content-Type: application/x-www-form-urlencoded
uid=2&topic_id=1&content=测试内容嗨嗨嗨嗨
返回报文:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 09 May 2022 05:22:38 GMT
Content-Length: 47
{
"code": 0,
"msg": "success",
"data": {
"post_id": 5
}
}
Response code: 200 (OK); Time: 103ms; Content length: 47 bytes