Go语言入门-工程实践 | 青训营笔记

77 阅读9分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记

语言进阶

并发 VS 并行

  • 并发:多个线程程序在一个核的cpu上运行
  • 并行:多个线程程序在多个核的cpu上运行

协程 VS 线程

  • 协程:用户态,轻量级线程,栈KB级别。
  • 线程:内核态,线程跑多个协程,栈MB级别

goroutine

入门

 package main
 ​
 import (
     "fmt"
     "time"
 )
 ​
 func main() {
     //开启五个协程打印hello world
     for i := 0; i < 5; i++ {
         go sayHelloWorld(i)
     }
     //为了避免线程结束导致协程没全部运行完毕
     time.Sleep(time.Second * 2)
 }
 ​
 func sayHelloWorld(i int) {
     fmt.Println("Hello World : " + fmt.Sprint(i))
 }

倡导

提倡通过通信共享内存而不是通过共享内存通信

channel

基础入门

定义

var 变量名 chan 数据结构 通过make(chan 元素类型,[缓冲大小])

无缓冲通道

也被称为同步通道

有缓冲通道

关于同步的说明

默认情况下,通信是同步且无缓冲的,通道的发送/接收操作在对方准备好之前都是阻塞的:

  • 对于同一个通道,在没有接受者接收数据之前,发送操作会被阻塞。
  • 对于同一个通道,在没有发送者发送数据之前,接收操作会被阻塞。

下面例子:sendData阻塞了main1线程,所以下面的语句没办法被执行

 func main() {
     ch := make(chan string)
     
     //go sendData(ch)
     sendData(ch)
     go getData(ch)
     
     time.Sleep(1e9)
 }

演示

 package main
 ​
 import "fmt"
 ​
 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 val := range src {
             dest <- val * val
         }
     }()
 ​
     for val := range dest {
         fmt.Println(val)
     }
 }

sync.Mutex

 package main
 ​
 import (
     "fmt"
     "sync"
     "time"
 )
 ​
 var (
     lock sync.Mutex
     x    int64
 )
 ​
 func main() {
     x = 0
     //加锁的
     for i := 0; i < 5; i++ {
         go addWithLock()
     }
     time.Sleep(time.Second)
     fmt.Println("add with lock x =", x)
     //不加锁
     x = 0
     for i := 0; i < 5; i++ {
         go addWithoutLock()
     }
     time.Sleep(time.Second)
     fmt.Println("add without lock x =", x)
 }
 ​
 func addWithLock() {
     for i := 0; i < 2000; i++ {
         lock.Lock()
         x += 1
         lock.Unlock()
     }
 }
 ​
 func addWithoutLock() {
     for i := 0; i < 2000; i++ {
         x += 1
     }
 }

sync.WaitGroup

  • Add(delete int):计数器
  • Done():计数器-1
  • Wait():阻塞直到计数器为0
 package main
 ​
 import (
     "fmt"
     "sync"
 )
 ​
 func main() {
     var wg sync.WaitGroup
     wg.Add(5)
     for i := 0; i < 5; i++ {
         go func(j int) {
             defer wg.Done()
             helloWorld(j)
         }(i)
     }
     //5减完之后就停止阻塞
     wg.Wait()
 }
 ​
 func helloWorld(i int) {
     fmt.Println("Hello world i =", i)
 }

依赖管理

  • 文章:点击跳转
  • 文章形象说明了gopath和go vendor的弊端,还有go modules的使用

依赖管理演变

GOPATH => Go Vendor => Go Module

GOPATH

所有的 Go 项目都需要放在同一个工作空间:$GOPATH/src 内 这和 Go 的一设计理念紧密相关:包管理应该是去中心化的

  • 项目代码直接依赖src下代码
  • go get 下载最新版本的包到src目录下
  • 弊端:无法实现项目的多版本控制

弊端详解

