Files
cs-note/hzh/GolangStar/Go语言进阶/Sync.md
T

769 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- Go
- golang
- go进阶
- sync
- 并发安全
---
# sync包
在前面学习`channel`的时候,我们说到在Go语言并发编程中,倡导使用通信共享内存,不要使用共享内存通信,`goroutine`之间尽量通过`channel`来协作。而在其他的传统语言中,都是通过共享内存加上锁机制来保证并发安全的,同样Go语言也提供了对共享内存并发安全机制的支持,这些功能都存在于`sync`包下。
## sync.WaitGroup
在前面很多`goroutine`的示例中,我们都是通过`time.Sleep()`方法让主`goroutine`等待一段时间以便子`gortoutine`能够执行完打印结果,显然这不是一个很好的办法,因为我们不知道所有的子`gortoutine`要多久才能执行完,不能确切的知道需要等待多久。那要怎么处理呢?
### 使用channel实现等待
看下面例子:
```go
package main
import (
"fmt"
)
func main() {
ch := make(chan struct{}, 10)
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Printf("num:%d\n",i)
ch <- struct{}{}
}(i)
}
for i := 0; i < 10; i++ {
<-ch
}
fmt.Println("end")
}
```
运行结果:
```
num:0
num:2
num:1
num:4
num:6
num:7
num:5
num:8
num:9
num:3
end
```
我们在每个`goroutine`中,向管道里发送一条数据,这样我们在程序最后,通过`for`循环将管道里的数据全部取出,直到数据全部取出完毕才能继续后面的逻辑,这样就可以实现等待各个`goroutine`执行完。
但是,这样使用`channel`显得并不优雅,其次,我们得知道具体循环的次数,来创建管道的大小,假设次数非常的多,则需要申请同样数量大小的管道出来,对内存也是不小的开销。
### 使用WaitGroup实现等待
这里我们可以用`sync`包下的`WaitGroup`来实现,Go语言中可以使用`sync.WaitGroup`来实现并发任务的同步以及协程任务等待。
`sync.WaitGroup`是一个对象,里面维护者一个计数器,并且通过三个方法来配合使用:
- (wg * WaitGroup) Add(delta int) 计数器加delta
- (wg *WaitGroup) Done() 计数器减1
- (wg *WaitGroup) Wait() 会阻塞代码的运行,直至计数器减为0
先看示例:
```go
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func myGoroutine() {
defer wg.Done()
fmt.Println("myGoroutine!")
}
func main() {
wg.Add(10)
for i := 0; i < 10; i++ {
go myGoroutine()
}
wg.Wait()
fmt.Println("end!!!")
}
```
运行结果:
```
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
end!!!
```
程序首先把`wg`的计数设置为10,每个`for`循环运行完毕都把计数器减1,`main`函数中执行到`wg.Wait()`会一直阻塞,直到`wg`的计数器为零。最后打印了10个`myGoroutine!`,是所有子`goroutine`任务结束后主`goroutine`才退出。
**注意:`sync.WaitGroup`对象的计数器不能为负数,否则会panic,在使用的过程中,我们需要保证`add()`的参数值,以及执行完`Done()`之后计数器大于等于零**
## sync.Once
在我们写项目的时候,程序中有很多的逻辑只需要执行一次,最典型的就是项目工程里配置文件的加载,我们只需要加载一次即可,让配置保存在内存中,下次使用的时候直接使用内存中的配置数据即可。这里就要用到`sync.Once`。
`sync.Once`可以在代码的任意位置初始化和调用,并且线程安全。`sync.Once`最大的作用就是延迟初始化,对于一个`sync.Once`变量我们并不会在程序启动的时候初始化,而是在第一次用的它的时候才会初始化,并且只初始化这一次,初始化之后驻留在内存里,这就非常适合我们之前提到的配置文件加载场景,设想一下,如果是在程序刚开始就加载配置,若迟迟未被使用,则既浪费了内存,又延长了程序加载时间,而`sync.Once`就刚好解决了这个问题。
使用示例:
```go
// 声明配置结构体Config
type Config struct{}
var instance *Config
var once sync.Once // 声明一个sync.Once变量
// 获取配置结构体
func InitConfig() *Config {
once.Do(func(){
instance = &Config{}
})
return instance
}
```
只有在第一次调用InitConfig()获取Config 指针的时候才会执行once.Do(func(){instance = &Config{} })语句,执行完之后instance就驻留在内存中,后面再次执行InitConfig()的时候,就直接返回内存中的instance。
### sync.Once与init()的区别
有时候我们使用init()方法进行初始化,init()方法是在其所在的package首次加载时执行的,而sync.Once可以在代码的任意位置初始化和调用,是在第一次用的它的时候才会初始化。
## sync.Lock
说到并发编程,就不得不谈一个老生常谈的问题,那就是资源竞争,也就是我们这节要讲的并发安全。因为一旦开启了多个`goroutine`去处理问题,那么这些`goroutine`就有可能在同一时间操作同一个系统资源,比如同一个变量,同一份文件等等,这里我们如果不加控制的话,可能会出现并发安全问题,在Go语言中,有两种方式来控制并发安全,锁和原子操作
举个例子,看下面代码
```go
package main
import (
"fmt"
"sync"
)
var (
num int
wg = sync.WaitGroup{}
)
func add() {
defer wg.Done()
num += 1
}
func main() {
var n = 10 * 10 * 10 * 10
wg.Add(n)
for i := 0; i < n; i++ {
// 启动n个goroutine去累加num
go add()
}
// 等待所有goroutine执行完毕
wg.Wait()
// 不出意外的话,num应该等于n,但是,但是,但是实际上不一致!
fmt.Println(num == n)
}
```
运行结果:
```
false
```
我们用`n`(这里是10000,可以自行修改,尽量数字大一点)个`goroutine`去给`num`做累加,最后并`num`并不等于`n`,这就是并发问题,同一时间有多个`goroutine`都在对`num`做`+1`操作,但是后一个并不是在前一次执行完的基础之上运行的,可能两次运行`num`的初始相同,这样前一个`num+1`的结果就被后一个覆盖了,看起来好像只做了一个加法。为了避免类似的并发安全问题,我们一般会采用下面两种方式处理,在go语言中并发相关的都在`sync`包下面。
### 锁
#### 互斥锁Mutex
互斥锁是一种最常用的控制并发安全的方式,它在同一时间只允许一个goroutine对共享资源进行访问。
互斥锁的声明方式如下:
```go
var lock sync.Mutex
```
互斥锁有两个方法
```go
func (m *Mutex) Lock() // 加锁
func (m *Mutex) Unlock() // 解锁
```
一个互斥锁只能同时被一个`goroutine`锁定,其它`goroutine`将阻塞直到互斥锁被解锁才能加锁成功。`sync.Mutex`在使用的时候要注意:**对一个未锁定的互斥锁解锁将会产生运行时错误**。
对上面的例子稍作修改,加上互斥锁:
```go
package main
import (
"fmt"
"sync"
)
var (
num int
wg = sync.WaitGroup{}
// 我们用锁来保证num的并发安全
mu = sync.Mutex{}
)
func add() {
mu.Lock()
defer wg.Done()
num += 1
mu.Unlock()
}
func main() {
var n = 10 * 10 * 10 * 10
wg.Add(n)
for i := 0; i < n; i++ {
// 启动n个goroutine去累加num
go add()
}
// 等待所有goroutine执行完毕
wg.Wait()
fmt.Println(num == n)
}
```
运行结果:
```
true
```
我们可以自行修改n的值,在我们能开启足够多的`goroutine`的情况下,他结果一定会是`true`。
本例使用了前面介绍的`sync.WaitGroup`来等待所有协程执行结束。并且在`add`函数里使用了互斥锁来保证`num += 1`操作的并发安全,但是注意不要忘了用`mu.Unlock`来进行解锁,否则其他`goroutine`将一直等待加锁造成阻塞。
#### 读写锁RWMutex
读写锁就是将读操作和写操作分开,可以分别对读和写进行加锁,一般用在大量读操作、少量写操作的情况。
读写锁的声明方式如下:
```go
var rw sync.RWMutex
```
读写锁有两个方法:
```go
func (rw *RWMutex) Lock() // 对写锁加锁
func (rw *RWMutex) Unlock() // 对写锁解锁
func (rw *RWMutex) RLock() // 对读锁加锁
func (rw *RWMutex) RUnlock() // 对读锁解锁
```
读写锁的使用遵循以下几个法则:
1. 同时只能有一个 goroutine 能够获得写锁定。
2. 同时可以有任意多个 gorouinte 获得读锁定。
3. 同时只能存在写锁定或读锁定(读和写互斥)。
通俗理解就是可以多个`goroutine`同时读,但是只有一个`goroutine`能写,共享资源要么在被一个或多个`goroutine`读取,要么在被一个`goroutine`写入, 读写不能同时进行。
读写锁示例:
```go
package main
import (
"fmt"
"sync"
"time"
)
var cnt = 0
func main() {
var mr sync.RWMutex
for i := 1; i <= 3; i++ {
go write(&mr, i)
}
for i := 1; i <= 3; i++ {
go read(&mr, i)
}
time.Sleep(time.Second)
fmt.Println("final count:", cnt)
}
func read(mr *sync.RWMutex, i int) {
fmt.Printf("goroutine%d reader start\n", i)
mr.RLock()
fmt.Printf("goroutine%d reading count:%d\n", i, cnt)
time.Sleep(time.Millisecond)
mr.RUnlock()
fmt.Printf("goroutine%d reader over\n", i)
}
func write(mr *sync.RWMutex, i int) {
fmt.Printf("goroutine%d writer start\n", i)
mr.Lock()
cnt++
fmt.Printf("goroutine%d writing count:%d\n", i, cnt)
time.Sleep(time.Millisecond)
mr.Unlock()
fmt.Printf("goroutine%d writer over\n", i)
}
```
运行结果:
```
goroutine3 reader start
goroutine3 reading count:0
goroutine1 writer start
goroutine2 writer start
goroutine1 reader start
goroutine2 reader start
goroutine3 writer start
goroutine3 reader over
goroutine1 writing count:1
goroutine1 writer over
goroutine1 reading count:1
goroutine2 reading count:1
goroutine2 reader over
goroutine2 writing count:2
goroutine1 reader over
goroutine2 writer over
goroutine3 writing count:3
goroutine3 writer over
final count: 3
```
简单分析:首先`goroutine3`开始加了读锁,开始读取,读到count的值为0,然后`goroutine1`尝试写入,`goroutine2`尝试写入,但是都会阻塞,因为`goroutine3`加了读锁,不能再加写锁,在第8行`goroutine3` 读取完毕之后,`goroutine1`争抢到了锁,加了写锁,写完释放写锁之后,`goroutine1`和`goroutine2`同时加了读锁,读到count的值为1。可以看到读写锁是互斥的,写写锁是互斥的,读读锁可以一起加。
#### 死锁
提到锁,就有一个绕不开的话题:死锁。死锁就是一种状态,当两个或以上的`goroutine`在执行过程中,因争夺共享资源处在互相等待的状态,如果没有外部干涉将会一直处于这种阻塞状态,我们称这时的系统发生了死锁。思索场景一般有以下两种
1. **Lock/Unlock不成对**。这类情况最常见的场景就是对锁进行拷贝使用
```go
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
copyMutex(mu)
}
func copyMutex(mu sync.Mutex) {
mu.Lock()
defer mu.Unlock()
fmt.Println("ok")
}
```
运行结果:
```
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_SemacquireMutex(0xc0000160ac, 0x0, 0x1)
D:/Program Files/Go/src/runtime/sema.go:71 +0x4e
sync.(*Mutex).lockSlow(0xc0000160a8)
D:/Program Files/Go/src/sync/mutex.go:138 +0x10f
sync.(*Mutex).Lock(...)
D:/Program Files/Go/src/sync/mutex.go:81
main.copyTest(0xc0000160a8)
```
会报死锁,为什么呢?有的同学可能会注意到,这里`mu sync.Mutex`当作参数传入到函数`copyMutex`,锁进行了拷贝,不是原来的锁变量了,那么一把新的锁,在执行`mu.Lock()`的时候应该没问题。这就是要注意的地方,如果将带有锁结构的变量赋值给其他变量,锁的状态会复制。所以多锁复制后的新的锁拥有了原来的锁状态,那么在`copyMutex`函数内执行`mu.Lock()`的时候会一直阻塞,因为外层的`main`函数已经`Lock()`了一次,但是并没有机会`Unlock()`,导致内层函数会一直等待`Lock()`,而外层函数一直等待`Unlock()`,这样就造成了死锁
所以在使用锁的时候,我们应当尽量避免锁拷贝,并且保证Lock()和Unlock()成对出现,没有成对出现容易会出现死锁的情况,或者是Unlock 一个未加锁的Mutex而导致 panic。尽量养成如下使用习惯
```go
mu.Lock()
defer mu.Unlock()
```
2. **循环等待**
另一个容易造成死锁的场景就是循环等待,A等B,B等C,C等A,循环等待
```go
package main
import (
"sync"
"time"
)
func main() {
var mu1, mu2 sync.Mutex
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
mu1.Lock()
defer mu1.Unlock()
time.Sleep(1 * time.Second)
mu2.Lock()
defer mu2.Unlock()
}()
go func() {
defer wg.Done()
mu2.Lock()
defer mu2.Unlock()
time.Sleep(1 * time.Second)
mu1.Lock()
defer mu1.Unlock()
}()
wg.Wait()
}
```
运行结果:
```
fatal error: all goroutines are asleep - deadlock!
```
死锁了,代码很简单,两个`goroutine`,一个`goroutine`先锁`mu1`,再锁`mu2`,另一个`goroutine`先锁`mu2`,再锁`mu1`,但是在它们进行第二次枷锁操作的时候,彼此等待对方释放锁,这样就造成了循环等待,一直阻塞,形成死锁。
## sync.Map
Go语言内置的Map并不是并发安全的,在多个`goroutine`同时操作map的时候,会有并发问
具体看下面例子
```go
package main
import (
"fmt"
"strconv"
"sync"
)
var m = make(map[string]int)
func getVal(key string) int {
return m[key]
}
func setVal(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(num int) {
defer wg.Done()
key := strconv.Itoa(num)
setVal(key, num)
fmt.Printf("key=:%v,val:=%v\n", key, getVal(key))
}(i)
}
wg.Wait()
}
```
运行结果:
```
fatal error: concurrent map writes
```
程序报错了,说明`map`不能同时被多个`goroutine`读写。要解决`map`的并发写问题一种方式使用我们前面学到的对`map`加锁,这样就可以了
```go
package main
import (
"fmt"
"strconv"
"sync"
)
var m = make(map[string]int)
var mu sync.Mutex
func getVal(key string) int {
return m[key]
}
func setVal(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(num int) {
defer func() {
wg.Done()
mu.Unlock()
}()
key := strconv.Itoa(num)
mu.Lock()
setVal(key, num)
fmt.Printf("key=:%v,val:=%v\n", key, getVal(key))
}(i)
}
wg.Wait()
}
```
运行结果:
```
key=:9,val:=9
key=:4,val:=4
key=:0,val:=0
key=:1,val:=1
key=:2,val:=2
key=:3,val:=3
key=:6,val:=6
key=:7,val:=7
key=:5,val:=5
key=:8,val:=8
```
另外一种方式是使用`sync`包中提供的一个开箱即用的并发安全版`map`–`sync.Map`,在 Go 1.9 引入。`sync.Map`不用初始化就可以使用,同时`sync.Map`内置了诸如`Store`、`Load`、`LoadOrStore`、`Delete`、`Range`等操作方法。
具体使用方法看示例:
```go
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 1. 写入
m.Store("name", "zhangsan")
m.Store("age", 18)
// 2. 读取
age, _ := m.Load("age")
fmt.Println(age.(int))
// 3. 遍历
m.Range(func(key, value interface{}) bool {
fmt.Printf("key is:%v, val is:%v\n", key, value)
return true
})
// 4. 删除
m.Delete("age")
age, ok := m.Load("age")
fmt.Println(age, ok)
// 5. 读取或写入
m.LoadOrStore("name", "zhangsan")
name, _ := m.Load("name")
fmt.Println(name)
}
```
运行结果:
```
18
key is:name, val is:zhangsan
key is:age, val is:18
<nil> false
zhangsan
```
1. 通过store方法写入两个键值对
2. 读取key为age的值,读出来age为18
3. 通过range方法遍历map的key和value
4. 删除key为age的键值对,删除完之后,再次读取age,age为空,ok为false表示map里没有这个key
5. LoadOrStore尝试读取key为name的值,读取不到就写入键值对name-zhangsan,能读取到就返回原来map里的name对应的值
**注意:`sync.Map` 没有提供获取 map 数量的方法,需要我们在对 `sync.Map`进行遍历时自行计算,`sync.Map` 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 `map` 相比使用 `sync.Map` 会有更好的性能**
## sync/Atomic
除了前面介绍的锁`mutex`以外,还有一种解决并发安全的策略,就是原子操作。所谓原子操作就是这一系列的操作在`cpu`上执行是一个不可分割的整体,显然要么全部执行,要么全部不执行,不会受到其他操作的影响,也就不会存在并发问题。
### atomic和mutex的区别
1. 使用方式:通常`mutex`用于保护一段执行逻辑,而`atomic`主要是对变量进行操作
2. 底层实现:`mutex`由操作系统调度器实现,而`atomic`操作有底层硬件指令支持,保证在`cpu`上执行不中断。所以`atomic`的性能也能随`cpu`的个数增加线性提升
#### `atomic`提供的方法:
```go
func AddT(addr *T, delta T)(new T)
func StoreT(addr *T, val T)
func LoadT(addr *T) (val T)
func SwapT(addr *T, new T) (old T)
func CompareAndSwapT(addr *T, old, new T) (swapped bool)
T的类型是int32、int64、uint32、uint64和uintptr中的任意一种
```
这里就不一一演示各个方法了,以`AddT`方法为例简单看一个例子
```go
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var sum int32 = 0
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt32(&sum, 1)
}()
}
wg.Wait()
fmt.Printf("sum is %d\n",sum)
}
```
100个goroutine,每个goroutine都对sum+1,最后结果为100。
### atomic.value
上面展示的`AddT`,`StoreT`等方法都是针对的基本数据类型做的操作,假设想对多个变量进行同步保护,即假设想对一个`struct`这样的复合类型用原子操作,也是支持的吗?也可以做支持,go语言里的`atomic.value`支持任意一种接口类型进行原子操作,且提供了`Load`、`Store`、`Swap`和`CompareAndSwap`四种方法:
- `Load`:func (v *Value) Load() (val any),从value读出数据
- `Store`:func (v *Value) Store(val any),向value写入数据
- `Swap`:func (v *Value) Swap(new any) (old any),用new交换value中存储的数据,返回value原来存储的旧数据
- `CompareAndSwap`:func (v *Value) CompareAndSwap(old, new any) (swapped bool),比较value中存储的数据和old是否相同,相同的话,将value中的数据替换为new
代码示例
```go
package main
import (
"fmt"
"sync/atomic"
)
type Student struct {
Name string
Age int
}
func main() {
st1 := Student{
Name: "zhangsan",
Age: 18,
}
st2 := Student{
Name: "lisi",
Age: 19,
}
st3 := Student{
Name: "wangwu",
Age: 20,
}
var v atomic.Value
v.Store(st1)
fmt.Println(v.Load().(Student))
old := v.Swap(st2)
fmt.Printf("after swap: v=%v\n", v.Load().(Student))
fmt.Printf("after swap: old=%v\n", old)
swapped := v.CompareAndSwap(st1, st3) // v中存储的和st1不相同,交换失败
fmt.Println("compare st1 and v\n", swapped, v)
swapped = v.CompareAndSwap(st2, st3) // v中存储的和st2相同,交换成功,v中变为st3
fmt.Println("compare st2 and v\n", swapped, v)
}
```
运行结果
```
{zhangsan 18}
after swap: v={lisi 19}
after swap: old={zhangsan 18}
compare st1 and v
false {{lisi 19}}
compare st2 and v
true {{wangwu 20}}
```
## sync.pool
`sync.Pool`是在`sync`包下的一个内存池组件,用来实现对象的复用,避免重复创建相同的对象,造成频繁的内存分配和gc,以达到提升程序性能的目的。虽然池子中的对象可以被复用,但是是`sync.Pool`并不会永久保存这个对象,池子中的对象会在一定时间后被gc回收,这个时间是随机的。所以,用`sync.Pool`来持久化存储对象是不可取的。
另外,`sync.Pool`本身是并发安全的,支持多个`goroutine`并发的往`sync.Pool`存取数据
### sync.pool使用方法
关于`sync.Pool`的使用,一般是通过三个方法来完成的
| 方法 | 说明 |
| --- | --- |
| New() | sync.Pool的构造函数,用于指定sync.Pool中缓存的数据类型,当调用Get方法从对象池中获取对象的时候,对象池中如果没有,会调用New方法创建一个新的对象 |
| Get() | 从对象池取对象 |
| Put() | 往对象池放对象,下次Get的时候可以复用 |
下面通过例子看一下`sync.Pool`的使用方式
```go
package main
import (
"fmt"
"sync"
)
type Student struct {
Name string
Age int
}
func main() {
pool := sync.Pool{
New: func() interface{} {
return &Student{
Name: "zhangsan",
Age: 18,
}
},
}
st := pool.Get().(*Student)
println(st.Name, st.Age)
fmt.Printf("addr is %p\n", st)
pool.Put(st)
st1 := pool.Get().(*Student)
println(st1.Name, st1.Age)
fmt.Printf("addr1 is %p\n", st1)
}
```
程序输出
```
zhangsan 18
addr is 0x140000a0018
zhangsan 18
addr1 is 0x140000a0018
```
在程序中,首先初始化一个`sync.Pool`对象,初始化里面的`New`方法,用于创建对象,这里是返回一个`Student`类型的指针。第一次调用`pool.Get().(*Student)`的时候,由于池子内没有对象,所以会通过`New`方法创建一个,注意`pool.Get()`返回的是一个`interface{}`,所以我们需要断言成`*Student`类型,在我们使用完,打印出`Name`和`Age`之后,再调用`Put`方法,将这个对象放回到池子内,后面我们紧接着又调用`pool.Get()`取对象,可以看到两次去除的对象地址是同一个,说明是同一个对象,表明`sync.Pool`有缓存对象的功能。
>注意
>我们在第一次`pool.Get()`取出`*Student`对象打印完地址之后,put进池子的时候没有进行一个Reset的过程,这里是因为我们取出`*Student`对象之后,仅仅是读取里面的字段,并没有修改操作,假设我们有修改操作,那么这里就需要在`pool.Put(st)`之前执行`Reset`,将对象的值复原,如果不这样做,那么下一次`pool.Get()`取出的`*Student`对象就不是我们希望复用的初始对象
假设我们对`*Student`做修改
```go
package main
import (
"fmt"
"sync"
)
type Student struct {
Name string
Age int
}
func main() {
pool := sync.Pool{
New: func() interface{} {
return &Student{
Name: "zhangsan",
Age: 18,
}
},
}
st := pool.Get().(*Student)
println(st.Name, st.Age)
fmt.Printf("addr is %p\n", st)
// 修改
st.Name = "lisi"
st.Age = 20
// 回收
pool.Put(st)
st1 := pool.Get().(*Student)
println(st1.Name, st1.Age)
fmt.Printf("addr1 is %p\n", st1)
}
```
程序输出
```
zhangsan 18
addr is 0x1400000c030
lisi 20
addr1 is 0x1400000c030
```
可以看到,我们第二次取出的对象虽然和第一次是同一个,地址形同,但是对象的字段值却发生了变化,不是我们初始化的对象了,我们想要一只重复使用一个相同的对象的话,显然这里有问题。所以,我们需要在`pool.Put(st)`回收对象之前,进行对象的`Reset`操作,将对象值复原,同时在每次我们`pool.Get()`取出完对象使用完毕之后,也不要忘了调用`pool.Put`方法把对象再次放入对象池,以便对象能够复用。
### sync.pool使用场景
1. `sync.pool`主要是通过对象复用来降低gc带来的性能损耗,所以在高并发场景下,由于每个`goroutine`都可能过于频繁的创建一些大对象,造成gc压力很大。所以在高并发业务场景下出现 GC 问题时,可以使用 `sync.Pool` 减少 GC 负担
2. `sync.pool`不适合存储带状态的对象,比如socket 连接、数据库连接等,因为里面的对象随时可能会被gc回收释放掉
3. 不适合需要控制缓存对象个数的场景,因为`Pool` 池里面的对象个数是随机变化的,因为池子里的对象是会被gc的,且释放时机是随机的