Go 语言进阶 - 工程进阶 | 青训营笔记

91 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第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所需等待的协程数量;通过DoneWaitGroup所需等待的协程数量减一;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框架的社区话题页面实战项目的实现,讲解了项目开发的思路和流程。

image.png

在项目中,使用了典型的分层结构设计,如上图所示,整个系统被分为了数据层、逻辑层与视图层三个子模型。其中数据层封装了数据的存储以及增删改查操作;逻辑层处理具体的业务逻辑,并将数据上送到视图层;视图层处理与外部的交互逻辑,并封装返回请求结果。

数据层

在系统的数据层中,使用文件的形式持久化系统数据,并将系统的数据抽象为PostTopic两个模型:

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函数完成数据层的初始化。

参考链接

Using Go Modules - The Go Programming Language

Go 语言之旅 (go-zh.org)