在 Go 1.11版本之前,开发者是必须要配置 这个GOPATH环境变量的,这种代码代码管理模式存在比较严重的问题就是没有版本控制。 因为多个项目都会放在src目录下,而每个项目依赖的一些第三方包也是下载在src目录下的,当升级某个依赖包时那就是全局升级了,引用这个依赖包的项目都跟着升级包版本了,这样是一件很危险的事,你不知道升级的包在另外一个项目中是否能正常运行的。而且当多人协同开发时,你不知道别人下载的包是不是你所用的那个版本,容易出错且不好排查原因。

Go Vendor

其实针对 GOPATH 这种方式的弊端,官方也给出了解决方案,就是引入 vendor。解决的思路就是,在每个项目下都创建一个 vendor 目录,每个项目所需的依赖都只会下载到自己vendor目录下,项目之间的依赖包就互不影响了。在使用包时,会先从当前项目下的 vendor 目录查找,然后依次向上级目录查找。这种方式依旧是 GOPATH 模式下的,它解决了不同项目不能使用不同版本库的问题,但是也并不完美。如果多个项目使用的第三方库版本是一样的,那么就会造成相同的库存在多个目录下,占用空间而且没办法集中管理再一个就是当你要分享自己的项目时,除了源码,还要上传所有依赖的包,才能保证别人使用时不会因为版本问题报错

上课说的弊端:

  • 无法控制依赖的版本。
  • 更新项目又可能出现依赖冲突,导致编译出错。

Go Modules

GoModules 模式是 Go 语言 1.11 版本正式推出的,在 1.14 版本时,官方就发话 GoModules 的模拟已经成熟,可以用于生产环境了。所以新学习者们,可以直接用上这种模式了! 使用 GoModules 模式主要依赖于官方发布了自己的包管理工具,即 mod,如果做过前端的话,那你一定熟悉 npm,mod 就是类似于 npm 的工具。当你要使用 GoModules 模式时,你需要主动开启它,在终端输入命令 go env 时,你会发现一个 GO111MODULE 变量:

它就是开关,默认是 “auto” 模式,它所有可以设置的值有:

  1. auto: 自动模式,当项目下存在 go.mod 文件时,就启用 GoModules 模式;
  2. on: 开启模块支持,编译时会忽略 GOPATH 和 vendor 文件夹,只根据 go.mod下载依赖;
  3. off: 关闭模块支持,使用 GOPATH 模式。
 go mod download    // 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
 go mod edit        // 编辑go.mod文件
 go mod graph       // 打印模块依赖图
 go mod init        // 初始化当前文件夹, 创建go.mod文件
 go mod tidy        // 增加缺少的module,删除无用的module
 go mod vendor      // 将依赖复制到vendor下
 go mod verify      // 校验依赖
 go mod why         // 解释为什么需要依赖
 ​
 ​
 ​
 go get //第一步是下载源码包,第二步是执行go install。
 go install //go install 命令在内部实际上分成了两步操作:第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到 GOPATH/pkg。
 //https://studygolang.com/articles/3189
 //https://blog.csdn.net/zhangliangzi/article/details/77914943?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-1-77914943-blog-78866793.pc_relevant_baidufeatures_v7&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-1-77914943-blog-78866793.pc_relevant_baidufeatures_v7&utm_relevant_index=2

下载完成后,就会发现项目下多出一个 go.sum 文件,它的作用相当于锁定了当前项⽬依赖的所有模块版本,保证今后项目依赖的版本不会被篡改,更多的介绍可以自行搜索相关文档,一般我们不用管这个。 再看看 go.mod 文件中,多了一行 require github.com/jinzhu/now v1.1.2 // indirect ,这表示当前项目引入了这个包,其中注释 // indirect 表示的是间接的依赖,你可以理解为直接依赖的是 jiinzhu,间接依赖的是 now。 这里通过 go get 下载的包储是存在 GOPATH/pkg/mod目录下,同时会根据引入的路径来保存,比如上面下载的这个包,它的存放位置就是GOPATH/pkg/mod 目录下,同时会根据引入的路径来保存,比如上面下载的这个包,它的存放位置就是 GOPATH/pkg/mod/github.com/jinzhu。注意 GOPATH 模式下,下载的包是存在 $GOPATH/src 目录下的。

