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

61 阅读4分钟

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指令工具管理依赖包

依赖管理三要素

  1. go.mod 描述依赖的包和依赖的定位
  2. proxy中心仓库
  3. 本地工具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
	}
}