这是我参与「第五届青训营」伴学笔记创作活动的第2天。青训营的第二次课程中讲解了Go语言并发编程、依赖管理、单元测试、青训营话题页项目实战四个方面,下面是我针对课程整理的一些个人理解笔记。
Go语言并发编程
Go语言支持创建协程goroutine的方法来实现并发,goroutine的使用方法非常简单,仅需要go 函数名(函数列表)即可快速的运行一个goroutine:
func say(s string) {
fmt.Println(s)
}
...
go say("hello")
say("world")
相较于操作系统所支持的线程而言,协程goroutine的级别更加轻量化。goroutine间共享同一进程地址空间,当创建goroutine的函数返回后,goroutine也会同样结束。在Go语言中可以通过WaitGroup的方式等待协程完成。
var wg sync.WaitGroup
wg.Add(1)
go func() {
fmt.Println("hello")
wg.Done()
}
wg.Wait()
通过Add方法,可以增加WaitGroup所需等待的协程数量;通过Done将WaitGroup所需等待的协程数量减一;Wait将阻塞直到WaitGroup所需等待的协程数量为0。
在Go语言中,支持channel信道的方法进行协程同步。
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum
}
...
s := []int{1, 2, 3}
c := make(chan int)
go sum(s, c)
ret := <-c
fmt.Println(ret)
在Go语言中,支持chan信道类型,信道支持并发的FIFO消息传递,可以使用ch <- v将值v发送至信道ch中,也可以通过v := <-ch从ch中接收值。信道满时向其发送,以及信道空时从其接收均是阻塞的。
Go语言依赖管理
在大型项目中,对外部库的依赖是必不可少的。在最新版本的Go语言中,使用Go Module解决管理依赖问题,通过Go Module可以有效的控制项目依赖的关系及版本规则。
在Go语言中,可以通过go mod init在当前目录创建一个Go模块。值得注意的是,Go语言可以在构建项目的过程中,自动地识别模块中import但未提供的包,并自动查找该包的最新版本安装到本地。
然而,通过该方法虽然可以快速的添加依赖,但无法灵活的控制所依赖包的版本,同时所依赖包还会引入其他地间接依赖项。在某些时候,所依赖包或间接依赖包地最新版本可能并不兼容本地模块的使用。
$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
在Go语言中,可以通过go get语句显示的声明依赖项目及其制定版本,这样就可以自由的管理模块的依赖关系。值得注意的是,由于构建Go模块时会自动安装依赖项目,Go模块中可能存在未使用的依赖项,可以通过go mod tidy清除未使用的依赖项。
Go语言单元测试
在项目构建中,良好的单元测试是避免上线故障以及提高调试效率的重要途径。在Go语言中,提供了对于单元测试的默认功能,任何以_test.go结尾的文件都将被视作为测试文件,并可以通过go test命令构建并执行测试。
在测试文件中,func TestXxx(*testing.T)格式的函数将被视作为测试函数,并在执行测试的时候被运行。有关测试的初始化逻辑应当被放入TestMain中。
在测试中,某些函数的行为可能会依赖于某些外部因素,或不容易体现出所需测试的具体运行状态。因此,在Go语言中可以使用Mock语句使得测试文件中的某个函数被替换至另一个函数:
func TestLine(t *testing.T) {
monky.Patch(Readline, func() string {
return "hello world"
})
defer monkey.Unpatch(Readline)
assert.Equal(t, "hello world", line)
}
如上述语句所示,对应的Readline函数通过monkey.Patch语句被替换为固定返回hello world字符串的函数。通过该种模式,可以固定化Readline的运行模式,从而模拟出特定的运行情景。
社区话题页面实战项目
在本次课程中,演示了基于Gin框架的社区话题页面实战项目的实现,讲解了项目开发的思路和流程。
在项目中,使用了典型的分层结构设计,如上图所示,整个系统被分为了数据层、逻辑层与视图层三个子模型。其中数据层封装了数据的存储以及增删改查操作;逻辑层处理具体的业务逻辑,并将数据上送到视图层;视图层处理与外部的交互逻辑,并封装返回请求结果。
数据层
在系统的数据层中,使用文件的形式持久化系统数据,并将系统的数据抽象为Post和Topic两个模型:
type Post struct {
Id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type Topic struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
其中,Post模拟用户的投稿、Topic模拟话题,投稿和话题分别有唯一的Id所标识,并且每个Post中都存储了其所属的话题ID,用于将两者关联。
var (
topicIndexMap map[int64]*Topic
postIndexMap map[int64][]*Post
)
为了加速系统数据的查询,数据层中使用哈希表作为文件数据的缓存,在系统初始化时,数据从文件中读取并被写入至哈希表。并且,数据层向外提供访问哈希表的接口,。
func NewPostDaoInstance() *PostDao {
postOnce.Do(
func() {
postDao = &PostDao{}
})
return postDao
}
func (*PostDao) QueryPostsByParentId(parentId int64) []*Post {
return postIndexMap[parentId]
}
视图层
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(),
}
}
pageInfo, err := service.QueryPageInfo(topicId)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
return &PageData{
Code: 0,
Msg: "success",
Data: pageInfo,
}
}
在视图层中,使用QueryPageInfo解析所需访问的话题ID,并通过逻辑层的service.QueryPageInfo获得相应的回复,并将其填充PageData形式返回。
逻辑层
type PageInfo struct {
Topic *repository.Topic
PostList []*repository.Post
}
service.QueryPageInfo创建一个PageInfo对象,并调用其Do方法填充其字段。
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()
Do方法的核心流程为创建两个goroutine,通过数据层的接口返回话题及投稿数据,并将其填充入PageInfo对象。
主函数
func main() {
if err := Init("./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
}
在主函数中,使用gin框架接收HTTP请求,并通过视图层将HTTP请求中的ID传入系统,并返回查询数据,最后通过JSON发送HTTP回复。在执行gin框架前,需要调用Init函数完成数据层的初始化。