version

语义化版本

major.{major}.{minor}.${patch} v1.3.0 v2.3.0 第二个是做了新增等,但是兼容major;第三个是修复bug

基于commit伪版本 vx.0.0-yyyymmddhhmmss-哈希码 v0.0.0-20200401081211-c38fb59326b7

indirect

A->B->C A->C间接依赖,就会加一个://indirect

incompatible

我们还是以 Module github.com/RainbowMango/m 为例,假如其当前版本为 v3.6.0,因为其 Module 名字未遵循 Golang 所推荐的风格,即 Module 名中附带版本信息,我们称这个 Module 为不规范的 Module。 不规范的 Module 还是可以引用的,但跟引用规范的 Module 略有差别。 如果我们在项目 A 中引用了该 module,使用命令 go mod tidy,go 命令会自动查找 Module m 的最新版本,即 v3.6.0。 由于 Module 为不规范的 Module,为了加以区分,go 命令会在 go.mod 中增加 +incompatible 标识。

依赖图

选择最低的兼容版本

依赖分发-proxy

回源

  • 无法保证构建稳定性(增加/修改/删除软件版本)
  • 无法保证依赖可用性(删除软件)
  • 增加第三方压力(代码托管平台负载问题)
  • 稳定
  • 可靠

GOPROXY="proxy1.cn,https://proxy2.cn,…" 服务点URL列表,direct表示源站

工具

go mod

  • go mod init 初始化,创建go.mod文件
  • go mod download 下载模块到本地缓存
  • go mod tidy 增加需要的依赖,删除不需要的依赖

go get

  • go get example.org/pkg

    • @update 默认
    • @none 删除依赖
    • @v1.1.2 tag版本,语义版本
    • @23dfdd5 特定的commit
    • @master 分支的最新commit

测试

规则

  • 所有测试文件以_test.go结尾
  • 测试方法:func TestXxx(*testing.T):这第一个X不可以是小写字母[a-z]
  • 初始化逻辑放到TestMain中

使用:go test -v 执行所有 测试单个方法:例子:go test -v -run TestAppendInt 测试单个文件:例子:go test -v cal_test.go cal.go -v输出日志

  • Fatalf,格式化输出错误信息,并退出程序
  • Logf:可以输出相应的日志

TestMain

测试从TestMain进入,依次执行测试用例,最后从TestMain退出。

 package hello
 ​
 import(
     "fmt"
     "testing"
 )
 ​
 func TestAdd(t *testing.T) {
 ​
         r := Add(1, 2)
         if r !=3 {
 ​
                 t.Errorf("Add(1, 2) failed. Got %d, expected 3.", r)
         }
 ​
 }
 ​
 ​
 func TestMain(m *testing.M) {
     fmt.Println("begin")
     m.Run()
     fmt.Println("end")
 }
 func TestMain(m *testing.M) {
     //测试前:数据装载、配置初始化等工作
     code := m.Run()
     //测试后:释放资源等收尾工作
     os.Exit(code)//执行完这一句就退出了,后面的代码就不会执行了
 }

assert

 package testing
 ​
 func HelloTom() string {
     //return "tom"
     return "Tom"
 }
 package testing
 ​
 import (
     "github.com/stretchr/testify/assert"
     "testing"
 )
 ​
 func TestHelloTom(t *testing.T) {
     output := HelloTom()
     expectOutput := "Tom"
     assert.Equal(t, expectOutput, output)
 }

覆盖率

  • 进行一次测试,代码被执行的行数/总行数
 func JudgePassLine(score int16) bool {
     if score >= 60 {
         return true
     }
     return false
 }
 ​
 package testing
 ​
 import (
     "github.com/stretchr/testify/assert"
     "testing"
 )
 ​
 func TestJudgePassLine(t *testing.T) {
     isPass := JudgePassLine(70)
     assert.Equal(t, true, isPass)
 }
 ​

