Go工程实践

一.语言进阶

1.0并发VS并行

并发

多线程程序在一个核CPU运行

并发是指在一个时间段内多个程序同时处于已启动运行到运行完毕之间的状态。在操作系统中,并发是指同时执行多个程序,但任一时刻只有一个程序在处理机上运行。这种并发是通过分时复用技术实现的,每个程序都被分配一段时间片,然后依次轮流执行。

并行

多线程程序在一个核CPU运行

并行是指多个任务在同一时刻同时执行。在操作系统中,并行是指一组程序按独立异步的速度执行,不等于时间上的重叠(宏观上是同时,微观上仍是顺序执行)。这种并行可以通过多处理器系统或分布式系统实现,每个处理器或节点可以同时执行一个或多个任务。

1.1Goroutine

协程

协程:用户态,轻量级线程,栈KB 级别。

线程:内核态,线程跑多个协程,栈 MB 级别。

可以通过协程实现并发处理任务,提高程序的性能和效率。

并发本身来说,也可以配合并行进行

主流的语言各自又着自己的协程支持,并提供了不同的操作给开发者使用,Goroutine 是有栈协程

func hello(i int) {
	println("hello goroutine:" + fmt.Sprint(i))
}
func HelloGoRoutine() {
	for i := 0; i < 5; i++ {
		go func(j int) {
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)
}

 

在一个函数前加上go关键字就能为一个函数创建一个协程来运行

循环并不会等待打印操作执行完再创建下一个协程,而是直接进行下一个循环,立刻创建新协程,乱序输出

在程序中不使用一个go关键字,也存在一个协程运行(main)

go func(j int) {
			hello(j)
		}(i)

Go 的匿名函数语法:创建了一个未定义名称的函数,声明了一个 int 类型的形参 j,并在该函数的函数体内调用了 hello 函数。接下来在其后面添加 () 并传入实参 i 以直接调用这个匿名函数。

time.Sleep(time.Second)是一个函数,用于在程序中暂停指定的秒数。这种暂停可以让程序在执行其他任务时获得更好的性能,因为计算机的其他部分可以在程序暂停时继续运行。

在这个例子中,time.Second表示暂停一秒钟。这意味着程序将在一秒钟后继续运行。你可以在程序的任何位置使用time.Sleep(time.Second)来暂停程序,以便在执行其他任务时更加高效。

需要注意的是,time.Sleep(time.Second)的返回值为None。

1.2CSP

提倡通过通信共享内存实现通信

通过共享内存实现的数据通信,会遇到位置问题

1.3 Channel

make(chan 元素类型,[缓存大小])

无缓冲通道  make(chan int)

有缓冲通道  make(chan int,2) 两个缓存区

 

无缓冲通道就是同步通道,解决同部问题的方式就是使用有缓冲通道

无缓存 Channel :一个数据的发送必须等待另一端代码的接收,如果没有人接收发送的数据,那么发送端便会被永远阻塞。

有缓存 Channel :可以直接发送数据而不必收到阻塞,如果超出缓冲区,则依旧会被阻塞。

例子:

A 子协程发送0~9数字

B 子协程计算输入数字的平方

主协程输出最后的平方数

1.4并发安全Lock

package main

import "time"

var x int64

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

func main() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("WithoutLock:", x)
}

结果(多次运行):

WithoutLock: 10000

WithoutLock: 8000

理论上应该得到10000

但是运行结果却不同

这就是并发安全,修改一个变量需要经过取出变量值,修改取出的值,将值存回变量三个步骤,在这中间就会产生错误

需要并发锁 sync.Mutex解决这个问题

var (
    x    int64
    lock sync.Mutex
)
 
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x += 1
        lock.Unlock()
    }
}
 
func main() {
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    println("WithLock:", x)
}

原理:第一次调用Lock方法时,没有变化,但是第二次调用Lock方法时,会立刻阻塞协程,再程序调用 Unlock 方法解锁

1.5WaitGroup

WaitGroup是Go语言中的一种并发原语,用于解决并发等待问题。在实际使用Go协程实现并行应用时,可能会遇到需要阻塞部分代码执行,直到其他协程成功执行之后才继续执行的情况。这时可以使用WaitGroup来阻塞应用,直到所有协程都成功执行。

