Golang工程实践


1.并发编程

1.1 关于并发和并行

并发:并发通常指的是在一个CPU下,多个进程任务通过时间片的切换来在CPU上进行调度的一种模式。多线程程序在一个核上的CPU运行
并行:多个进程任务同时进行,这个同时是真正意义上的同时,也就是两个进程任务同时进行,而不是通过时间片轮转抢占CPU的方式来进行的。多线程的程序在多个核的CPU上运行

1.2 协程

协程,英文Coroutines,是一种比线程更加轻量级的存在。正如进程可以拥有多个线程调度一样,线程也可以拥有多个协程,最重要的是,在系统级线程的机制下,线程的切换和调度都是在内核态下进行的,一旦切换到内核态,开销就会相当巨大,协程不是被操作系统所管理,而是完全由程序所控制(在用户态下执行),这样做的好处就是性能得到了很大的提升。

1.3 CSP(Communicating Sequential Process)

在golang中,提倡使用channel的通信共享内存来实现通信,而不是共享内存而实现通信

1.4 Channel

channel1 := make(chan int, 5) //有缓冲的信道定义
channel2 := make(chan int)    //无缓冲的信道定义

有缓冲通道和无缓冲通道的区别:无缓冲通道实际上会导致两个进程之间的通信同步化,也就是传多少,接受多少,那么有缓冲通道的话就可以实现异步化,假设业务场景是需要上传一个大容量的文件,假设没有缓冲区,那么用户需要一直等待,直到接受的进程接受文件完毕才能关闭发送进程。假设有缓冲区,那么发送进程就可以提前把大容量的文件先发送到缓冲通道中,然后缓冲通道再把文件发送到接受进程,这个过程就是异步。
channel使用示例

package main

import "fmt"

func CalSquare() {
	src := make(chan int)
	dst := make(chan int, 3) //定义缓冲区为10
	go func() {              //向生产者送数
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()

	go func() { //消费者用数
		defer close(dst) //在该函数结束后,自动关闭该信道
		for i := range src {
			dst <- i * i
		}
	}() //匿名函数,这个括号代表对函数进行调用
	for i := range dst {
		fmt.Println(i)
	}
}

func main() {
	CalSquare()
}

1.5 并发安全 Lock

简单来说就是解决临界区互斥的问题

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	x    int64      //临界资源
	lock sync.Mutex //锁
) //这个变量是全局的,任意线程均可以改变

func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock() //互斥
		x++
		lock.Unlock()
	}
}

func addWithoutLock() {
	for i := 0; i < 2000; i++ {
		x++
	}
}

func testAdd() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	fmt.Println("如果没有对临界区加锁,计算得到的x是:", x)
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	fmt.Println("如果对临界区加锁,计算得到的x是:", x)
}

func main() {
	testAdd()
}

1.6 WaitGroup 实现阻塞

上述实现阻塞的方法是不合理的,因为它是通过time.sleep强制进程进入阻塞状态的,而阻塞的时间是人为规定的
这个例子的需求只是阻塞主协程,子协程的执行顺序随意

package main

import (
	"fmt"
	"sync"
)

func hello(j int) {
	fmt.Println(j)
}

func implFastAdd() {
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(j int) {
			defer wg.Done() //完成一个协程的工作后 mutex -= 1
			hello(j)
		}(i)
	}
	wg.Wait() //主协程并发执行到这,检测mutex是否为0,不为0才继续向下执行,否则阻塞
	fmt.Println("执行完毕!")
}

func main() {
	implFastAdd()
}

2. 依赖管理

2.1 依赖管理三要素

  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 go get/mod
    可以类比与JAVA的Maven

2.2.1 依赖配置-version

语义化版本
${MAJOR}.${MINOR}.${PATCH}
MAJOR是代码隔离的部分,是不相兼容的,MINOR是新增的函数等,PTACH是分支
如 v1.3.0 v2.3.0
基于commit的伪版本
如vx.0.0-yymmddhhmmss-abcdefg

2.2.2 依赖配置-indirect

对于非直接依赖项,用//indirect来标识出该项是非直接依赖的

2.2.3 依赖配置-incompatible

主版本在2+以上的模块会在模块路径增加/vN后缀
对于没有go.mod文件并且主版本2+的依赖,会+incompatible

2.2.4 依赖图

如果X项目依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3、v1.4两个版本,最终编译时所使用的C项目的版本为如下哪个选项?
答案是1.4,因为要选最低的兼容版本,我们首先知道1.3和1.4是前后兼容的,而B只能兼容1.4,A只能兼容1.3,因此最低兼容版本为1.4

2.3.1 依赖分发-回源

其实就是依赖的代码模块从哪下载的问题,一般都存在一个代码仓库,托管相关代码在平台上,在这些平台上,存在一些问题

  • 无法保证构建的稳定性
    代码作者可以在平台上增加/删除代码,很有可能因为代码作者对代码的改动而无法构建代码
  • 无法保证依赖的可用性
    与代码作者有关
  • 增加第三方压力
    代码托管平台压力负载的问题

2.3.2 依赖分发-GOPROXY

为了解决上述问题,用了go Proxy机制
go Proxy提供了一个缓冲库,该缓冲库中保存、下载了代码,最终就可以直接从go Proxy中取依赖
一般定义为

GOPROXY = "https://proxy1.cn,https://proxy2.cn,direct"
 //服务站点url,"direct"表示源站,当上述所有的url都无法连接时,就会回到第三方代码托管平台去下载代码

2.4 goMod工具

go get

go get example.org/pkg @update //默认抓取最新版本
					   @none   //删除该依赖
					   @v1.1.2 //tag版本,语义版本
					   @23dfdd5 //特定的commit
					   @master //分支的最新版本

go mod

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

3.测试

3.1 单元测试

3.1.1 测试命名规则

  • 所有测试文件以_test结尾
  • func TestXxx(t *testing T)
  • 初始化逻辑放到TestMain中

3.1.2 单元测试实例

package main

import "testing"

func HelloTom() string {
	return "Jerry"
}

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	if output != expectOutput {
		t.Errorf("Error!%s dont match %s", output, expectOutput)
	}
}

func main() {

}

3.2 单元测试-Mock

快速Mock函数

  • 为一个函数打桩
  • 为一个方法打桩
    打桩:实际上就是为方法/函数安装一个别名函数以此来避免对文件/网络/缓存的强依赖。

文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录