这里,我们代码执行了两行,2/3 约= 66.7%

 func TestJudgePassLine2(t *testing.T) {
     isPass := JudgePassLine(50)
     assert.Equal(t, false, isPass)
 }

多测一组就可以覆盖全部了

tips

  • 一般覆盖率:50%-60%,较高覆盖率80%+
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责

依赖

幂等:多次运行同一个case的结果一样,而要实现这一目的就要用到mock机制 稳定:稳定是指相互隔离,能在任何时间,任何环境,运行测试。

文件处理

下面举个栗子,将文件中的第一行字符串中的11替换成00,执行单测,测试通过,而我们的单测需要依赖本地的文件,如果文件被修改或者删除测试就会fail。为了保证测试case的稳定性,我们对读取文件函数进行mock,屏蔽对于文件的依赖。

Mock

monkey:github.com/bouck/monke…

  • Pathch:为一个函数打桩
  • Unpatch:为一个方法打桩

下面是一个mock的使用样例,通过patch对Readfineline进行打桩mock,默认返回line110,这里通过defer卸载mock,这样整个测试函数就拜托了本地文件的束缚和依赖。 Mokey的简单机制

例子

 package testing
 ​
 func ReadFirstLine() string {
     return "line 000"
 }
 ​
 func ProcessFirstLineWithMock() {
     //以前他是需要依赖ReadFirstLine函数的,我们想它脱离
 }
 package testing
 ​
 import (
     "bou.ke/monkey"
     "testing"
 )
 ​
 func TestProcessFirstLineWithMock(t *testing.T) {
     monkey.Patch(ReadFirstLine, func() string {
         return "line 000"
     })
     defer monkey.Unpatch(ReadFirstLine())
     //后面,我们执行就会用这个匿名函数代替ReadFirstLine
 }

我拉的时候是:github.com/bou.ke/monkey v1.0.2 拉:github.com/bouck/monkey,里面的moduel写了bou.ke/monkey v1.0.2

基准测试

例子

 package testing
 ​
 import "math/rand"
 ​
 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)]
 }
 package testing
 ​
 import "testing"
 ​
 //基准测试
 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()
         }
     })
 }

第一个的 第二个的:并发

基准测试以Benchmark开头,入参是testing.B, 用b中的N值反复递增循环测试 (对一个测试用例的默认测试时间是 1 秒,当测试用例函数返回时还不到 1 秒,那么 testing.B 中的 N 值将按 1、2、5、10、20、50……递增,并以递增后的值重新进行用例函数测试。 ) Resttimer重置计时器,我们再reset之前做了init或其他的准备操作,这些操作不应该作为基准测试的范围;runparallel是多协程并发测试;执行2个基准测试,发现代码在并发情况下存在劣化,主要原因是rand为了保证全局的随机性和并发安全,持有了一把全局锁。

优化

 func Select() int {
     //return ServerIndex[rand.Intn(10)]
     return ServerIndex[fastrand.Intn(10)]
 }
  • go get github.com/NebulousLabs/fastrand

项目实战

 package main
 ​
 import (
     "github.com/gin-gonic/gin"
     "go-gin-proj/handler"
     "go-gin-proj/repo"
     "go-gin-proj/util"
     "os"
 )
 ​
 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")
         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)
         json := make(map[string]string)
         c.BindJSON(&json)
         data := handler.PublishPost(json["uid"], json["topic_id"], json["content"])
         c.JSON(200, data)
     })
 ​
     if err := r.Run(); err != nil {
         return
     }
 }
 ​
 func Init() error {
     if err := repo.Init(); err != nil {
         return err
     }
     if err := util.InitLogger(); err != nil {
         return err
     }
     return nil
 }

数据库连接

这里的全局变量的生命周期是程序开始到结束,所以当我们完成对这个db全局变量的初始化之后就可以保证我们在其他包调用它的时候可以成功。怕这里你们存在疑惑。

 import (
     "gorm.io/driver/mysql"
     "gorm.io/gorm"
 )
 ​
 var db *gorm.DB
 ​
 func Init() error {
     dsn := "root:00000000@tcp(127.0.0.1:3306)/community?charset=utf8mb4&parseTime=True&loc=Local"
     var err error = nil
     db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
     return err
 }

