这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
这一章总结了Go语言工程进阶的内容,包括并发编程、依赖管理、单元测试和项目实战四个部分。
并发编程
-
Go可以充分发挥多核优势,高效运行
-
Goroutine协程
协程:用户态,轻量级线程,栈MB级别。
线程:内核态,线程跑多个协程,栈KB级别。
go func(参数定义){
}(传参)
-
Go提倡通过协程间通信以共享内存。
-
Channel通道
无缓冲通道make(chan int)
有缓冲通道make(chan int, 2)
通过channel来实现不同协程直接的信息交互
示例:第一个协程产生数字,传入无缓冲通道src中;第二个协程从src中取出数字,平方操作后传入有缓冲通道dest中;最后在main函数中打印dest中的数字。
这里dest设置为有缓冲,是因为如果存入的数字还没来得及输出,就有新的数字生成,就会造成错误。
func main() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
println("goroutine:", i)
}
}
同一个程序不同协程共享同一内存,有一定概率触发并发安全问题,这里需要用到lock锁。
示例:两个函数,都是对x进行加一操作,一个无锁,一个有锁,分别执行五次,每次建立一个协程。
两个循环之间暂停一秒time.Sleep(time.Second),以免协程还未结束就直接打印输出。
var (
x int64
lock sync.Mutex
)
func main() {
x = 0
for i := 0; i < 5; i++ {
go withoutLock()
}
time.Sleep(time.Second)
println("withoutLock:", x)
x = 0
for i := 0; i < 5; i++ {
go withLock()
}
time.Sleep(time.Second)
println("withLock:", x)
}
func withoutLock() {
for i := 0; i < 20000; i++ {
x += 1
}
}
func withLock() {
for i := 0; i < 20000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
最后输出:
withoutLock: 50543
withLock: 100000
说明没有锁的协程出现混乱。
- WaitGroup
上述代码成功执行主要靠time.Sleep暂停了一秒,但是实际工程中并不能准确估计需要几秒,这里需要WaitGroup。
WaitGroup实际上是一种计数器,它会阻止函数(或者一个goroutine)的执行,直到其内部计数器变为 0。
WaitGroup是并发安全的,有三个主要方法,add按给定的整数值增加 WaitGroup 计数器;done将 WaitGroup 计数器减 1,我们将使用它来指示 goroutine 的终止;wait阻止执行,直到它的内部计数器变为 0。
func main() {
var wg sync.WaitGroup
n := 5
wg.Add(n)
for i := 0; i < n; i++ {
go func(j int) {
defer wg.Done()
println("goroutine:", j)
}(i)
}
wg.Wait()
}
依赖管理
- Go Module
通过 go.mod 文件管理依赖包版本
通过 go get/go mod 指令工具管理依赖包
以达到终极目标,定义版本规则和管理项目依赖关系
- 依赖管理三要素
1、配置文件,描述依赖——go.mod
2、中心仓库管理依赖库——Proxy
3、本地工具——go get/mod
- 依赖配置go.mod
依赖管理基本单元+原生库+单元依赖
indirect指非直接依赖,incompatible指没有go.mod文件且主版本在2以上的依赖
module github.com/Moonlight-Zhao/go-project-example
go 1.16
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.3.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.6 // indirect
github.com/golang/protobuf v1.5.2 // indirect
}
某项目依赖于项目A和项目B,A依赖于1.3C,B依赖于1.4C,在C的1.3版本和1.4兼容的情况下选择1.4版本。
- 依赖分发proxy
GOPROXY="proxy1.cn, proxy2.cn ,direct"
proxy1=>proxy2=>direct
- go get
@update 默认
@none 删除依赖
@v1.1.2 tag版本,语义版本
@23dfdd5 特定的commit
@master 分支的最新commit
- go mod
init 初始化,创建go.mod文件
download 下载模块到本地缓存
tidy 增加需要的依赖,删除不需要的依赖
单元测试
-
事故举例:
1、营销配置错误,导致非预期用户享受权益,资金损失。
2、用户提现,幂等失效,短时间可以多次提现,资金损失。
3、代码逻辑错误,广告位被占,无法出广告,收入损失。
4、代码指针使用错误,导致APP不可用,损失巨大。 -
测试举例:
1、回归测试,在用户端进行场景测试。
2、集成测试,对功能进行测试。
3、单元测试,对单独的函数或模块等进行输入输出测试。
3.单元测试
所有测试文件命名以_test.go结尾;
测试函数命名func TestXxx(*testing.T);
func TestAppendInt(t *testing.T) {
AppendInt()
}
初始化逻辑放到 TestMain 中:
测远前的数据装载、配置初始化等前置工作;
code := m.Run();
测试后的释放资源等收尾工作
示例:
func AppendInt() {
intArray := [3]int64{1, 2, 3}
func(arr [3]int64) {
arr[2] = 4
fmt.Println("inner func array:",arr)
}(intArray)
fmt.Println("outer func array:",intArray)
}
import "testing"
func TestAppendInt(t *testing.T) {
AppendInt()
}
在GoLand中可以直接运行测试文件。
=== RUN TestAppendInt
inner func array: [1 2 4]
outer func array: [1 2 3]
--- PASS: TestAppendInt (0.00s)
PASS
可以导入assert包,使用assert.Equal来检测函数输出与期望输出是否相等。
import(
"github.com/stretchr/testify/assert"
)
{
assert.Equal(t, expectOutput, output)
}
- 评估项目单元测试测试水准的指标:代码覆盖率。
代码覆盖率指代码运行行数/没有运行的行数
在命令行输入。
PS D:\desktop\Go\go-project-example-main\attention> go test array_test.go array.go --cover
ok command-line-arguments 1.459s coverage: 100.0% of statements
注意:
一般覆盖率 : 50%~60%,较高覆盖率80%以上。
测试分支相互独立、全面覆盖。
测试单元粒度足够小,函数单一职责。
通过monkey.Patch对测试文件进行打桩测试,不再依赖本地file文件。
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
Line := ProcessFirstLine()
assert.Equal(t, expected:"line000", line)
}
- 基准测试
可以对当前代码进行性能分析,从而优化代码。
基准测试与单元测试类似,测试函数命名成func BenchmarkXXX(b *testing.B)
示例:
var (
ServerIndex [10]int
)
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
这里b.ResetTimer()重置时间,因为InitServerIndex()不是测试对象.
一个进行串行的基准测试,一个进行并行的基准测试b.RunParallel。
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
在命令行通过-bench命令进行基准测试
PS D:\desktop\Go\go-project-example-main\test\exe4> go test -bench=BenchmarkSelect
goos: windows
goarch: amd64
pkg: github.com/Moonlight-Zhao/go-project-example/test/exe4
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkSelect-8 83171610 18.30 ns/op
BenchmarkSelectParallel-8 21975200 57.69 ns/op
PASS
ok github.com/Moonlight-Zhao/go-project-example/test/exe4 6.341s
BenchmarkSelect-8表示逻辑CPU最多有8个,每次调用 BenchmarkSelect 函数花费 18.30 ns ,是执行 83171610次 的平均时间。
导入包
go get github.com/NebulousLabs/fastrand
import "github.com/NebulousLabs/fastrand"
将上示rand.Intn(10)改成fastrand.Intn(10)
命令行输入
PS D:\desktop\Go\go-project-example-main\test\exe4> go test -bench=BenchmarkSelect
goos: windows
goarch: amd64
pkg: github.com/Moonlight-Zhao/go-project-example/test/exe4
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkSelect-8 6455589 186.0 ns/op
BenchmarkSelectParallel-8 19993602 56.66 ns/op
PASS
ok github.com/Moonlight-Zhao/go-project-example/test/exe4 4.884s
感觉没有快百倍,但是在并发处理方面比math.rand要好
项目实战
做一个社区话题页面:
展示话题 (标题,文字描述) 和回帖列表
暂不考虑前端页面实现,仅仅实现一个本地web服务
话题和回帖数据用文件存储,不涉及使用数据库(简化使用)
-
对于用户user有两个行为,话题topic和帖子post
-
ER 图-Entity Relationship Diagram
-
分层结构
数据层: 数据 Model,外部数据的增删改查
逻辑层: 业务 Entity,处理核心业务逻辑输出
视图层: 视图 view,处理和外部的交互逻辑
-
数据层repository
创建topic和post两个结构体。
type Topic struct {
Id int64 `gorm:"column:id"`
UserId int64 `gorm:"column:user_id"`
Title string `gorm:"column:title"`
Content string `gorm:"column:content"`
CreateTime time.Time `gorm:"column:create_time"`
}
type Post struct {
Id int64 `gorm:"column:id"`
ParentId int64 `gorm:"parent_id"`
UserId int64 `gorm:"column:user_id"`
Content string `gorm:"column:content"`
DiggCount int32 `gorm:"column:digg_count"`
CreateTime time.Time `gorm:"column:create_time"`
}
- 索引
topic需要通过post中的id查找该话题下的帖子,一种方法是遍历比对查询,一种方法是将数据行转化成内存Map,通过索引直接定位对应的post。
var (
topicIndexMap map[int64]*repository.Topic
postIndexMap map[int64][]*repository.Post
)
初始化topic数据索引
读取文件,通过文件创建scanner,遍历scanner,将数据格式化成struct类型,存储在topicIndexMap中,索引为topic的id。
func initTopicIndexMap(filePath string) error {
open, err := os.Open(filePath + "topic")
if err == nil {
return err
}
scanner := bufio.NewScanner(open)
topicTmpMap := make(map[int64]*repository.Topic)
for scanner.Scan() {
text := scanner.Text()
var topic repository.Topic
err := json.Unmarshal([]byte(text), &topic)
if err != nil {
return err
}
topicTmpMap[topic.Id] = &topic
}
topicIndexMap = topicTmpMap
return nil
}
post的数据索引与topic类似。
通过topic的id可以查询话题和帖子
这里用到sync.Once,这个适合并发状态下只执行一次的函数,即单例模式。
type Topic struct {
Id int64 `gorm:"column:id"`
UserId int64 `gorm:"column:user_id"`
Title string `gorm:"column:title"`
Content string `gorm:"column:content"`
CreateTime time.Time `gorm:"column:create_time"`
}
func (Topic) TableName() string {
return "topic"
}
type TopicDao struct {
}
var topicDao *TopicDao
var topicOnce sync.Once
func NewTopicDaoInstance() *TopicDao {
topicOnce.Do(
func() {
topicDao = &TopicDao{}
})
return topicDao
}
func (*TopicDao) QueryTopicById(id int64) (*Topic, error) {
var topic Topic
err := db.Where("id = ?", id).Find(&topic).Error
if err != nil {
util.Logger.Error("find topic by id err:" + err.Error())
return nil, err
}
return &topic, nil
}
查询到数据之后,将数据传送至逻辑层进行实体封装。
- 逻辑层service
创建结构体
type TopicInfo struct {
Topic *repository.Topic
User *repository.User
}
type PostInfo struct {
Post *repository.Post
User *repository.User
}
type PageInfo struct {
TopicInfo *TopicInfo
PostList []*PostInfo
}
实现流程:参数校验->准备数据->组装数据
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
if err := f.checkParam(); err != nil {
return nil, err
}
if err := f.prepareInfo(); err != nil {
return nil, err
}
if err := f.packPageInfo(); err != nil {
return nil, err
}
return f.pageInfo, nil
}
在prepareInfo()中并行获取topic信息和回帖信息。
- 视图层controller
构建view对象,校验业务错误码
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(),
}
}
//获取service层结果
pageInfo, err := service.QueryPageInfo(topicId)
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
return &PageData{
Code: 0,
Msg: "success",
Data: pageInfo,
}
}
- 启动服务Router
go mod init
go get gopkg.in/gin-gonic/gin.v1@v1.3.0
就可以在go.mod里添加依赖,所有go.mod中缺少的包都可以用go get命令导入
PS D:\desktop\Go\go-project-example-main> go mod init
go: D:\desktop\Go\go-project-example-main\go.mod already exists
PS D:\desktop\Go\go-project-example-main> go get gopkg.in/gin-gonic/gin.v1@v1.3.0
go: downloading gopkg.in/gin-gonic/gin.v1 v1.3.0
go: downloading github.com/gin-contrib/sse v0.1.0
go: downloading github.com/gin-gonic/gin v1.3.0
......
GinWeb使用示例
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
运行后访问 http://localhost:8080/ping 即可。
启动流程:初始化数据索引->初始化引擎配置->构建路由->启动服务
func main() {
if err := Init(); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := handler.QueryPageInfo(topicId)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
- 最后运行server.go。
go run server.go
curl --location --request GET http://0.0.0.0:8080/community/page/get/2
- 更多关于框架的学习
github.com/bxcodec/go-…
medium.easyread.co/golang-clea…
课后实践作业
1、支持发布帖子。
2、本地 ld 生成需要保证不重复、唯一性。
3、Append 文件,更新索引,注意 Map 的并发安全问题。