func hello(i int) {
    println("hello goroutine : " + fmt.Sprint(i))
}
 
func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}
 
func main() {
    HelloGoRoutine()
}

协程程序(1.1),HelloGoRoutine函数打印5个1-5数字,最后,对主协程进行休眠情况,确保子协程执行完成

在这里可以进行改进,wg.Wait()取代time.Sleep(time.Second)


func HelloGoRoutine() {
	var wg sync.WaitGroup // 声明名为 wg 的 WaitGroup 变量
	wg.Add(5)             //调用 WaitGroup 实例的 Add 方法,传入数字 5
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done() 
			//延迟(defer表示函数末尾执行,以先进后出的顺序执行)调用 WaitGroup 实例的 Done 方法
			hello(j)
		}(i)
	}
	wg.Wait() //调用 WaitGroup 实例的 Wait 方法
}

defer 关键字常用于资源释放等用处

WaitGroup 其实内部维护了一个计数器,工作步骤:

通过调用 Add 方法,向 WaitGroup 的计数器添加指定值;

通过调用 Wait 方法阻塞当前协程,这会使得协程陷入无限的等待;

通过调用 Done 方法使 WaitGroup 内部的计数器 -1,直到计数器值为 0 时,先前被阻塞的协程便会被释放,继续执行接下来的代码或是直接结束运行。

开启协程+1;执行结束-1;主协程阻塞直到计数器为0

二.依赖管理

2.0 背景

依赖指各种开发包,我们在开发项目中,需要学会站在巨人的肩膀上,也就是利用已经封装好的、经过验证的开发组件或工具来提升自己的研发效率。

对于hello world以及类似的单体四数只需要依赖原生 SDK,而实际工程会相对复杂,这样对依赖包的管理就显得尤为重要

2.1 Go依赖管理演进

对于 Go 的依赖管理来说,经历了 GOPATH,Go Vender,Go Module 三部分的演进。

  • 不同环境(项目)依赖的版本不同
  • 控制依赖库的版本

2.1.1 GOPATH

GOPATH 是一个环境变量,指向一个目录,作为项目的编译产出目录和依赖目录。这是一个公共环境变量,也就意味着,所有项目都依赖于同一个 GOPATH,Go 直接将依赖库源码扔进 GOPATH 的 src 文件夹以作为项目依赖。GOPATH 是 Go 语言的一个构建模式,它默认的路径是 $HOME/go。在 GOPATH 构建模式下,编译器会在 GOPATH 路径下搜索第三方模块,而不关心第三方包版本。

GOPATH-弊端

如果项目 A 依赖于依赖库 Lib 的版本 1,而项目 B 依赖于同一个依赖库的版本 2,由于 GOPATH 并没有任何版本管理措施,就会导致编译出错。为了解决依赖管理的问题,引入了 vendor 机制。但是,GOPATH 在多工程情况下支持不算友好,也无法对依赖包进行有效的版本管理。

go get下载最新版本的包到src目录下 

2.1.2 Go Vender

Go Vender 是 Go 语言的一个依赖管理工具,它使用 go get 下载依赖的方式简单暴力。虽然 go get 在 1.6 版本才正式支持了 vendor,Go 引入了 Go Vendor,通过在项目目录下新建 vendor 文件夹,并存放依赖库文件副本的方式,使得不同项目可以依赖不同的依赖库版本,解决了版本冲突的问题。如果无法在 vendor 文件夹中找到项目所需的依赖文件,那么 Go 会尝试回到 GOPATH 查找。

Go Vender-弊端

无法管理依赖版本:如果项目 A 引入了 项目 B 和项目 C 作为依赖库,而后两者又共同依赖了项目 D 的不同版本,那么由于 B,C,D 作为项目 A 的依赖依然被同时存在同一个 vendor 文件夹中,依旧导致了依赖冲突。

2.1.3 Go Mouble

Go Module 是 Go 语言的一个构建模式,通过环境变量 GO111MODULE 设置为 on 来打开 Go Module 构建模式。在 Go Module 构建模式下,通过 go mod init 创建 go.mod 文件,初始化项目,然后通过 go mod tidy 命令自动更新当前 module 的依赖信息,最后通过 go build 构建 module。Go Module 采用语义导入版本和最小版本选择等机制,使得依赖管理更加有效和灵活。

