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/gin 和 go-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)
}