go语言进阶学习笔记 | 青训营笔记

88 阅读1分钟

go语言进阶

并发编程

创建协程

创建协程的方法:在函数前面使用关键字go即可,语法如下:

go 函数名(实参列表)

也可通过创建匿名函数的方法语法如下:

go func(函数形参列表){

// 协程执行的内容

}(协程实参)

比如下面快速打印hello goroutine的例子:

func main() {
    helloGoRutine()
}
​
func hello(i int) {
    fmt.Println("hello routine:" + fmt.Sprint(i))
}
​
func helloGoRutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i) // 这里是传入j的参数
    }
    time.Sleep(time.Second)
}

协程通信

channel

通过channel实现协程之间的通信

channel通过make关键字来创建,语法如下:

变量名:=make(chan 元素类型,[缓冲大小])

可以使用range来遍历channel

func helloChannel() {
    src := make(chan int)
    dest := make(chan int, 3)
    go func() {
        // 执行完协程后关闭channel
        defer close(src)
        for i := 0; i < 5; i++ {
            src <- i
        }
    }()
    go func() {
        // 执行完协程后关闭channel
        defer close(dest)
        for i := range src {
            dest <- i * i
        }
    }()
    for i := range dest {
        fmt.Println(i)
    }
}

共享内存

所谓的共享内存的方式就是多个协程共用一个内存单元,当多个协程同时访问一个内存单元的时候,在不加锁的时候,会出现的情况是协程同时拿到了内存资源,并都对其做了修改,但彼此之间是隔离的,也就是说并不知道其他协程作了修改,造成不同步问题。解决的方法就是在协程对共享内存操作时先加锁,保证在自己修改的过程中,别人无法修改。

var (
    x    int
    lock sync.Mutex
)
​
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}
func addWithoutLock() {
    for i := 0; i < 2000; i++ {
        x += 1
    }
}
func add() {
    x = 0
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
    time.Sleep(time.Second)
    println("不加锁是:", x)
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    println("加锁是:", x)
​
}

waitGroup

waitgroup是一个同步原语,用于等待一组协程执行完毕。

WaitGroup 主要有三个方法:

  • Add(delta int):向内部计数器添加 delta,表示等待多少个协程完成;
  • Done():从内部计数器减去一个计数值,表示有一个协程完成了工作;
  • Wait():在内部计数器归零之前一直阻塞,表示等待所有协程完成。

开启协程之前先调add方法,设置计数器初始值

然后在协程内调用done让数值-1

最后调用wait阻塞

func waitGroupTest() {
    var wg sync.WaitGroup
    //设置计数器,值为5,就是可以有5个子协程
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            defer wg.Done()
            hello(j)
        }(i)
    }
    wg.Wait()
}

依赖管理

依赖管理的演进,目前主流是go module

go module通过go.mod文件管理依赖包版本

通过go get/go mod 指令工具管理依赖包

依赖管理三要素:

  • 配置文件,描述依赖

    go.mod

  • 中心仓库管理依赖库

    Proxy

  • 本地工具

    go get/mod

go.mod的使用

1.创建go.mod文件

在项目的根目录下执行以下命令来创建一个go.mod文件:

go mod init example.com/hello

其中 example.com/hello 是项目的唯一标识符,可以是任意的字符串,通常会使用项目的仓库地址或者域名。

执行该命令之后,会创建一个 go.mod 文件,其中记录了模块的名称和 Go 版本等信息。同时,依赖包的版本信息也应该在该文件中添加

2.管理依赖包

go.mod 文件中,可以使用 require 来指定依赖包的名称和版本,如下所示:

module example.com/hello
​
go 1.17
​
require (
    github.com/gin-gonic/gin v1.7.4
    github.com/go-sql-driver/mysql v1.6.0
)

上面的示例中,我们使用 require 指定了项目引用的两个依赖包:gin-gonic/gingo-sql-driver/mysql,分别使用了不同的版本号。

除了手动指定依赖包和版本之外,还可以使用 go get 命令快速自动拉取依赖包:

go get -u github.com/gin-gonic/gin

该命令会将 gin-gonic/gin 包拉取到 $GOPATH/pkg/mod/github.com/gin-gonic/gin@v1.7.4 目录下,并在 go.mod 中自动添加依赖记录。

也可以用于更新依赖包版本。

测试

单元测试

单元测试规则

  • 所有测试文件以 _test.go结尾
  • 测试函数定义为 func TestXxx(*testing.T)
  • 初始化逻辑放在TestMain函数中

单元测试例子

需要测试的函数:

func HelloTom() string {
    return "jerry"
}

测试函数:

func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    if output != expectOutput {
        t.Errorf("期望输出是%s,实际输出是%s", expectOutput, output)
    }
}

也可使用assert的Equal方法对期望值与实际值比较,代码可改成:

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	assert.Equal(t,expectOutput,output)
}

覆盖率

通俗理解是指测试时运行到的代码占所有测试代码的百分之多少

Mock测试

由于有些待测试函数依赖于外界,所以测试时可以把原函数进行打桩,其实就是用B函数替换A函数,叫做A函数打桩,比如:

func ReadFirstLine() string {
    open, err := os.Open("D:\GoCode\test\test.txt")
    defer open.Close()
    if err != nil {
        return ""
    }
    scanner := bufio.NewScanner(open)
    for scanner.Scan() {
        return scanner.Text()
    }
    return ""
}
func ProcessFirstLine() string {
    line := ReadFirstLine()
    destLine := strings.ReplaceAll(line, "11", "00")
    return destLine
}

测试ProcessFirstLine有没有替换成功:

func TestProcessFirstLine(t *testing.T) {
	firstLine := ProcessFirstLine()
	assert.Equal(t, "line00", firstLine)
}

此时的ReadFirstLine依赖于文件的输入,因此可以对ReadFirstLine函数打桩:

注:使用第三方库monkey:github.com/bouk/monkey

func TestProcessFirstLineWithMock(t *testing.T) {
    monkey.Patch(ReadFirstLine(), func() string { return "line110" })
    defer monkey.Unpatch(ReadFirstLine())
    line := ProcessFirstLine()
    assert.Equal(t, "line000", line)
}