手把手教学-Go语言进阶与依赖管理课 | 青训营

249 阅读9分钟

go语言进阶与依赖管理

不是开玩笑的,本文只要你愿意跟着抄,基本就能把课上的东西弄明白抄一遍🐶,实操写的非常非常详细。如果你看不明白,到下方github仓库提交issue,我来帮你解决问题🕊

如果这篇文章对你有帮助,希望你能留下点赞,如果你能到github点个star就更好了(@^0^@)/。

本文所有案例代码都可以在这里获取: MoFishXiaodui/ExecutableManual: 青训营后端-可执行手册-非常详细的手把手教学教程 (github.com)

相关资料(青训营同学配套)

Manual

此次可操作手册以PPT顺序为主,建议大家对照着PPT一起阅读,并且跟着操作

01 语言进阶

从并发编程的视角带大家了解Go性能的本质

案例1 - 快速打印

  1. 建立新文件夹,命名为2-1-1

  2. 创建文件,命名为 main.go (可自定义命名)

  3. 在文件逐步中写入一下代码

    1. 先写个基本框架

      package main
      ​
      func main() {
          
      }
      
    2. 写个hello函数,实现打印整数 i 的功能

      func hello(i int) {
          println("hello goroutine :", fmt.Sprint(i))
      }
      
    3. 写一个函数HelloGoRoutine来调用hello函数,实现并发调用5个hello函数。

      func HelloGoRoutine() {
          for i := 0; i < 5; i++ {     
              go func(j int) {
                  hello(j)
              }(i) // 声明一个匿名函数并立即调用
          }
          // 设置延时等待
          time.Sleep(time.Second)
      }
      
    4. 把HelloGoRoutine函数放到main里进行调用

      func main() {
          HelloGoRoutine()
      }
      
    5. 当你保存时,可以看到代码上方已经自动imports了fmt和time包。

    6. 最终代码见 src/2-2-1/main

    7. 然后可以执行命令 go run main.go 看到一下效果

      (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-1> go run .\main.go
      hello goroutine : 4
      hello goroutine : 2
      hello goroutine : 3
      hello goroutine : 0
      hello goroutine : 1
      

      为了查看并发的效果,可以多跑几次。可以观察到每次输出的顺序都是不一样的

案例2 - Channel

这个案例主要学习Channel的作用以及尝试使用有缓冲的Channel

  1. 建立新的文件夹 2-1-2channel (文件夹命名不要用空格)

  2. 创建新的文件 channelWithBuf.go

  3. 然后往channelWithBuf.go文件写入代码

    1. 先写个基本框架

      package main
      ​
      func main() {
          CalSquare()
      }
      
    2. 定义函数CalSquare,阅读代码顺序参见注释的编号

      func CalSquare() {
          // 1-创建一个无缓冲的channel: src,创建一个有缓冲的channel: dest
          src := make(chan int)
          dest := make(chan int, 3)
          
          // 2-创建一个立即执行的匿名routine,依次往src管道里面放入数字 0~9,并在最后关闭src管道
          go func() {
              defer close(src)
              for i := 0; i < 10; i++ {
                  src <- i
              }
          }()
          
          // 3-创建一个立即执行的匿名routine,接收src管道的数字,计算其平方放入dest管道中,并在最后关闭dest管道
          go func() {
              defer close(dest)
              for i := range src {
                  dest <- i * i
              }
          }()
          
          // 4-接收dest管道的数字并打印出来
          for i := range dest {
              fmt.Println(i)
          }
      }
      
    3. 保存文件时自动导入相关的包,最终的代码见 src/2-1-2channel/channelWithBuf.go

    4. 执行go run channelWithBuf.go查看效果,大致如下:

      (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-2channel> go run .\channelWithBuf.go
      0
      1
      4
      9
      16
      25
      36
      49
      64
      81
      

案例3-并发安全Lock

这个案例主要学习使用 sync.Mutex 锁让并发变得安全

  1. 建新文件夹2-1-3mutex

  2. 创建文件mutex.go

  3. mutex.go文件中写入:

    1. 基本框架

      package main
      
      func main() {
      	
      }
      
    2. 定义全局变量x和全局锁lock

      var x int64
      var lock sync.Mutex
      
    3. 定义一个带锁操作x 和 一个不带锁操作x 的函数addWithLockaddWithoutLock

      func addWithLock() {
      	for i := 0; i < 2000; i++ {
      		lock.Lock()
      		x += 1
      		lock.Unlock()
      	}
      }
      
      func addWithoutLock() {
      	for i := 0; i < 2000; i++ {
      		x += 1
      	}
      }
      
    4. 在main函数中并发五个goroutine去调用无锁的函数,等待1秒输出x的值。然后把x清零,再并发五个goroutine去调用有锁的函数,等待1秒输出x的值

      func main() {
      	x = 0
      	for i := 0; i < 5; i++ {
      		go addWithoutLock()
      	}
      	time.Sleep(time.Second)
      	println("withoutLock: ", x)
      
      	x = 0
      	for i := 0; i < 5; i++ {
      		go addWithLock()
      	}
      	time.Sleep(time.Second)
      	println("withLock: ", x)
      }
      
  4. 保存文件,自动导入synctime包。最终代码src/2-1-3mutex/mutex.go

  5. 多次运行go run mutex.go,查看效果。(有锁的能准确输出10000,无锁的就会有差错)

    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-3mutex> go run .\mutex.go
    withoutLock:  8475
    withLock:  10000
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-3mutex> go run .\mutex.go
    withoutLock:  8519
    withLock:  10000
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-3mutex> go run .\mutex.go
    withoutLock:  8359
    withLock:  10000
    

案例4 - WaitGroup

案例1中,我们并发打印数字,是通过main中的time.Sleep()函数给予充足时间等待并发完全执行,这是一种不太理想的做法,我们在实际情况中通常无法评估需要多长时间进行等待合适。通过等待管道可以解决这个问题。在此案例,我们这里通过sync包下的WaitGroup结构更优雅地处理。

  • sync.WaitGroup

    • Add(delta int) - 计数器 +delta
    • Done() - 计数器 -1
    • Wait() - 阻塞直到计数器为 0
  1. 新文件夹 2-1-4waitGroup,新文件waitGroup.go

  2. waitGroup中:

    1. 基本框架

    2. 直接在main中写:

      func main() {
      	// 新建一个WaitGroup结构
      	wg := sync.WaitGroup{}
      
      	// 计数器 + 5
      	wg.Add(5)
      
      	// 并发快速打印 0-5
      	for i := 0; i < 5; i++ {
      		go func(num int) {
      			// 延迟计数器 -1
      			defer wg.Done()
      			println(num)
      		}(i)
      	}
      
      	// 阻塞等待计数器清零
      	wg.Wait()
      }
      
  3. 保存自动导包sync,最终代码见src/2-1-4waitGroup/waitGroup.go

  4. 直接运行

    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-3mutex> go run .\mutex.go
    withoutLock:  8519
    4
    2
    3
    0
    1
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-4waitGroup> go run .\waitGroup.go
    4
    2
    3
    1
    0
    

02 依赖管理

  • 背景
  • Go依赖管理演进
  • Go Module实践

GoPath -> GoVendor -> GoModule

Go Module

  • 通过go.mod文件管理依赖包版本
  • 通过go get / go mod 指令工具管理依赖包

终极目标:定义版本规则和管理项目的依赖关系

依赖管理三要素

  • 配置文件,描述依赖 - go.mod
  • 中心仓库管理依赖库 - proxy
  • 本地工具 - go get / mod

依赖配置-version

  • 语义化版本

    • ${Major}.${Minor}.${Patch}
    • Major版本不同会被认为是不同的模块
    • Minor版本通常是新增函数或功能,向后兼容
    • Patch版本一般是修复bug
  • 基于commit伪版本

    • vX.0.0-时间戳yyyymmddhhmmss-哈希前缀12位abcdefgh1234

案例5 - 感觉不太重要,以后被催更了再补充

03 测试

  • 单元测试

    • 规则

      • 所有测试文件以 _test.go 结尾

      • 测试函数的签名:func TestXxx(*testing.T)

      • 初始化逻辑放到TestMain中

        func TestMain(m *testing.M) {
        	// 测试前:装载数据、配置初始化等前置工作
            code := m.Run()
            // 测试后:释放资源等收尾工作
        }
        
  • Mock测试

  • 基准测试

案例6 - 单元测试

  1. 新建文件夹2-1-6unitTest,在内新建文件helloTom.gohelloTom_test.go

  2. helloTom.go中书写以下代码

    package __1_6unitTest
    
    func HelloTom() string {
    	return "Jerry"
    	//return "Tom"
    }
    
  3. helloTom_test.go中书写以下代码

    package __1_6unitTest
    
    import (
    	"testing"
    )
    
    func TestHelloTom(t *testing.T) {
    	// T is a type passed to Test functions to manage test state and support formatted test logs.
    	output := HelloTom()
    	expectOutput := "Tom"
    
    	if output != expectOutput {
    		t.Errorf("Expected %s do not match actual %s", expectOutput, output)
    	}
    }
    
    //func TestMain(m *testing.M) {
    // M is a type passed to a TestMain function to run the actual tests.
    //	code := m.Run()
    //	os.Exit(code)
    //}
    
  4. 最终代码内容见 src/2-1-6unitTest/

  5. 在终端中执行 go test helloTom.go helloTom_test.go,查看效果

    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-6unitTest> go test helloTom_test.go helloTom.go    
    --- FAIL: TestHelloTom (0.00s)
        helloTom_test.go:13: Expected Tom do not match actual Jerry
    FAIL
    FAIL    command-line-arguments  0.184s
    FAIL
    
    # 把HelloTom()的返回值改为Tom后测试
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-6unitTest> go test helloTom_test.go helloTom.go
    ok      command-line-arguments  0.180s
    

案例7 - 单元测试assert

此次案例采用外部库github.com/stretchr/testify/assert来测试。

  1. 复制上一个案例2-1-6unitTest文件夹,更名为2-1-7assertTest

  2. 可以把两个文件的package都指定为main(就是代码的第一行)

  3. 2-1-7assertTest目录下,执行命令go mod int assertTestgo get "github.com/stretchr/testify/assert"

    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-7assertTest> go mod init assertTest
    go: creating new go.mod: module assertTest
    go: to add module requirements and sums:
            go mod tidy
    
    (base) 【...】\2-1-7assertTest> go get "github.com/stretchr/testify/assert"
    go: added github.com/davecgh/go-spew v1.1.1
    go: added github.com/pmezard/go-difflib v1.0.0
    go: added github.com/stretchr/testify v1.8.4
    go: added gopkg.in/yaml.v3 v3.0.1 
    
  4. 然后你可以看到新建的 go.modgo.sum文件记录了模块和依赖信息

  5. 修改helloTom_test.go的代码,先在import导入新的包

    import (
    	"testing"
    	"github.com/stretchr/testify/assert"
    )
    
  6. 把下面手动比较的代码换成assert的Equal函数

    // 原来
    if output != expectOutput {
        t.Errorf("Expected %s do not match actual %s", expectOutput, output)
    }
    
    // 替换为
    assert.Equal(t, expectOutput, output)
    
  7. 最终代码见src/2-1-7assertTest/

  8. 案例6一样执行测试命令,查看效果如下

    # HelloTom 返回 Tom 时
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-7assertTest> go test .\helloTom.go .\helloTom_test.go
    ok      command-line-arguments  0.199s
    
    # HelloTom 返回 Jerry时
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-7assertTest> go test .\helloTom.go .\helloTom_test.go
    --- FAIL: TestHelloTom (0.00s)
        helloTom_test.go:13:
                    Error Trace:    D:/code/MoFishXiaodui/ExecutableManual/src/2-1-7assertTest/helloTom_test.go:13
                    Error:          Not equal:
                                    expected: "Tom"
                                    actual  : "Jerry"
    
                                    Diff:
                                    --- Expected
                                    +++ Actual
                                    @@ -1 +1 @@
                                    -Tom
                                    +Jerry
                    Test:           TestHelloTom
    FAIL
    FAIL    command-line-arguments  0.204s
    FAIL
    

案例8 - 单元测试 覆盖率

如果想知道测试代码所测代码覆盖的范围,可以在 test 命令后面加一个参数 --cover

在测试新案例的代码之前,可以先在案例6或者案例7的代码上面测试。

# HelloTom 返回 Tom
(base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-6unitTest> go test .\helloTom.go .\helloTom_test.go --cover
ok      command-line-arguments  0.194s  coverage: 100.0% of statements

# HelloTom 返回 Jerry
(base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-7assertTest> go test .\helloTom.go .\helloTom_test.go --cover
--- FAIL: TestHelloTom (0.00s)
    helloTom_test.go:13:
                Error Trace:    D:/code/MoFishXiaodui/ExecutableManual/src/2-1-7assertTest/helloTom_test.go:13
                Error:          Not equal:
                                expected: "Tom"
                                actual  : "Jerry"

                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1 +1 @@
                                -Tom
                                +Jerry
                Test:           TestHelloTom
FAIL
        command-line-arguments  coverage: 100.0% of statements
FAIL    command-line-arguments  0.214s
FAIL

可以看到两次输出都有一行coverage标识,标识测试的覆盖率

为了体验并非100%覆盖率的情况我们使用案例代码

  1. 新建文件夹2-1-8coverage,在内新建文件夹coverage,再在内新建文件judge.gojudge_test.go

  2. 2-1-8coverage目录下执行

    1. go mod init coverage
    2. go get "github.com/stretchr/testify/assert"
  3. judge.go中书写以下代码

    package coverage
    
    func Judge(n int) bool {
    	if n >= 60 {
    		return true
    	}
    
    	// 观察通过率时加一些只增加行数,不改变逻辑的代码
    	// n += 10
    	// n -= 10
    
    	return false
    }
    
  4. judge_test.go中书写以下代码

    package coverage
    
    import (
    	"github.com/stretchr/testify/assert"
    	"testing"
    )
    
    func TestJudge(t *testing.T) {
    	score1 := Judge(66)
    	assert.Equal(t, true, score1)
    
    	// score2 := Judge(40)
    	// assert.Equal(t, false, score2)
    }
    
  5. 最终代码见src/2-1-8coverage/

  6. 2-1-8coverage目录下执行 go test .\coverage\ --cover 测试

    # 单独66分的情况
    (base) PS \2-1-8coverage> go test .\coverage --cover 
    ok      coverage/coverage       (cached)        coverage: 66.7% of statements
    
    # 66分和40分一起测的情况
    (base) PS \2-1-8coverage> go test .\coverage --cover 
    ok      coverage/coverage       0.212s  coverage: 100.0% of statements
    
    # 单独40分的情况
    (base) PS \2-1-8coverage> go test .\coverage\ --cover
    ok      coverage/coverage       0.224s  coverage: 66.7% of statements
    
  7. 然后可以把Judge函数里面的冗余代码解除注释重复上面的测试

    # 单独66分的情况
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-8coverage> go test .\coverage\ --cover
    ok      coverage/coverage       0.216s  coverage: 40.0% of statements
    
    # 66分和40分一起测的情况
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-8coverage> go test .\coverage\ --cover
    ok      coverage/coverage       0.223s  coverage: 100.0% of statements
    
    # 单独40分的情况
    (base) PS D:\code\MoFishXiaodui\ExecutableManual\src\2-1-8coverage> go test .\coverage\ --cover
    ok      coverage/coverage       0.227s  coverage: 80.0% of statements
    

案例9 - 单元测试 多个测试函数

  1. 案例8中的 2-1-8coverage 文件夹在同级复制一份,命名为 2-1-9coverage-more

  2. judge_test.go把原来的一个测试函数拆分成两个

    // 原来
    func TestJudge(t *testing.T) {
    	// score1 := Judge(66)
    	// assert.Equal(t, true, score1)
    
    	score2 := Judge(40)
    	assert.Equal(t, false, score2)
    }
    
    // 现在
    func TestJudgeTrue(t *testing.T) {
    	score1 := Judge(66)
    	assert.Equal(t, true, score1)
    }
    
    func TestJudgeFail(t *testing.T) {
    	score2 := Judge(40)
    	assert.Equal(t, false, score2)
    }
    
  3. 最终代码见src/2-1-9coverage-more/

  4. 案例8 的命令一样进行测试

    (base) 【...】\src\2-1-9coverage-more> go test .\coverage\ --cover
    ok      coverage/coverage       0.205s  coverage: 100.0% of statements
    

后续测试案例请查看第二篇文章:手把手教学-Go工程实践之测试 | 青训营 - 掘金 (juejin.cn)