课后实践 -《Go 语言工程实践之测试》| 青训营

135 阅读6分钟

课后作业

需求

  1. 支持发布帖子
  1. 本地 ID 生成需要保证不重复、唯一性
  1. Append 文件,更新索引,注意 Map 的并发安全问题

设计

1. server.go

	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)
	})
	err := r.Run()
	if err != nil {
		return
	}cd

这段代码使用了Gin框架实现了一个HTTP POST请求的处理函数,处理的路径是/community/post/do。该函数会从POST请求的form数据中获取uidtopic_idcontent三个参数,然后将这些参数传递给handler.PublishPost函数进行处理,并将处理结果作为JSON格式的响应返回给客户端。

其中,c是Gin框架提供的上下文对象,可以通过该对象获取HTTP请求的相关信息,并返回HTTP响应。c.GetPostForm方法用于从POST请求的form数据中获取参数值,如果参数不存在则返回空字符串。handler.PublishPost是业务逻辑处理函数,该函数接受三个参数,分别是用户ID、话题ID和帖子内容,返回处理结果。

最后,该函数使用r.Run()启动了一个HTTP服务器,监听请求并将其转发给相应的处理函数进行处理。如果启动服务器失败,则会返回错误。

2. controller层

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,
		},
	}

}

该函数接受三个参数,分别是用户ID、话题ID和帖子内容。函数首先将字符串类型的用户ID和话题ID转换为int64类型,然后调用service.PublishPost函数进行帖子发布操作。

如果帖子发布成功,该函数返回一个PageData指针类型的结构体,结构体中的Code字段设置为0,Msg字段设置为"success",Data字段设置为一个map[string]int64类型的字典,其中包含帖子ID的键值对。如果帖子发布失败,该函数返回一个PageData指针类型的结构体,结构体中的Code字段设置为-1,Msg字段设置为错误信息。

我们可以看到该函数的作用是将HTTP请求中传递的参数转换为业务逻辑层需要的类型,并将处理结果返回给HTTP处理函数。这种分层的设计模式可以使代码更加清晰和易于维护。

3. service层

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
}

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
}

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
}

上段代码中用到了第三方库idworker来生成唯一ID。在使用idworker库时,首先在init函数中创建了一个idworker.IdWorker对象,并调用InitIdWorker方法初始化ID生成器。然后,在发布帖子时,调用NextId方法生成一个全局唯一的ID,并将其设置为帖子的ID。

在发布帖子的流程中,首先调用NewPublishPostFlow方法创建一个PublishPostFlow对象,然后调用Do方法执行发布流程。在Do方法中,首先调用checkParam方法检查参数是否合法,然后调用publish方法将帖子插入到数据库中。

checkParam方法中,使用了utf16.Encode方法将字符串转换为UTF-16编码,并计算编码后的长度,以确保帖子内容不超过500个字符。

publish方法中,使用repository.Post结构体来表示帖子,并使用repository.NewPostDaoInstance方法获取PostDao单例对象,然后调用InsertPost方法将帖子插入到数据库中。同时,使用idworker.IdWorker对象生成唯一ID,并将其设置为帖子的ID。

由于idworker是第三方库,我们需要先使用go get命令将其安装到本地环境中。在使用idworker库时,我们需要注意以下几点:

  • 在初始化ID生成器时,我们需要指定worker IDdatacenter ID,以确保全局唯一。
  • 在生成ID时,我们需要使用NextId方法,它会返回一个int64类型的ID。
  • 在多线程环境下,我们需要使用锁来保护ID生成器。

idworker和snowflake都是用于生成全局唯一ID的算法,它们的核心思想都是在分布式系统中生成唯一ID,避免ID的冲突。

idworker和snowflake的区别主要在以下几个方面:

  1. 算法实现:idworker是基于Twitter的Snowflake算法实现的,而snowflake是由Twitter开发的一种ID生成算法。
  2. ID格式:idworker生成的ID是64位的,由3个部分组成:时间戳、工作机器ID和序列号;而snowflake生成的ID也是64位的,由3个部分组成:时间戳、工作机器ID和序列号。
  3. 时间精度:idworker的时间戳精度为毫秒级别,而snowflake的时间戳精度为毫微秒级别。
  4. 生成器数量:idworker支持的节点数量为2^21个,而snowflake支持的节点数量为2^10个。
  5. 使用方法:在Go语言中,idworker的使用方法是通过调用NextId方法生成ID,而snowflake的使用方法是通过调用Generate方法生成ID。

4. repository层

func (*PostDao) InsertPost(post *Post) error {
	f, err := os.OpenFile("./data/post", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
	if err != nil {
		return err
	}

	defer f.Close()
	marshal, _ := json.Marshal(post)
	if _, err = f.WriteString(string(marshal)+"\n"); err != nil {
		return err
	}

	rwMutex.Lock()
	postList, ok := postIndexMap[post.ParentId]
	if !ok {
		postIndexMap[post.ParentId] = []*Post{post}
	} else {
		postList = append(postList, post)
		postIndexMap[post.ParentId] = postList
	}
	rwMutex.Unlock()
	return nil
}

这段代码实现了将帖子对象序列化为JSON格式,并将其写入到名为"data/post"的文件中。同时,使用PostDao结构体来表示对帖子数据的访问,其中InsertPost方法用于将帖子插入到文件中。

在将帖子插入到文件系统中时,需要注意以下几点:

  1. 文件路径:需要指定正确的文件路径,以确保数据能够正确地写入到文件中。
  2. 文件打开模式:需要指定正确的文件打开模式。在这段代码中,使用了os.O_APPEND|os.O_WRONLY|os.O_CREATE模式,它表示以追加写入的方式打开文件,如果文件不存在则创建一个新文件。
  3. JSON序列化:需要将帖子对象序列化为JSON格式,并将其写入到文件中。在这段代码中,使用了json.Marshal方法将帖子对象序列化为JSON格式,然后将其转换为字符串并写入文件中。
  4. 并发访问:由于多个线程可能同时访问帖子数据,因此需要使用锁来保护帖子数据的访问。在这段代码中,使用了sync.RWMutex来保护postIndexMap的读写操作。

这种将帖子数据写入到本地文件系统中的方式适用于小规模的应用场景,数据量较大时可能会影响性能。在实际应用中,可以考虑使用分布式数据库或对象存储等技术来存储帖子数据。

5. 测试结果

1.jpg

2.png

3.png

4.png

5.png

6.png