Go 为我们提供了 go get 和 go mod 两条指令来方便的添加和移除项目中的依赖,不需要像 Java 的 Maven/Gradle 那样手动编辑配置文件指定依赖。

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

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

2.2依赖管理三要素

依赖配置-go.mod

依赖配置-version

依赖配置-indorect

2.3.1 依赖配置-go.mod

module example/project/app
 
go 1.16/
 
require (	
    example/lib1 v1.0.2
    example/lib2 v1.0.0 // indirect
    example/lib3 v0.1.0-20190725025543-5a5fe074e612
    example/lib4 0.0.0-20180306012644-bacd9c7efldd // indirect
    example/lib5/v3 v3.0.2
    example/lib6 v3.2.0+incompatible
)

module example/project/app//依赖管理的基本单元

go 1.16//原生库(1.16版本)

require 内则指定了单元依赖,格式是 [Module Path] [Version/Pseudo-version]

2.3.2 依赖配置-version

版本号应当按照语义化版本(MAJOR.MINOR.PATCH)的格式填入,例如 v1.0.2;

或者,我们可以填入一个基于 commit 的伪版本,代表我们需要来自某个 commit 的依赖库版本,它的格式是 vx.0.0-yyyymmddhhmmss-abcdefgh1234,其中 yyyymmddhhmmss 是提交 Commit 的时间,而 abcdefgh1234 则是该 commit 的哈希值。

2.3.3 依赖配置-indorect

有的依赖可能会使用 // indirect 注释标识,这意味着该依赖并非由项目直接引入,而是透过其他依赖间接引入(例如 A 项目引入了 B 依赖,B 依赖依赖于依赖 C,那么依赖 C 就是 项目 A 的间接依赖)

2.3.4 依赖配置-incompatible

有的依赖可能会在版本末尾添加 +incompatible 标识,这是为了兼容非语义化版本所致。

2.3.5 依赖分发-回源

依赖不同的依赖库,来源于不同的代码托管网站,例如 GitHub,GitLab,这些代码托管网站可能无法接受来自数以百万计的 Go 开发者的依赖拉取请求。

依赖分发-Proxy

Go 引入了 Proxy 系统,通过指定 Proxy 服务器,优先从 Proxy 服务器拉取依赖,这不仅减轻了源站负担,也保证了依赖的可用性,避免依赖库开发者删库跑路。

2.3.6 依赖分发-变量GOPROXY

可以通过指定 GOPROXY 环境变量的方式指定 Proxy 服务器,其值为一个由逗号分隔的网址列表,例如 "https://proxy1.cn, https://proxy2.cn, direct"。当需要拉取依赖时,Go 便会按顺序从依赖服务器拉取代码,如果找不到指定的依赖,那么就前往下一个依赖服务器拉取,直到前往源站(即 direct)拉取代码。

国内:

Goproxy.cn

https://goproxy.io

2.3.7 工具-go get

go get 是一个命令行指令,可用于添加和移除依赖,在项目目录执行它以为项目配置依赖:

基本语法:go get example.org/pkg????

其中,example.org/pkg 是所需依赖的仓库地址,???? 可以取以下值:

@update,缺省值(不填写时使用的值),使用最新版本

@none,删除项目中的此依赖

@v1.1.2,使用此版本的依赖

@23dfdd5,使用特定的 commit

@master,使用指定分支的最新 commit

go install 指令取代go get 指令用于安装二进制可执行程序

2.3.8 工具-go mod

go mod 是一个命令行指令,可用于初始化项目和管理依赖,在项目目录执行它以为项目配置依赖:

  • go mod init,初始化项目,这将创建 go.mod 文件,类似于 npm 的 npm init
  • go mod download,下载模块到本地缓存
  • go mod tidy,添加需要的依赖,删除不需要的依赖(有点类似于 apt autoremove)

依赖管理三要素

  1. 配置文件,描述依赖  go.mod
  2. 中心仓库管理依赖库  Proxy
  3. 本地工具 go get/mod

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值