Go语言进阶与依赖管理
语言进阶
并发指多线程在一个和的CPU上交替运行,而并行指的是多线程在多个核CPU上同步运行。Go语言实现了一个调度模型,充分发挥多核的CPU的优势
在Go语言中有一个Goroutine的概念,称为协程,及用户态的线程
协程 协程 协程 协程 [用户态]
----------------------------
线程 线程 [内核态]
协程的调度成本低,栈的空间小,由Go语言自行调度,因此一个程序可以开很多个协程
协程的例子
func hello(i int){
println("hello goroutine: " + fmt.Sprint(i))
}
func HelloGoRoutine(){
for i := 0; i < 5; i++{
go func(j int i){
hello(i)
}(i)
}
time.Sleep(time.Second)
}
协程间通信
Go提倡通过通信来共享内存。一般来说使用Channel来实现协程之间的通信。可以理解成传输队列。
channel分为无缓冲和有缓冲类型,创建方式:
c1 := make(chan int) //int型的无缓冲通道
c2 := make(chan int, 2) //int型的有缓冲通道,大小为2
无缓冲必须同步,即消息必须被接收协程才能继续运行,而有缓冲就可以不同步,只要缓冲没满就可以继续。
channel的具体使用
A协程发送0~9,B协程计算平方,主协程输出结果
func CalSuare(){
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(i)
}
}
主协程分别通过src dest两个channel与子协程通信。这里dest使用有缓冲,主要考虑消费者的消费速度可能更慢。
Lock
Go也可以共享内存,有锁可以实现临界内存的访问。和C语言中的互斥锁类似,需要使用Sync包中的内容。
WaitGroup
WaitGroup也是Go语言提供的并发工具,可以实现协程之间的同步,它的内部维护了一个计数,可以增加和减少,调用Add\Done即可,Wait方法会阻塞知道计数为0,可以维护协程之间的同步。比如最开始的hello程序就可以使用WaitGrouup
func hello(i int){
println("hello goroutine: " + fmt.Sprint(i))
}
func HelloGoRoutine(){
wg := sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++{
go func(j int i){
defer wg.Done()
hello(i)
}(i)
}
wg.wait()
}
依赖管理
工程项目中需要使用很多库,需要进行管理。Go的依赖管理从GOPATH到Go Vendor到现在的Go Module
不同环境依赖的库的版本不同,需要控制依赖库的版本
GOPATH
GoPATH是Go语言支持的环境变量,该目录下保护
- bin:二进制文件
- pkg:编译中间产物
- src:项目源码
项目代码都在src中,直接使用go get下载
这种方式就无法实现多个版本的package控制 v
Go Vender
在项目目录下增加一个vender文件,增加了一份依赖的副本,这样就可以支持不同的版本,如果在vender中没找到就再到src中找.
Go Vender的问题在于,如果项目A同时依赖包B和C,但是B和C依赖了同一个包的不同版本,这样就无法控制版本的选择,出现依赖冲突。
Go Module
- 通过go.mod文件管理依赖包管理
- 通过go get/go mod指令工具管理依赖包
依赖管理三要素
- go.mod 描述依赖的包和依赖的定位
- proxy中心仓库
- 本地工具go get/go mod
如何使用go Module配置?
go.mod的一个例子:
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
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/stretchr/testify v1.7.1 // indirect
github.com/ugorji/go v1.2.7 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/gin-gonic/gin.v1 v1.3.0
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gorm.io/driver/mysql v1.3.3 // indirect
gorm.io/gorm v1.23.4 // indirect
)
module是依赖管理的基本单单元
然后是go的版本
最后是单元依赖,Module的路径+版本号
indirect关键字表示不直接依赖
依赖分发
依赖可以在代码仓库的某一个特定提交或者版本上下载。
直接在代码仓库下载会出现问题:
- 无法保证构建稳定性
- 无法保证依赖可用性
- 增加第三方压力
Go Proxy是一个服务站点,缓冲仓库中的内容,开发者直接使用Proxy保证依赖的可靠性和稳定性。直接从Proxy拉依赖。
设置GOPROXY环境变量可以设置代理的位置
go mod工具
go get:可以拉依赖,如果直接拉就拉最新的包
可以加上@可以设置特殊方式
go mod:
- init 初始化go.mod文件
- download 下载模块到本地缓冲
- tidy 增加需要的依赖,删除不需要的依赖
Go语言工程实践 测试
测试分为单元测试、集成测试和回归测试。回归测试直接在实际的场景下进行测试,集成测试指对系统功能的测试,比如对某个接口进行自动化的测试,单元测试主要是开发者对某个函数或模块进行测试
单元测试
输入——>测试单元——>输出 校对
(函数/模块)
单元测试规则
- 所有测试文件以_test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑发放到func TestMain(*testing.M)
例如
func HelloTom() string{
return "Jerry"
}
func TestHelloTom(t *testing.T){
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput{
t.Errorf("...")
}
}
然后执行go test命令进行测试
可以使用一些assert包来实现相等/不相等的比较。
单元测试——覆盖率
代码覆盖率越完备,,代码的就越有保证
使用go test时加上--cover就可以现实出测试的覆盖率,这个覆盖率即被测试到的代码的比例。
单元测试——依赖
在一个项目中存在很多依赖,比如DB、File,单元测试需要达到幂等(重复运行结果相同),稳定(单元测试可以相互隔离)
如果直接使用单元测试调用DB等依赖,可能会遇到网络不稳定等问题。
可以使用Mock机制。
单元测试——Mock
利用Mock可以快速为一个函数/方法打桩,这样就可以消除单元测试的依赖
例如
func TestProcessFirstLineWithMock(t *testing T){
monkey.Patch(ReadFirstLine, func() string{
return "line110"
}) //用返回110的函数替代ReadFirstLine,不再依赖于读到文件
line := ProcessFirstLine()
assert.Eual(t, "line000", line)
}
基准测试
实际项目开发中会遇到性能瓶颈问题,需要进行记住测试,例如
var ServerIndex [10]int //10个服务
func InitServerIndex(){
for i := 0; i < 10; i++{
ServerIndex[i] = i + 100
}
}
func Select() int{
return ServerIndex[rand.Intn(10)
}
随机选择一个服务执行,这里可以使用Benchmark作基准测试这个负载均衡的性能,使用方式和test函数类似
项目实践
这个项目大致是用户可以浏览话题和回帖
- 展示话题和回帖列表
- 只考虑web服务
- 话题和回帖数据用文件存储
即用户可以浏览话题和帖子
需要设计两个实体
------- -----------
|Topic| |post_list|
------- -----------
|id | |id |
|title| |Topic_id |
|content| |content |
|create_time| |create_time|
------------ -------------
每个帖子需要关联到一个话题
分层结构
- 数据层:数据Model,数据的增删改查(这里数据存在文件中)
- 逻辑层:业务Entity,接收数据
- 视图层,处理和外部的交互
组件工具
- Gin高性能go web框架
- Go Mode
建立项目
go mod init mproject
go get gopkg.in/gin-gonic/gin.v1@v1.3.0
这样生成一个go.mod文件,包含了所有的依赖包
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
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/stretchr/testify v1.7.1 // indirect
github.com/ugorji/go v1.2.7 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/gin-gonic/gin.v1 v1.3.0
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gorm.io/driver/mysql v1.3.3 // indirect
gorm.io/gorm v1.23.4 // indirect
)
Repository
{
"id":1,
"title":"hihihi",
"content":"?123",
"create_time":543212
}
{
"id":66,
"title_id":1,
"content":"?123",
"create_time":123456
}
我们可以通过扫描整个文件来得到某个id对应的topic或者帖子。
这里在内存中用map模拟索引来加快查询
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
在Service层可以定义一个PageInfo,即页面实体
Service的流程包括参数校验、准备数据、组装实体
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
}
Controller层
Controller层主要是和外部进行交互,只需要定义号输入和输出协议即可:
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
在完成业务流程之后,就通过Gin来搭建框架,gin搭建一个基本矿建很简单,主要有以下几步:
func main() {
if err := Init(); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.Use(gin.Logger())
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id") //获得一个id
data := handler.QueryPageInfo(topicId)
c.JSON(200, data)
})
r.POST("/community/post/do", func(c *gin.Context) {
uid, _ := c.GetPostForm("uid")
topicId, _ := c.GetPostForm("topic_id")
content, _ := c.GetPostForm("content")
data := handler.PublishPost(uid, topicId, content)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}