dao层封装例子

 package repo
 ​
 import (
     "go-gin-proj/util"
     "gorm.io/gorm"
     "sync"
     "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"`
 }
 ​
 func (Post) TableName() string {
     return "post"
 }
 ​
 type PostDao struct {
 }
 ​
 var postDao *PostDao
 var postOnce sync.Once
 ​
 func NewPostDaoInstance() *PostDao {
     //单例模式
     postOnce.Do(
         func() {
             postDao = &PostDao{}
         },
     )
     return postDao
 }
 ​
 func (*PostDao) QueryPostById(id int64) (*Post, error) {
     var post Post
     err := db.Where("id = ?", id).Find(&post).Error
     if err == gorm.ErrRecordNotFound {
         return nil, nil
     }
     if err != nil {
         util.Logger.Error("find post by id err:" + err.Error())
         return nil, err
     }
     return &post, nil
 }
 ​
 func (*PostDao) QueryPostByParentId(parentId int64) ([]*Post, error) {
     var posts []*Post
     err := db.Where("parent_id = ?", parentId).Find(&posts).Error
     if err != nil {
         util.Logger.Error("find posts by parent_id err:" + err.Error())
         return nil, err
     }
     return posts, nil
 }
 ​
 func (*PostDao) CreatePost(post *Post) error {
     if err := db.Create(post).Error; err != nil {
         util.Logger.Error("insert post err:" + err.Error())
         return err
     }
     return nil
 }

service封装例子

 package service
 ​
 import (
     "errors"
     repository "go-gin-proj/repo"
     "time"
     "unicode/utf8"
 )
 ​
 func PublishPost(topicId, userId int64, content string) (int64, error) {
     return NewPublishPostFlow(topicId, userId, content).Do()
 }
 ​
 func NewPublishPostFlow(topicId, userId int64, content string) *PublishPostFlow {
     return &PublishPostFlow{
         userId:  userId,
         content: content,
         topicId: topicId,
     }
 }
 ​
 type PublishPostFlow struct {
     userId  int64
     content string
     topicId int64
     postId  int64
 }
 ​
 //检查参数
 func (f *PublishPostFlow) checkParam() error {
     //用户id不可以小于等于0
     if f.userId <= 0 {
         return errors.New("userId id must be larger than 0")
     }
     //内容不能大于等于500字
     if utf8.RuneCountInString(f.content) >= 500 {
         return errors.New("content length must be less than 500")
     }
     return nil
 }
 ​
 //调用repo保存数据
 func (f *PublishPostFlow) publish() error {
     post := &repository.Post{
         ParentId:   f.topicId,
         UserId:     f.userId,
         Content:    f.content,
         CreateTime: time.Now(),
     }
     if err := repository.NewPostDaoInstance().CreatePost(post); err != nil {
         return err
     }
     f.postId = post.Id
     return nil
 }
 ​
 // Do 将参数检查和数据提交封装到这里
 func (f *PublishPostFlow) Do() (int64, error) {
     if err := f.checkParam(); err != nil {
         return 0, err
     }
     if err := f.publish(); err != nil {
         return 0, err
     }
     return f.postId, nil
 }

控制层封装例子

 package handler
 ​
 import (
     "go-gin-proj/service"
     "strconv"
 )
 ​
 func PublishPost(uidStr, topicIdStr, content string) *PageData {
     //参数转换
     uid, _ := strconv.ParseInt(uidStr, 10, 64)
     topic, _ := strconv.ParseInt(topicIdStr, 10, 64)
     //获取service层结果
     postId, err := service.PublishPost(topic, uid, content)
     if err != nil {
         return &PageData{
             Code: -1,
             Msg:  err.Error(),
         }
     }
     return &PageData{
         Code: 0,
         Msg:  "success",
         Data: map[string]int64{
             "post_id": postId,
         },
     }
 ​
 }