GO_高阶
interface 接口
在 Go 中接口是一种抽象类型,是一组方法的集合,里面只声明方法,而没有任何数据成员。
而在 Go 中实现一个接口也不需要显式的声明,只需要其他类型实现了接口中所有的方法,就是实现了这个接口。
-
语法
-
type <interface_name> interface { <method_name>(<method_params>) [<return_type>...] ... }
-
-
注意
- Go 中接口声明的方法并不要求需要全部公开
- 接口可以嵌套
- 直接用接口类型作为变量时,如果方法都是值接收者,可以用值或指针。如果任何方法是指针接收者,则必须用指针。
- 如果函数参数使用 interface{}可以接受任何类型的实参。同样,可以接收任何类型的值也可以赋值给 interface{}类型的变量
-
package main import "fmt" // PaymentMethod 接口定义了支付方法的基本操作 type PayMethod interface { Account Pay(amount int) bool } type Account interface { GetBalance() int } // CreditCard 结构体实现 PaymentMethod 接口 type CreditCard struct { balance int limit int } func (c *CreditCard) Pay(amount int) bool { if c.balance+amount <= c.limit { c.balance += amount fmt.Printf("信用卡支付成功: %d\n", amount) return true } fmt.Println("信用卡支付失败: 超出额度") return false } func (c *CreditCard) GetBalance() int { return c.balance } // DebitCard 结构体实现 PaymentMethod 接口 type DebitCard struct { balance int } func (d *DebitCard) Pay(amount int) bool { if d.balance >= amount { d.balance -= amount fmt.Printf("借记卡支付成功: %d\n", amount) return true } fmt.Println("借记卡支付失败: 余额不足") return false } func (d *DebitCard) GetBalance() int { return d.balance } // 使用 PaymentMethod 接口的函数 func purchaseItem(p PayMethod, price int) { if p.Pay(price) { fmt.Printf("购买成功,剩余余额: %d\n", p.GetBalance()) } else { fmt.Println("购买失败") } } func main() { creditCard := &CreditCard{balance: 0, limit: 1000} debitCard := &DebitCard{balance: 500} fmt.Println("使用信用卡购买:") purchaseItem(creditCard, 800) fmt.Println("\n使用借记卡购买:") purchaseItem(debitCard, 300) fmt.Println("\n再次使用借记卡购买:") purchaseItem(debitCard, 300) }
-
并发-goroutine 与 channel
go 支持并发的方式,就是通过 goroutine 和 channel 提供的简洁且高效的方式实现的。
-
goroutine
goroutine 是轻量线程,创建一个 goroutine 所需的资源开销很小,所以可以创建非常多的 goroutine 来并发工作。
它们是由 Go 运行时调度的。调度过程就是 Go 运行时把 goroutine 任务分配给 CPU 执行的过程。但是 goroutine 不是通常理解的线程,线程是操作系统调度的。
在 Go 中,想让某个任务并发或者异步执行,只需把任务封装为一个函数或闭包,交给 goroutine 执行即可。
-
声明
-
声明方式 1,把方法或函数交给 goroutine 执行: go <method_name>(<method_params>...)
声明方式 2,把闭包交给 goroutine 执行: go func(<method_params>...){ <statement_or_expression> ... }(<params>...)
-
-
-
示例
-
-
-
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go func() { fmt.Println("run goroutine in closure") }() go func(string) { }("gorouine: closure params") go say("in goroutine: world") say("hello") }
-
-
同步原语
提到并发编程、多线程编程时,我们往往都离不开『锁』这一概念,Go 语言也为开发者提供这一功能,锁的主要作用就是保证多个线程或者 Goroutine 在访问同一片内存时不会出现混乱的问题,锁其实是一种并发编程中的同步原语(Synchronization Primitives)。
在这一节中我们就会介绍 Go 语言中常见的同步原语 Mutex、RWMutex、WaitGroup、Once 和 Cond 以及扩展原语 ErrGroup、Semaphore和 SingleFlight 的实现原理,同时也会涉及互斥锁、信号量等并发编程中的常见概念。
-
-
sync.Mutex - 互斥锁
-
一份互斥锁对一个资源加锁,只能同时被一个 goroutine 锁定,其他 goroutine 阻塞等待资源释放。
Go 语言中的互斥锁在 sync 包中,它由两个字段 state 和 sema 组成,state 表示当前互斥锁的状态,而 sema 真正用于控制锁状态的信号量,这两个加起来只占 8 个字节空间的结构体就表示了 Go 语言中的互斥锁。
-
-
-
锁结构
-
type Mutex struct { state int32 sema uint32 }
-
state:互斥锁的当前状态
mutexLocked — 表示互斥锁的锁定状态;
mutexWoken — 表示从正常模式被从唤醒;
mutexStarving — 当前的互斥锁进入饥饿状态;
waitersCount — 当前互斥锁上等待的 Goroutine 个数; -
sema:控制锁状态
正常模式 - 锁的等待者会按照先进先出的顺序获取资源。
刚被唤起的 goroutine 与新建的 goroutine 竞争资源的时,大概率竞争失败而无法获取锁资源,所以若 goroutine 超过 1ms 没有获取到锁,互斥锁会自动被切换成饥饿模式,防止 goroutine 没有资源。
饥饿模式 - 互斥锁会直接将资源交给等待队列最前的 goroutine,新创建的 goroutine 会被至于等待最尾端。目的是为了确保互斥锁的公平性。
若一个 goroutine 获得了互斥锁,并且它在队列尾端,或者等待的时间少于 1ms ,则互斥锁会自动切换成正常模式
饥饿模式可以有效的避免 goroutine 陷入由于等待无法获取锁的高危延时。
-
-
-
-
-
-
示例
-
sum:= 0 mutex := sync.Mutex func add(i int) { mutex.Lock() defer mutex.Unlock() //采用defer 语句释放锁。 sum += i } // 被解锁保护的sum += i 代码片段又称为临界区。在同步原语设计中,临界区指的是一个访问共享资源的代码片段,而这些共享资源又有无法同时被多个协程访问的特征。当有协程进入临界区段时,其它协程必须等待,这样就保证了临界区的并发安全。 func main() { for i := 0; i < 100; i ++ { go add(10) } for i := 1; i <10 ; i++ { go fmt.Println("和为:", getSum()) } time.Sleep(2 * time.Second) } func getSum() int { mutex.Lock() defer mutex.Unlock() b := sum return b }
-
-
-
-
-
sync.RWMutex
-
因为add 和 getSum函数使用的是同一个sync.Muetx,所以它们的操作是互斥的,也就是一个goroutine进行修改操作时,另一个goroutine读取操作会等待,直到修改操作完毕。
我们解决了goroutine同时读写的资源竞争问题,但是又遇到了另一个问题——性能。因为每次读写共享资源都要加锁,所以性能底下,如何解决呢?
所以可以通过sync.RWMutex 来优化此段代码,提升性能。改用读写锁,来实现我们想要的结果
-
-
-
语法
-
-
-
-
-
-
var mutex snyx.RWMutex func getSum() int { // 获取读写锁 mutex.RLock() defer mutex.RUnlock() b := sum return b } // 这样性能会有很大提升,因为多个goroutine可以同时读取数据,不再相互等待。
-
-
-
sync.WaitGroup
-
我们上述的示例中使用了time.Sleep函数, 是为了防止主函数main() 返回,一旦main函数范湖了, 程序也就退出了。 提示:一个函数或方法的返回(return) 也就意味着当前函数执行完毕。
如我们我们执行100个add协程和10个getSum协程,不知道什么时候执行完毕,所以设置了比较长的等待时间,但是存在一个问题,如果这110个协程很快就执行完毕,main函数应该提前返回,但是还要等待一会才能返回,会产生性能问题。但是如果等待时间截止时,协程没有执行完毕,程序会提前退出,导致有协程没有执行完,产生不可预知的结果。
如何解决这个问题呢? 有没有办法监听所有协程的执行,一旦全部执行完毕,程序马上退出,这样既可保证所有协程执行完毕,又可以及时退出节省时间,提升性能。Channel可以解决这个问题,不过很复杂。Go语言提供了更简洁的方法,它就是 sync.WaitGroup。
-
-
-
语法
-
sync.WaitGroup使用比较简单,一共分为三步:
-
声明一个sync.WaitGroup,然后通过Add方法设置计数器的值,需要跟踪多少个协程就设置多少
-
在每个协程执行完毕时调用Done方法,让计数器减1,告诉sync.WaiGroup 该协程已经执行完毕
-
最后调用Wait方法一直等待,直到计数器值为0,也就是所有跟踪的协程都执行完毕
-
-
-
-
通过sync.WaitGroup可以很好地跟踪协程。在协程执行完毕后,整个函数才会执行完毕,时间不多不少,正好是协程执行的时间。
sync.WaitGroup适合协调多个协程共同做一件事情的场景,比如下载一个文件,假设使用10个协程,每个协程写在1/10,只有10个协程都下载好了整个文件才下载好了。这就是我们经常听说的多线程下载,通过多个线程共同做一件事情,显著提高效率。小提示我们可以把Go语言的协程理解为平常说的线程,从用户体验上并无不可,但是从技术实现上,它们是不一样的。
-
-
-
示例
-
func run() { var wg sync.WaitGroup wg.Add(110) for i := 0; i< 100; i ++ { go func() { // 计数器减1 defer wg.Done() add(10) }() } for i := 1; i < 10 ; i++ { go func() { defer wg.Done() fmt.Println("sum is", getSum()) }() } // 一直等待,直到计数器值为0 wg.Wait() }
-
-
-
sync.Once
在实际工作中,可能会有这样的需求:让代码只执行一次,哪怕是高并发情况下,比如创建一个单例。针对这种情形,Go语言为我们提供了sync.Once来保证代码只执行一次。
这是Go语言自带的一个示例,虽然启动了10个协程来执行onceBody函数,但是因为用了once.Do方法,所以函数onceBody只会执行一次。也就是在高并发的情况下,sync.Once也会保证onceBody函数只执行一次。
sync.Once 适用于创建某个对象的单例、只加载一次的资源等只执行一次的场景。
-
func main() { doOnce() } func doOnce() { var once sync.Once onceBody := func() { fmt.Println("Only once") } // 用于等待协程执行完毕 done := make(chan bool) // 启动10个协程执行once.Do(onceBody) for i := 1; i < 10; i++ { go func(){ go func() { // 把要执行的函数(方法)作为参数传给once.Do方法即可 once.Do(onceBody) done <- true } }() } for i := 10; i < 10 ; i++{ <- done } }
-
-
sync.Cond
在Go语言中,sync.WaitGroup用于最终完成的场景,关键点在于一定要等待所有协程都执行完毕。而sync.Cond 可以用于发号施令,一声令下所有协程都可以开始执行,关键点在于协程开始的时候是等待的,要等待sync.Cond唤醒才能执行。
sync.Cond从字面意思看是条件变量,它具有阻塞协程和唤醒协程的功能,所有可以在满足一定条件的情况下唤醒协程,但条件变量只是它的一种使用场景。
-
// 10个人赛跑,1人裁判发号施令 func race() { cond := sync.NewCond(&sync.Mutex{}) var wg sync.WaitGroup wg.Add(11) for i := 1; i < 10; i++ { go func(num int) { defer wg.Done() fmt.Println(num, "号已就位") cond.L.Lock() cond.Wait() // 等待发令枪响 fmt.Println(num, "号开始跑") cond.L.Unlock() }(i) } time.Sleep(2 * time.Second) go func(){ defer wg.Done() fmt.Println("裁判已就位,准备发令枪") fmt.Println("比赛开始,大家开始跑") cond.Broadcast() // 发令枪响 }() wg.Wait() }
-
大概步骤:
-
通过sync.NewCond函数生成一个*sync.Cond,用于阻塞和唤醒协程
-
然后启动10个协程模拟10个人,准备就位后调用cond.Wait()方法阻塞当前协程等待发令枪响,这里需要注意的是调用cond.Wait()方法是要加锁
-
time.Sleep 用于等待所有人都进入wait状态,这样裁判才能调用cond.Broadcast()发号施令
-
裁判准备完毕后,可以调用cond.Broadcast通知所有人开始跑了
-
-
sync.Cond 有三个方法,分别是:
-
Wait,阻塞当前协程,知道被其他协程调用Broadcast 或者 Signal 方法唤醒,使用的时候需要加锁,使用sync.Cond中的锁即可,也就是L字段
-
**Signal **,唤醒一个等待时间最长的协程
-
Broadcast,唤醒所有等待的协程
-
-
注意:在调用signal 或者Broadcast之前,要确保目标协程处于Wait阻塞状态,不然会出现死锁问题。
-
-
-
channel
channel 是 Go 中定义的一种类型,专门用来在多个 goroutine 之间通信的线程安全的数据结构。
可以在一个 goroutine 中向一个 channel 中发送数据,从另外一个 goroutine 中接收数据。
channel 类似队列,满足先进先出原则。
-
定义
-
// 仅声明 var <channel_name> chan <type_name> // 初始化 <channel_name> := make(chan <type_name>) // 初始化有缓冲的channel <channel_name> := make(chan <type_name>, 3)
-
-
channel 的三种操作:发送数据,接收数据,以及关闭通道。
-
// 发送数据 channel_name <- variable_name_or_value // 接收数据 value_name, ok_flag := <- channel_name value_name := <- channel_name // 关闭channel close(channel_name)
-
-
channel 还有两个变种
-
可以把 channel 作为参数传递时,限制 channel 在函数或方法中能够执行的操作。
-
-
//仅发送数据 func <method_name>(<channel_name> chan <- <type>) //仅接收数据 func <method_name>(<channel_name> <-chan <type>)
-
-
-
示例
-
-
-
package main import ( "fmt" "time" ) // 只接收channel的函数 func receiveOnly(ch <-chan int) { for v := range ch { fmt.Printf("接收到: %d\n", v) } } // 只发送channel的函数 func sendOnly(ch chan<- int) { for i := 0; i < 5; i++ { ch <- i fmt.Printf("发送: %d\n", i) } close(ch) } func main() { // 创建一个带缓冲的channel ch := make(chan int, 3) // 启动发送goroutine go sendOnly(ch) // 启动接收goroutine go receiveOnly(ch) // 使用select进行多路复用 timeout := time.After(2 * time.Second) for { select { case v, ok := <-ch: if !ok { fmt.Println("Channel已关闭") return } fmt.Printf("主goroutine接收到: %d\n", v) case <-timeout: fmt.Println("操作超时") return default: fmt.Println("没有数据,等待中...") time.Sleep(500 * time.Millisecond) } } }
-
-
锁与 channel
在 Go 中,当需要 goroutine 之间协作地方,更常见的方式是使用 channel,而不是 sync 包中的 Mutex 或 RWMutex 的互斥锁。但其实它们各有侧重。大部分时候,流程是根据数据驱动的,channel 会被使用得更频繁。
-
-
channel 擅长的是数据流动的场景:
-
- 传递数据的所有权,即把某个数据发送给其他协程。
- 分发任务,每个任务都是一个数据。
- 交流异步结果,结果是一个数据。
-
-
而锁使用的场景更偏向同一时间只给一个协程访问数据的权限:
-
- 访问缓存
- 管理状态
select
select 语义是和 channel 绑定在一起使用的,select 可以实现从多个 channel 收发数据,可以使得一个 goroutine 就可以处理多个 channel 的通信。
语法上和 switch 类似,有 case 分支和 default 分支,只不过 select 的每个 case 后面跟的是 channel 的收发操作。
-
定义
-
select { case channel_name <- varaible_name_or_value: do sth case value_name, ok_flag := <- channel_name: do sth default: do sth }语法上和switch的一些区别:
- select 关键字和后面的
{之间,不能有表达式或者语句。 - 每个 case 关键字后面跟的必须是 channel 的发送或者接收操作
- 允许多个 case 分支使用相同的 channel,case 分支后的语句甚至可以重复
- select 关键字和后面的
-
-
示例
-
package main import ( "fmt" ) func main() { ch1 := make(chan int, 10) ch2 := make(chan int, 10) ch3 := make(chan int, 10) go func() { for i := 0; i < 10; i++ { ch1 <- i ch2 <- i ch3 <- i } }() for i := 0; i < 10; i++ { select { case x := <-ch1: fmt.Printf("receive %d from channel 1\n", x) case y := <-ch2: fmt.Printf("receive %d from channel 2\n", y) case z := <-ch3: fmt.Printf("receive %d from channel 3\n", z) } } }
-
在执行 select 语句的时候,如果当下那个时间点没有一个 case 满足条件,就会走 default 分支。
-
至多只能有一个 default 分支。
-
如果没有 default 分支,select 语句就会阻塞,直到某一个 case 满足条件。
-
如果 select 里任何 case 和 default 分支都没有,就会一直阻塞。
-
如果多个 case 同时满足,select 会随机选一个 case 执行。
-
编译可执行文件
-
无参编译
-
如果声明 main 函数的文件就在项目目录下,只需要在项目根目录下执行 go build 命令:
go buildGo 就会根据当前目录下声明的 go.mod 文件中,声明的 module 名称,把当前项目的执行文件输出到 module 名称同名的文件中。
-
-
-
如果想要修改路径和编译出来的可执行文件的名称,还需要添加
-o参数:go build -o <output_path>
-
-
- 另外,Go 的工程标准,一般会把声明 main 函数的文件放到项目目录的
cmd目录下,所以需要再加上这个文件的路径:
- 另外,Go 的工程标准,一般会把声明 main 函数的文件放到项目目录的
go build -o <output_path_with_file_name> <go_file_path>
-
交叉编译
-
而为了让程序在不同平台下正常运行,Go 还让 build 命令在执行时,可以读到环境变量中的一些参数以及 tags 参数。
其中两比较重要的变量分别是:
- GOOS:指定运行操作系统,目前支持
linux、windows和darwin(MacOS),三种类型。 - GOARCH:指定运行的架构,可以配置
amd64,arm64,i386等。 - CGO_ENABLED:如果项目依赖于另外的 C 库或其他工具,就需要在交叉编译时配置
CGO_ENABLED变量,默认情况下是 0,也就是关闭,当为 1 时,表示开启。
当需要使用到这些变量时,可以配置成环境变量,也可以直接在命令中定义,即可开始编译:
//编译 Linux 64位 GOOS=linux GOARCH=amd64 go build -o demo-amd64 main.go //编译 Windows 64位 GOOS=windows GOARCH=amd64 go build -o demo-windows-amd64.exe main.go //编译 macOS 64位 GOOS=darwin GOARCH=amd64 go build -o demo-darwin-amd64 main.go //编译 Linux 32位 GOOS=linux GOARCH=i386 go build -o myapp-linux-386 main.go
- GOOS:指定运行操作系统,目前支持
-
-
条件编译
-
概念
-
有时候在不同架构或系统下,由于各种各样的原因,不同系统提供的函数是不同的,那么在为了保证在不同系统下,程序能够正常运行。又或者公司的项目,为了区分收费版与免费版的程序,也可以使用条件编译的方式,让程序能够在一个项目下,编译出不同行为的可执行文件。
-
-
示例
- 创建 func_linux.go 文件,以下是文件代码
-
// +build linux package main import "fmt" func platformSpecificFunction() { fmt.Println("This is the Linux implementation.") }
-
-
-
- 创建 func_windows.go 文件:
-
// +build windows package main import "fmt" func platformSpecificFunction() { fmt.Println("This is the Windows implementation.") }
-
创建 func_darwin.go 文件:
-
// +build darwin package main import "fmt" func platformSpecificFunction() { fmt.Println("This is the Darwin implementation.") }
-
在 go1.17 版本以上,上面这几个文件的代码会被 IDE 自动添加一行代码://go:build xxx,这是 go 新引入的条件编译的格式,是为了替换 + build。
在 main.go 中,可以直接调用 platformSpecificFunction。
-
-
-
package main func main() { platformSpecificFunction() }
-
-
可以直接使用 go build 命令编译并执行。
这里添加 tags 参数,是因为 linux、windows 以及 darwin 这三个 tag 比较特殊,会根据 GOARCH 自动添加。
假如把 tag 名称改成其他的值,就需要在编译时特意指定,定义格式:
-
-
-
-
go build -tags="<tag1> <tag2> <...>"
-
-
-
在一个包中使用多个标签时,它们会使用布尔逻辑进行交互,具体取决于我们如何进行声明。
Build tags 遵循以下三个规则:
-
-
以空格分隔的标签将在 OR 逻辑下进行解释。
-
+build tag1 tag2
-
-
当编译时的 tags 参数只要声明了 tag1、tag2 这两个 tag 值任意一个,则编译时包含此文件
-
-
逗号分隔的标签将在 AND 逻辑下进行解释。
-
+build tag1, tag2
或者// +build tag1 // +build tag2当编译时的
tags参数需要同时声明 tag1 和 tag2 时,这个 go 文件才能包含在编译中。
-
-
每个术语都是一个字母数字单词,如果前面有! 它意味着它被否定。
-
// +build !tag1
当编译时的
tags参数没有tag1的值时,这个 go 文件才会 build 编译。
-
-
新版为了让 tag 之间的逻辑关系更加明显且容易辨认与编写, 简化了 tag 声明,并且为了与老版 tag 声明区分,修改的声明格式:
-
-
-
// go:build linux && amd64 || darwin
从
+ build变成了go:build,并且逻辑关系也更加明显,直接使用逻辑运算符来声明 tag 之间的逻辑关系。目前当声明旧的条件编译条件时,go 的开发环境工具会帮我们自动生成新版 tag 的声明。
-
-

浙公网安备 33010602011771号