上次作业讲解
- 切片元素的去重 ```go package main
import "fmt"
// - 切片元素的去重 // 使用map的key func main() { s1 := []string{"abc", "def", "abc", "okok", "okok"}
m := make(map[string]struct{})
for _, i := range s1 {
m[i] = struct{}{}
}
s2 := make([]string, 0)
for k := range m {
s2 = append(s2, k)
}
fmt.Println(s1)
fmt.Println(s2)
}
# 今天的内容大纲
- go中的锁
- map
- 管道
- 流程控制
- 函数
- 结构体
- 接口
- 面向对象
# 今天的内容重点
- map 带过期时间 、分片、sync.map
- chan 也比较重要,goroutine 一起理解
- 函数 太重要
# 下一节要讲什么内容
- panic和defer
- 结构体
- 结构体定义
- 结构体声明
- 结构体初始化
- new函数介绍
- 属性访问与修改
- 结构体命名嵌入
- 结构体匿名嵌入
- 结构体指针类型嵌入
- 结构体可见性
- 结构体浅拷贝和深拷贝
- 面向对象
- 接口
# 今天的作业
- 类似改造f1 f2 写一个函数 返回一个 切片,在defer要去操作,考察return+命名返回+slice操作+defer
- 不定长参数函数 : 合并多个map,return新的map
- 因为没讲到结构体,可以看看下面的两个
- jobManager 增量更新job
- 结构体 有数组 切片 map ,写一个接受不定长参数的函数,合并所有字段,然后返回一个新的结构体
# go中的锁
## 互斥锁
- sync.mutex
- 获取到互斥锁的任务,阻塞其他任务来获取锁
- 意味着同一时间只能有一个任务 才能持有互斥锁
```go
package main
import (
"log"
"sync"
"time"
)
// HcMutex 它是一个互斥锁
var HcMutex sync.Mutex
func runMutex(id int) {
log.Printf("[任务id :%d][尝试获取锁]", id)
HcMutex.Lock()
log.Printf("[任务id :%d][获取到了锁]", id)
time.Sleep(20 * time.Second)
HcMutex.Unlock()
log.Printf("[任务id :%d][干完活了 释放锁]", id)
}
func runHcLock() {
go runMutex(1)
go runMutex(2)
go runMutex(3)
}
func main() {
runHcLock()
time.Sleep(6 * time.Minute)
/*
2021/07/11 18:11:02 [任务id :3][尝试获取锁]
2021/07/11 18:11:02 [任务id :3][获取到了锁]
2021/07/11 18:11:02 [任务id :1][尝试获取锁]
2021/07/11 18:11:02 [任务id :2][尝试获取锁]
2021/07/11 18:11:22 [任务id :1][获取到了锁]
2021/07/11 18:11:22 [任务id :3][干完活了 释放锁]
2021/07/11 18:11:42 [任务id :1][干完活了 释放锁]
2021/07/11 18:11:42 [任务id :2][获取到了锁]
*/
}
读写锁
- sync.RWmutex
同时多个写锁任务,说明如果并非使用读写锁的写锁时,退化成了互斥锁
同时多个读锁任务,说明使用读写锁的读锁,可以同时施加多把读锁
先启动写锁任务,后并发5个读锁任务. 当有写锁存在时,读锁是施加不了的。写锁释放完,读锁可以施加多个
先并发5个读锁任务,后启动一个写锁任务. 当有读锁时,阻塞写锁。
- 举例 ```go package main
import ( "log" "sync" "time" )
var rwMutex sync.RWMutex
func runReadLock(id int) { log.Printf("[读任务id :%d][进入读方法尝试获取读锁]", id) rwMutex.RLock() log.Printf("[读任务id :%d][获取到了读锁][开始干活 睡眠10秒]", id) time.Sleep(10 * time.Second) rwMutex.RUnlock() log.Printf("[读任务id :%d][完成读任 释放读锁]", id) }
func runWriteLock(id int) { log.Printf("[写任务id :%d][进入写方法尝试获取写锁]", id) rwMutex.Lock() log.Printf("[写任务id :%d][获取到了写锁][开始干活 睡眠10秒]", id) time.Sleep(10 * time.Second) rwMutex.Unlock() log.Printf("[写任务id :%d][完成写任务 释放写锁]", id) }
// 全是写任务 func allWriteWorks() { for i := 1; i < 6; i++ { go runWriteLock(i) } }
// 全是读任务 func allReadWorks() { for i := 1; i < 6; i++ { go runReadLock(i) } }
// 先启动写任务 func writeFirst() { go runWriteLock(1) time.Sleep(1 * time.Second) go runReadLock(1) go runReadLock(2) go runReadLock(3) go runReadLock(4) go runReadLock(5) }
// 先启动写任务 func readFirst() { go runReadLock(1) go runReadLock(2) go runReadLock(3) go runReadLock(4) go runReadLock(5)
time.Sleep(1 * time.Second) go runWriteLock(1) }
func main() { log.Printf("执行读写锁效果的函数") // 1. 同时多个写锁任务,说明如果并非使用读写锁的写锁时,退化成了互斥锁 //allWriteWorks()
// 2. 同时多个读锁任务,说明使用读写锁的读锁,可以同时施加多把读锁 //allReadWorks()
// 3. 先启动写锁任务,后并发5个读锁任务. 当有写锁存在时,读锁是施加不了的。写锁释放完,读锁可以施加多个 //writeFirst()
// 4. 先并发5个读锁任务,后启动一个写锁任务. 当有读锁时,阻塞写锁。 readFirst()
time.Sleep(1 * time.Hour) }
# map使用
## 声明和初始化
### 使用var只声明
- `// 只声明
var gMap map[string]string`
### 使用var声明同时初始化
- `// 声明初始化
var hMap = map[string]string{"a": "b"}`
### 使用make 初始化
## 增删改查
### 增加和修改
### 删除
### 读取数据
- 两种手段 第一种 单变量 lang:=m["lang"],没有key就附一个value类型的0值
- 两种手段 第二种 双变量 app2,exists := m["app2"],可以根据exists判断 true代表有key
### 增删改查举例
- 举例
```go
package main
import "fmt"
// 只声明
var m1 map[string]string
// 声明又初始化
var m2 = map[string]string{"a": "b"}
func main() {
m := make(map[string]string)
// 增加
m["app"] = "taobao"
m["lang"] = "golang"
// 删除
delete(m, "app")
fmt.Println(m)
// 改
m["lang"] = "python"
// 查
// 单变量形式
lang := m["lang"]
fmt.Println(lang)
// 双变量形式
lang1, exists := m["lang"]
if exists {
fmt.Printf("[lang存在 值:%v]\n", lang1)
} else {
fmt.Println("lang不存在\n")
m["lang"] = "java"
}
}
遍历map和按key的顺序遍历
- 举例 ```go package main
import "fmt"
// 只声明 var m1 map[string]string
// 声明又初始化 var m2 = map[string]string{"a": "b"}
func main() { m := make(map[string]int) keys := make([]string, 0) for i := 0; i < 20; i++ { key := fmt.Sprintf("key_%d", i) keys = append(keys, key) m[key] = i } fmt.Println(m) // range 遍历 keys //for k := range m { // fmt.Printf("[key=%s]\n", k) //}
fmt.Println("无序遍历")
// range 遍历
for k, v := range m {
fmt.Printf("[%s=%d]\n", k, v)
}
// 有序key遍历
fmt.Println("有序遍历")
for _, k := range keys {
fmt.Printf("[%s=%d]\n", k, m[k])
}
}
## key的类型:float64可以作为key吗
- bool、int、string
- 特征是 支持 == 和!=比较
- float型可以做为key的,写入map时会做math.Float64bits()的转换,认为2.4=2.40000xxxx1 ,看起来是同一个key
```go
package main
import "fmt"
func main() {
m := make(map[float64]int)
m[2.4] = 2
fmt.Printf("k: %v, v: %d\n", 2.4000000000000000000000001, m[2.4000000000000000000000001])
fmt.Println(m[2.4000000000000000000000001] == m[2.4])
}
value的类型:任意类型
- map的value是个map,每一层map都要make
package main
func main() {
var doubleM map[string]map[string]string
// panic: assignment to entry in nil map
//doubleM = make(map[string]map[string]string)
v1 := make(map[string]string)
v1["k1"] = "v1"
doubleM["m1"] = v1
}
go原生的map线程不安全
fatal error :concurrent map read and map write
- 举例 ```go package main
import "time"
func main() {
c := make(map[int]int)
// 匿名goroutine 循环写map
go func() {
for i := 0; i < 10000; i++ {
c[i] = i
}
}()
// 匿名goroutine 循环读map
go func() {
for i := 0; i < 10000; i++ {
_ = c[i]
}
}()
time.Sleep(30 * time.Minute)
}
### fatal error: concurrent map writes
- 举例
```go
package main
import "time"
func main() {
c := make(map[int]int)
// 匿名goroutine 循环写map
go func() {
for i := 0; i < 10000; i++ {
c[i] = i
}
}()
// 匿名goroutine 循环写map
go func() {
for i := 0; i < 10000; i++ {
c[i] = i
}
}()
//
//// 匿名goroutine 循环读map
//go func() {
// for i := 0; i < 10000; i++ {
// _ = c[i]
// }
//}()
time.Sleep(30 * time.Minute)
}
上述问题原因
- go原生的map线程不安全 具体原因情况 go中的读写锁
解决方法之一 加锁
- 使用读写锁 ```go package main
import ( "fmt" "sync" "time" )
// 为了解决map线程不安全 ,我们自己加锁
type concurrentMap struct { mp map[int]int sync.RWMutex }
// 通过set 方法做原有map的赋值 m[key] =v func (c *concurrentMap) Set(key, value int) { // 加写锁 c.Lock() c.mp[key] = value c.Unlock()
}
// 通过get 方法做原有map的读取值操作 v:= m[key] func (c *concurrentMap) Get(key int) int { //先获取读锁 c.RLock() res := c.mp[key] c.RUnlock() return res }
func main() { c := concurrentMap{ mp: make(map[int]int), } // 一个线程循环写map go func() { for i := 0; i < 10000; i++ { c.Set(i, i) } }() // 一个线程循环读map go func() { for i := 0; i < 10000; i++ { res := c.Get(i) fmt.Printf("[cmap.get][%d=%d]\n", i, res) } }() time.Sleep(1 * time.Hour)
}
### 解决方法之二 使用sync.map
- go 1.9引入的内置方法,并发线程安全的map
- sync.Map 将key和value 按照interface{}存储
- 查询出来后要类型断言 x.(int) x.(string)
- 遍历使用Range() 方法,需要传入一个匿名函数作为参数,匿名函数的参数为k,v interface{},每次调用匿名函数将结果返回。
- 举例
```go
package main
import (
"fmt"
"log"
"strings"
"sync"
)
func main() {
m := sync.Map{}
// 新增
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key_%d", i)
m.Store(key, i)
}
// 删除
m.Delete("key_8")
// 改m.Store
m.Store("key_9", 999)
// 查询
res, loaded := m.Load("key_09")
if loaded {
// 类型断言 res.(int)
log.Printf("[key_09存在 :%v 数字类型:%d]", res, res.(int))
}
// 遍历 return false 停止
m.Range(func(key, value interface{}) bool {
k := key.(string)
v := value.(int)
if strings.HasSuffix(k, "3") {
log.Printf("不想要3")
//return true
return false
} else {
log.Printf("[sync.map.Range][遍历][key:=%s][v:=%d]", k, v)
return true
}
})
// LoadAndDelete 先获取值再删掉
s1, loaded := m.LoadAndDelete("key_7")
log.Printf("key_7 LoadAndDelete :%v", s1)
s2, loaded := m.Load("key_7")
log.Printf("key_7 LoadAndDelete:%v", s2)
actual, loaded := m.LoadOrStore("key_8", 158)
if loaded {
log.Printf("key_8原来的值是:%v", actual)
} else {
log.Printf("key_8原来没有,实际是:%v", actual)
}
actual, loaded = m.LoadOrStore("key_1", 158)
if loaded {
log.Printf("key_1原来的值是:%v", actual)
} else {
log.Printf("key_1原来没有,实际是:%v", actual)
}
}
sync.map 性能对比
性能对比结论
shell script 只读场景:sync.map > rwmutex >> mutex 读写场景(边读边写):rwmutex > mutex >> sync.map 读写场景(读80% 写20%):sync.map > rwmutex > mutex 读写场景(读98% 写2%):sync.map > rwmutex >> mutex 只写场景:sync.map >> mutex > rwmutex
sync.Map使用场景的建议
- 读多:给定的key-v只写一次,但是读了很多次,只增长的缓存场景
- key不相交: 覆盖更新的场景比少
结构体复杂的case多不用sync.Map
上述两种手段有什么问题
- 没有精细化锁控制 没有分片
- 加了大锁
分片锁 并发map github.com/orcaman/concurrent-map
- 基础用法 ```go package main
import ( "fmt" "github.com/orcaman/concurrent-map" "log" "time" )
func main() { // Create a new map. m := cmap.New()
// 循环写map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
m.Set(key, i)
}
}()
// 循环读map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
v, exists := m.Get(key)
if exists {
log.Printf("[%s=%v]", key, v)
}
}
}()
// 循环写map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
m.Set(key, i)
}
}()
// 循环写map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
m.Set(key, i)
}
}()
// 循环写map
go func() {
for i := 0; i < 10000; i++ {
key := fmt.Sprintf("key_%d", i)
m.Set(key, i)
}
}()
time.Sleep(1 * time.Hour)
}
### 带过期时间的map
- 为什么要有过期时间
- map做缓存用的 垃圾堆积k1 k2
- 希望缓存存活时间 5分钟,
- 将加锁的时间控制在最低,
- 耗时的操作在加锁外侧做
```go
package main
import (
"fmt"
"log"
"sync"
"time"
)
//带过期时间的map 定时清理
type Cache struct {
sync.RWMutex
mp map[string]*item
}
type item struct {
value int //值
ts int64 // 时间戳,item 被创建出来的时间
}
func (c *Cache) Get(key string) *item {
c.RLock()
defer c.RUnlock()
return c.mp[key]
}
func (c *Cache) CacheNum() int {
c.RLock()
keys := make([]string, 0)
//i := 0
for k, _ := range c.mp {
//fmt.Println(k)
keys = append(keys, k)
//i++
}
c.RUnlock()
return len(keys)
}
func (c *Cache) Set(key string, value *item) {
c.Lock()
defer c.Unlock()
c.mp[key] = value
}
func (c *Cache) Clean(timeDelta int64) {
// 每5秒执行一此清理
for {
now := time.Now().Unix()
// 待删除的key的切片
toDelKeys := make([]string, 0)
// 先加读锁,把所有待删除的拿到
c.RLock()
for k, v := range c.mp {
// 时间比较
if now-v.ts > timeDelta {
// 认为这个k,v过期了,
// 不直接删除,为了降低加锁时间,加入待删除的切片
toDelKeys = append(toDelKeys, k)
}
}
c.RUnlock()
// 加写锁 删除,降低加写锁的时间
c.Lock()
for _, k := range toDelKeys {
log.Printf("[删除过期数据][key:%s]", k)
delete(c.mp, k)
}
c.Unlock()
// 写锁释放
time.Sleep(2 * time.Second)
}
}
func main() {
c := Cache{
mp: make(map[string]*item),
}
// 让清理的任务异步执行
// 每2秒运行一次,检查时间差大于30秒item 就删除
go c.Clean(30)
// 从mysql中读取到了数据,塞入缓存
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key_%d", i)
ts := time.Now().Unix()
im := &item{
value: i,
ts: ts,
}
// 设置缓存
log.Printf("[设置缓存][item][key:%s][v:%v]", key, im)
c.Set(key, im)
}
log.Printf("缓存中的数据量:%d", c.CacheNum())
time.Sleep(33 * time.Second)
log.Printf("缓存中的数据量:%d", c.CacheNum())
// 更新缓存
for i := 0; i < 5; i++ {
key := fmt.Sprintf("key_%d", i)
ts := time.Now().Unix()
im := &item{
value: i,
ts: ts,
}
// 设置缓存
log.Printf("[更新缓存][item][key:%s][v:%v]", key, im)
c.Set(key, im)
}
log.Printf("缓存中的数据量:%d", c.CacheNum())
select {}
/*2021/07/18 11:48:36 [设置缓存][item][key:key_0][v:&{0 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_1][v:&{1 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_2][v:&{2 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_3][v:&{3 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_4][v:&{4 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_5][v:&{5 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_6][v:&{6 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_7][v:&{7 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_8][v:&{8 1626580116}]
2021/07/18 11:48:36 [设置缓存][item][key:key_9][v:&{9 1626580116}]
2021/07/18 11:48:36 缓存中的数据量:10
2021/07/18 11:49:08 [删除过期数据][key:key_2]
2021/07/18 11:49:08 [删除过期数据][key:key_4]
2021/07/18 11:49:08 [删除过期数据][key:key_5]
2021/07/18 11:49:08 [删除过期数据][key:key_8]
2021/07/18 11:49:08 [删除过期数据][key:key_9]
2021/07/18 11:49:08 [删除过期数据][key:key_0]
2021/07/18 11:49:08 [删除过期数据][key:key_1]
2021/07/18 11:49:08 [删除过期数据][key:key_3]
2021/07/18 11:49:08 [删除过期数据][key:key_6]
2021/07/18 11:49:08 [删除过期数据][key:key_7]
2021/07/18 11:49:09 缓存中的数据量:0
2021/07/18 11:49:09 [更新缓存][item][key:key_0][v:&{0 1626580149}]
2021/07/18 11:49:09 [更新缓存][item][key:key_1][v:&{1 1626580149}]
2021/07/18 11:49:09 [更新缓存][item][key:key_2][v:&{2 1626580149}]
2021/07/18 11:49:09 [更新缓存][item][key:key_3][v:&{3 1626580149}]
2021/07/18 11:49:09 [更新缓存][item][key:key_4][v:&{4 1626580149}]
2021/07/18 11:49:09 缓存中的数据量:5
2021/07/18 11:49:40 [删除过期数据][key:key_0]
2021/07/18 11:49:40 [删除过期数据][key:key_2]
2021/07/18 11:49:40 [删除过期数据][key:key_3]
2021/07/18 11:49:40 [删除过期数据][key:key_1]
2021/07/18 11:49:40 [删除过期数据][key:key_4]
*/
}
带过期时间的缓存 github.com/patrickmn/go-cache
简单应用上手 ```go package main
import ( "fmt" "github.com/patrickmn/go-cache" "time" )
func main() { // Create a cache with a default expiration time of 5 minutes, and which // purges expired items every 10 minutes c := cache.New(30time.Second, 5time.Second)
// eSet the value of the key "foo" to "bar", with the default xpiration time
c.Set("k1", "v1", 31*time.Second)
res, ok := c.Get("k1")
fmt.Println(res, ok)
time.Sleep(time.Second * 32)
res, ok = c.Get("k1")
fmt.Println(res, ok)
}
> 生产上的 web缓存应用
- 查询触发性质的
```go
package main
import (
"fmt"
"github.com/patrickmn/go-cache"
"log"
"time"
)
// 生产上的 web缓存应用
// 维护用户信息的模块
// 在mysql中有一张 user表
// 正常情况是用orm gorm xorm 去db中查询
// 查询qps很高,为了性能会加缓存
//(更新不会太频繁) ,说明在一定时间内,获取到旧数据也能容忍
type user struct {
Name string
Email string
Phone int64
}
var (
DefaultInterval = time.Minute * 1
UserCache = cache.New(DefaultInterval, DefaultInterval)
)
// 最外层调用函数
// 优先去本地缓存中查,有就返回
// 没有再去远端查询 ,远端用http表示
func GetUser(name string) user {
// 消耗 0.1cpu 0.1M内存 0.1秒返回
res, found := UserCache.Get(name)
if found {
//if res, found := UserCache.Get(name); found {
log.Printf("[本地缓存中找到了对应的用户][name:%v][value:%v]", name, res.(user))
return res.(user)
// 消耗 1cpu 10M内存 3秒返回
} else {
res := HttpGetUser(name)
log.Printf("[本地缓存中没找到对应的用户,去远端查询获取到了,塞入缓存中][name:%v][value:%v]", name, res)
// 本地没有,但是从远端拿到了最新的数据
// 更新本地缓存 ,我种树,其他人乘凉
UserCache.Set(name, res, DefaultInterval)
return res
}
}
func HttpGetUser(name string) user {
// mock 模拟我去接口查
u := user{
Name: name,
Email: "qq.com",
Phone: time.Now().Unix(),
}
return u
}
// 查询方法
func queryUser() {
for i := 0; i < 10; i++ {
userName := fmt.Sprintf("user_name_%d", i)
GetUser(userName)
}
}
func main() {
log.Printf("第1次query_user")
queryUser()
log.Printf("第2次query_user")
queryUser()
queryUser()
queryUser()
queryUser()
queryUser()
time.Sleep(61 * time.Second)
log.Printf("第3次query_user")
queryUser()
}
map的实际应用
map的原理
map底层原理文章推荐
- https://zhuanlan.zhihu.com/p/66676224
- https://segmentfault.com/a/1190000039101378
- https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/
channel
- Channel是Go中的一个核心类型
- 你可以把它看成一个管道
像 map 和 slice 数据类型一样, channel必须先创建再使用:
ch := make(chan int)
箭头的指向就是数据的流向
ch <- v // 发送值v到Channel ch中
v := <-ch // 从Channel ch中接收数据,并将数据赋值给v
channel可进行3种操作
- 读
- 写
- 关闭
channel存在3种状态:
- nil ,未初始化的状态: 只用var 只声明了,或者手动赋值为nil
- active ,正常的channel 。可以读或写
- closed ,已关闭的。它的值不是nil
同步模式
- channel默认为同步模式,即不创建缓冲区,发送和接收需要一一配对,不然发送方会被一直阻塞
package main
import (
"log"
"time"
)
func main() {
// 初始化一个类型为int的chan
data := make(chan int)
// 读取数据的任务
go func() {
for {
r := <-data
log.Printf("[接收到了数据,开始处理]:%v", r)
}
}()
// 写入数据
data <- 1
time.Sleep(2 * time.Second)
data <- 2
time.Sleep(2 * time.Second)
data <- 3
time.Sleep(2 * time.Second)
close(data)
}
使用range遍历
- 使用for-range读取channel,这样既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。
- 如果不加最后这个sleep , chan 关闭了不会打印 ```go package main
import ( "log" "time" )
func main() {
// 初始化一个类型为int的chan
data := make(chan int)
// 读取数据的任务
go func() {
for r := range data {
log.Printf("[接收到了数据,开始处理]:%v", r)
}
log.Printf("chan 关闭了")
}()
// 写入数据
data <- 1
time.Sleep(2 * time.Second)
data <- 2
time.Sleep(2 * time.Second)
data <- 3
time.Sleep(2 * time.Second)
close(data)
time.Sleep(1 * time.Second)
// 现象是 chan 关闭了没打印
// 如果加上 time.Sleep(1 * time.Second) 就会打印
}
> 使用 if双返回值判断 chan是否被关闭
- ok=false通道关闭,无数据读到。
```go
package main
import (
"log"
"time"
)
func main() {
// 初始化一个类型为int的chan
data := make(chan int)
// 读取数据的任务
go func() {
for {
if r, ok := <-data; ok {
log.Printf("[接收到了数据,开始处理]:%v", r)
} else {
log.Printf("chan 关闭了")
break
}
}
}()
// 写入数据
data <- 1
time.Sleep(2 * time.Second)
data <- 2
time.Sleep(2 * time.Second)
data <- 3
time.Sleep(2 * time.Second)
close(data)
time.Sleep(1 * time.Second)
// 现象是 chan 关闭了没打印
// 如果加上 time.Sleep(1 * time.Second) 就会打印
}
异步模式
- 异步模式channel有缓冲区
- 如果缓冲区已满,发送的主进程或者协程会被阻塞
- 如果未满不会被阻塞
- 如果为空,接收的协程会被阻塞
- 基于这种性质往往需要有个同步channel去控制主进程是否退出,否则有可能协程还未处理完所有的信息,主进程已经退出
- 代码 ```go package main
import ( "log" "time" )
func main() {
// 初始化一个类型为int的chan ,缓冲区为3
data := make(chan int, 3)
quit := make(chan bool) // 达成和最后的time.sleep一样的效果 阻主线程,防止异步任务中有未处理完的就推出
// 读取数据的任务
go func() {
for d := range data {
time.Sleep(2 * time.Second)
log.Printf("[接收到了数据,开始处理]:%v", d)
}
log.Printf("data chan关闭了,但是我还有清理工作,等我5秒钟")
time.Sleep(5 * time.Second)
// 本任务处理完了,告诉主线程可以退出了
quit <- true
}()
// 写入数据
data <- 1
time.Sleep(2 * time.Second)
data <- 2
time.Sleep(2 * time.Second)
data <- 3
time.Sleep(2 * time.Second)
data <- 4
log.Printf("发送4")
data <- 5
log.Printf("发送5")
data <- 6
log.Printf("发送6")
data <- 7
log.Printf("发送7")
data <- 8
log.Printf("发送8")
data <- 9
log.Printf("发送9")
close(data)
<-quit
log.Printf("真正退出了")
}
> 从一个close的chan 中读取到的是0值
- 如果改成 for { <-data} 这样去读取
- 在chan被关闭之后就是读取到的就是0值
```go
package main
import (
"log"
"time"
)
func main() {
// 初始化一个类型为int的chan ,缓冲区为3
data := make(chan int, 3)
quit := make(chan bool) // 达成和最后的time.sleep一样的效果 阻主线程,防止异步任务中有未处理完的就推出
// 读取数据的任务
go func() {
for d := range data {
time.Sleep(2 * time.Second)
log.Printf("[接收到了数据,开始处理]:%v", d)
}
log.Printf("data chan关闭了,但是我还有清理工作,等我5秒钟")
log.Printf("data chan关闭了,我再读取几个值看看:%v", <-data)
log.Printf("data chan关闭了,我再读取几个值看看:%v", <-data)
log.Printf("data chan关闭了,我再读取几个值看看:%v", <-data)
time.Sleep(5 * time.Second)
// 本任务处理完了,告诉主线程可以退出了
quit <- true
}()
// 写入数据
data <- 1
time.Sleep(2 * time.Second)
data <- 2
time.Sleep(2 * time.Second)
data <- 3
time.Sleep(2 * time.Second)
data <- 4
log.Printf("发送4")
data <- 5
log.Printf("发送5")
data <- 6
log.Printf("发送6")
data <- 7
log.Printf("发送7")
data <- 8
log.Printf("发送8")
data <- 9
log.Printf("发送9")
close(data)
<-quit
log.Printf("真正退出了")
}
几种会导致panic的 chan操作
01 close nil 的chan
02 向已关闭的chan 再次close
03 向已关闭的chan 再次写入值 ```go package main
func main() { // panic: close of nil channel //var c1 chan int //close(c1)
// panic: close of closed channel
//c2 := make(chan int)
//close(c2)
//close(c2)
// panic: send on closed channel
c3 := make(chan int)
close(c3)
c3 <- 1
}
## select 和close
- close(ch) 通知所有下游协程 :watch 这个ch的worker
- 使用select处理多个channel
- select可以同时监控多个通道的情况,只处理未阻塞的case。当通道为nil时,对应的case永远为阻塞,无论读写
```go
package main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
)
var quitC = make(chan struct{})
func signalWork() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// 当c中读取到值的时候,说明有人发了信号
sig := <-c
// 通知所有读取 quitC的任务
close(quitC)
time.Sleep(2 * time.Second)
log.Printf("接收到了停止的信号 信号是:%v,pid:=%d,要退出了", sig, os.Getpid())
}
func worker01() {
ticker := time.NewTicker(time.Second * 5)
for {
select {
case <-ticker.C:
log.Printf("[我是woker01][5秒周期到了,干活]")
case <-quitC:
log.Printf("[我是woker01][接受到主进程退出的信号]。进行清理操作")
return
}
}
}
func worker02() {
ticker := time.NewTicker(time.Second * 5)
for {
select {
case <-ticker.C:
log.Printf("[我是woker02][5秒周期到了,干活]")
case <-quitC:
log.Printf("[我是woker02][接受到主进程退出的信号]。进行清理操作")
return
}
}
}
func worker03() {
ticker := time.NewTicker(time.Second * 5)
for {
select {
case <-ticker.C:
log.Printf("[我是woker03][5秒周期到了,干活]")
case <-quitC:
log.Printf("[我是woker03][接受到主进程退出的信号]。进行清理操作")
return
}
}
}
func main() {
go worker01()
go worker02()
go worker03()
signalWork()
}
什么是流程控制
- 控制逻辑走向和执行次序的重要部分
go语言流程控制
- Go中流程控制分三大类:条件判断,循环控制和无条件跳转。
01 条件判断
- if 多条件 ```go package main
import "fmt"
func main() {
x := 11
y := "ok"
if x == 10 && y == "ok" {
fmt.Println("两个都满足")
} else if x > 10 || y == "ok" {
fmt.Println("任意一个")
}
}
- 条件判断语句里面允许声明一个变量
- 这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了
```go
package main
import (
"fmt"
"log"
)
func main() {
m := map[string]string{
"region": "bj",
"idc": "世纪互联",
}
if idc := m["idc"]; idc == "世纪互联" {
log.Printf("机房:%v", idc)
}
idc1 := m["idc"]
if idc1 == "世纪互联" {
log.Printf("机房:%v", idc1)
}
fmt.Println(idc)
fmt.Println(idc1)
}
switch条件判断
- 基础使用
package main
import (
"fmt"
)
func jj(s string) {
switch s {
case "go":
fmt.Println("go")
case "py":
fmt.Println("py")
case "java":
fmt.Println("py")
default:
fmt.Println("unknow")
}
}
func main() {
jj("py")
jj("go")
jj("go1")
}
不允许两个default
- // Multiple defaults in switch
case后面可以多个值
不允许case 两个常量相同 duplicate case "java" in switch
fallthrough 继续判断 ```go package main
import ( "fmt" )
func jj(s string) { java1 := "java" java2 := "java" switch s { case "go": fmt.Println("go") // case接多个 值 case "py", "python": fmt.Println("py")
case java1:
fmt.Println("java")
// 继续判断
fallthrough
case java2:
fmt.Println("java")
// 继续判断
//fallthrough
default:
fmt.Println("unknow")
}
}
func main() {
jj("py")
jj("python")
jj("java")
}
## 02 循环控制
### 单个逻辑表达式
> 死循环
```go
package main
import (
"log"
"time"
)
func main() {
for {
log.Printf("我干活呢")
time.Sleep(3 * time.Second)
}
}
单条件判断 ```go package main
import ( "log" )
func main() { var a, b int b = 10 for a < b { log.Printf("我干活呢", a, b) a++ //time.Sleep(3 * time.Second) } }
### 双分号
- for语句被两个分号分割为3个表达式
- 第一个表示为初始化(只会在第一次条件表达式之计算一次)
- 第二个表达式为条件判断表达式
- 第三个表达式一般为自增或自减,但这个表达式可以任何符合语法的表达式
- 而且这三个表达式, 只有第二个表达式是必须有的,其他表达式可以为空
```go
package main
import "log"
func main() {
for i := 0; i < 5; i++ {
log.Printf("第1种 三段全的")
}
for i := 0; i < 5; {
log.Printf("第2种 自增写在里面")
i++
}
var i int
for ; i < 5; {
log.Printf("第3种 自增写在里面 ,初始化写在上面")
i++
}
}
for和range结合的语句
- 用range来迭代数据是最常用的一种for语句
- range右边的表达式叫范围表达式
- 范围表达式可以是数组,数组指针,slice,字符串,map和channel ```go package main
import "log"
func main() { s1 := []int{10, 20, 30, 40, 50} m1 := map[string]string{"k1": "v1", "k2": "v2", "k3": "v3"}
for index := range s1 {
log.Printf("[切片][range 遍历单变量 只有索引][index:%v]", index)
}
for index, value := range s1 {
log.Printf("[切片][range 遍历双变量 索引和值][index:%v][value:%v]", index, value)
}
for key := range m1 {
log.Printf("[map][range 遍历单变量 只有key][key:%v]", key)
}
for key, value := range m1 {
log.Printf("[map][range 遍历双变量 key,value][key:%v][value:%v]", key, value)
}
}
> for range 给指针变量赋值时的问题
- 现象 给指针切片元素赋值的时候,值都是最后一个
```go
package main
import "log"
func main() {
a1 := make([]*int, 3)
a2 := make([]int, 3)
for k, v := range []int{1, 2, 3} {
log.Printf("[v的值:%v][v的地址:%p]", v, &v)
a1[k] = &v
a2[k] = v
}
for i := range a1 {
log.Printf("[指针切片的值为:%v]", *a1[i])
}
for i := range a2 {
log.Printf("[普通值类型切片的值为:%v]", a2[i])
}
/*
2021/07/18 16:18:36 [v的值:1][v的地址:0xc0000ac058]
2021/07/18 16:18:36 [v的值:2][v的地址:0xc0000ac058]
2021/07/18 16:18:36 [v的值:3][v的地址:0xc0000ac058]
2021/07/18 16:18:36 [指针切片的值为:3]
2021/07/18 16:18:36 [指针切片的值为:3]
2021/07/18 16:18:36 [指针切片的值为:3]
2021/07/18 16:18:36 [普通值类型切片的值为:1]
2021/07/18 16:18:36 [普通值类型切片的值为:2]
2021/07/18 16:18:36 [普通值类型切片的值为:3]
*/
}
问题原因
//实例代码 array :=[2]int{1,2} for k,v :=range array{ f(k,v) } //会被编译成 len_temp := len(array) range_temp :=array for index_temp =0 ;index_temp<len_temp;index_temp++{ value_temp= range_temp[index_temp] k = index_temp v = value_temp f(k,v) }
两个临时变量index_temp,value_temp, 在整个遍历中一直复用这两个变量
- 所以&v得到的地址一直都是相同的
- 解决方法 v:=v
continue和break ```go package main
import ( "fmt" "log" "strings" )
func main() {
m1 := make(map[string]string)
for i := 0; i < 30; i++ {
key := fmt.Sprintf("%d_key", i)
value := fmt.Sprintf("%d_value", i)
m1[key] = value
}
for k, v := range m1 {
if strings.HasPrefix(k, "1") {
log.Printf("[遇到1就continue]")
continue
}
if k == "23_key" {
log.Printf("[遇到23就break]")
break
}
log.Printf("[正常处理数据][%v=%v]", k, v)
}
}
## 03 无条件跳转
- goto代表无条件跳转
- 下面的例子会一直打印
```go
package main
import "log"
func main() {
i := 0
sum:
log.Printf("[i=%d]", i)
i++
goto sum
}
- 结合if可以控制 ```go package main
import "log"
func main() { i := 0 sum: { log.Printf("[i=%d]", i) i++ } if i <= 100 { goto sum }
}
- goto 副作用 在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。
# 函数定义
```go
func function_name( [parameter list] ) [return_types] {
函数体
}
- func:函数由 func 开始声明
- function_name:函数名称,函数名和参数列表一起构成了函数签名。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合。
简单使用
package main
import "fmt"
func max(n1, n2 int) int {
if n1 > n2 {
return n1
}
return n2
}
func main() {
fmt.Println(max(1, 10))
fmt.Println(max(-1, -2))
}
Go语言支持对返回值进行命名
- 命名的返回值变量的默认值为类型的0值
- 相当于var xxx
- map必须使用前make
package main
import "fmt"
func f1() (names []string, m map[string]int, num int) {
m = make(map[string]int)
m["k1"] = 2
return
}
func main() {
a, b, c := f1()
fmt.Println(a, b, c)
}
不定长参数
- 如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
- 函数内部的参数是数组
- 外部传参为数组时 ,可以使用 arr... ```go package main
import "log"
// 变长参数 返回最小值 func min(a ...int) int { if len(a) == 0 { return 0 } min := a[0] for _, v := range a { if v < min { min = v } } return min }
func main() { x1 := min(1, 7, 8, 3, 23, 9) log.Printf("[直接传多个参数]:%d", x1) s1 := []int{2, 3, 54, 10, 32, 5, 7, 34} x2 := min(s1...) log.Printf("[数组传参传]:%d", x2) }
## 按值传递 or 按引用传递
- Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量
- 如果希望值传递类型在函数中也可以修改变量,那么应该传递指针
- 引用类似的数据都是引用传递的,slice map chan interface
> 值类型
```go
package main
import (
"log"
"time"
)
// 值类型 值传递
func add1(num int) {
log.Printf("[值传递][传入的参数值为:%d]", num)
num++
log.Printf("[值传递][add1计算后的值为:%d]", num)
}
// 值类型 引用传递
func add2(num *int) {
log.Printf("[引用传递][传入的参数值为:%d]", *num)
*num++
log.Printf("[引用传递][add2计算后的值为:%d]", *num)
}
func main() {
num := 1
log.Printf("[局部遍历的值:%d]", num)
add1(num)
time.Sleep(1 * time.Second)
log.Printf("[局部遍历的值:%d]", num)
add2(&num)
time.Sleep(1 * time.Second)
log.Printf("[局部遍历的值:%d]", num)
}
引用类型 ```go package main
import ( "log" )
// 引用类型 引用传递 func mod(s1 []int, m1 map[string]string) {
log.Printf("[引用传递][传入的参数为:%v %v]", s1, m1)
s1[0] = 100
m1["a"] = "a2"
log.Printf("[引用传递][函数内部处理完的值为:%v %v]", s1, m1)
} func main() { s1 := []int{1, 2, 3} m1 := map[string]string{"a": "a1", "b": "b1"} mod(s1, m1) log.Printf("[引用传递][函数外部的值为:%v %v]", s1, m1) }
## 匿名函数
- 匿名函数最大的用途是来模拟块级作用域,避免数据污染的。
> 不带参
```go
package main
import "fmt"
func main() {
f := func() {
fmt.Println("abdc")
}
f()
fmt.Printf("%T", f)
}
带参数的
package main
import "fmt"
func main() {
f := func(args string) {
fmt.Println(args)
}
f("abdc")
fmt.Printf("%T", f)
}
带返回值 ```go package main
import "fmt"
func main() {
f := func() string { return "abdc" } a := f() fmt.Printf("%T\n", f) fmt.Println(a)
}
> 返回多个匿名函数
```go
package main
import "fmt"
func FGen(x, y int) (func() int, func(int) int) {
//求和的匿名函数
sum := func() int {
return x + y
}
// (x+y) *z 的匿名函数
avg := func(z int) int {
return (x + y) * z
}
return sum, avg
}
func main() {
f1, f2 := FGen(1, 2)
fmt.Println(f1())
fmt.Println(f2(3))
}
闭包
- 闭包:说白了就是函数的嵌套,内层的函数可以使用外层函数的所有变量,即使外层函数已经执行完毕。
- 不同闭包函数变量内部维护不是同一个变量
- 所有的匿名函数都是闭包 ```go package main
import "fmt"
func Greeting() func(string) string { a := " 你好啊 " return func(s string) string { a += s return a } }
func main() {
g1 := Greeting()
g2 := Greeting()
fmt.Println(g1("小乙"))
fmt.Println(g1("李逵"))
fmt.Println(g1("宋江"))
fmt.Println(g2("宋江"))
fmt.Println(g2("李逵"))
}

- 累加器例子
```go
package main
import (
"log"
)
func add1() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
log.Printf("[普通的累加器]:%d", sum)
}
func add2() func(int) int {
// 自由变量
sum := 0
return func(i int) int {
sum += i
return sum
}
}
func callCF() {
f := add2()
sum := 0
for i := 0; i < 10; i++ {
sum = f(i)
log.Printf("[闭包的累加器]:%d", sum)
}
}
func main() {
add1()
// 闭包的
callCF()
}
- 函数式编程的应用场景
- 对于数据的长流程处理
- 类似流水线,装配模式
- 可以随时增删流程
- 装饰器
递归函数必须使用闭包
递归函数定义
- 递归指函数可以直接或间接的调用自身。
- 递归函数通常有相同的结构:一个跳出条件和一个递归体
- 所谓跳出条件就是根据传入的参数判断是否需要停止递归,而递归体则是函数自身所做的一些处理。
- 斐波拉契数列
package main
import "log"
func fib(i int) int {
if i == 0 || i == 1 {
return i
}
return fib(i-1) + fib(i-2)
}
func main() {
log.Println(fib(7))
for i:=0;i<10;i++{
log.Printf("[%d=%d]",i,fib(i))
}
}
- 1+2+....100
- 从低位加从高位加 ```go package main
import ( "log" )
// 实现1+...+100 // 从低位加到高位 func sum1(num int) int { if num == 100 { return num } return num + sum1(num+1) }
// 从高位加到低位 func sum2(num int) int { if num == 1 { return num } return num + sum2(num-1) }
func main() { log.Printf("从低位加到高位 :%d", sum1(1)) log.Printf("从高位加到低位 :%d", sum2(100)) }
## defer语句
- 类似于栈,类比python中的 上下文管理器 `__exit__`
- Go 语言的 defer 会在当前函数返回前执行传入的函数
- 它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源
> defer 保证文件描述符被close
```go
f,err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
倒序打印num ```go package main
import "fmt"
func main() { for i := 0; i < 5; i++ { defer fmt.Println(i) } }
> defer 预计算参数 和defer func()
- 使用defer打印时间差
```go
package main
import (
"log"
"time"
)
func main() {
start := time.Now()
log.Printf("开始时间为:%v", start)
defer log.Printf("时间差:%v", time.Since(start))
time.Sleep(3 * time.Second)
log.Printf("函数结束")
/*
时间差不是3秒,而是很小
2021/07/18 18:04:10 开始时间为:2021-07-18 18:04:10.6563346 +0800 CST m=+0.004339401
2021/07/18 18:04:13 函数结束
2021/07/18 18:04:13 时间差:43.6851ms
*/
}
- 造成上述问题的原因
- Go 语言中所有的函数调用都是传值的
- 调用 defer 关键字会立刻拷贝函数中引用的外部参数 ,包括startAt 和time.Since中的Now
- defer的函数在压栈的时候也会保存参数的值,并非在执行时取值。
- 怎么解决这个问题:使用defer func() ```go package main
import ( "log" "time" )
func main() { start := time.Now() log.Printf("开始时间为:%v", start) defer func() { log.Printf("开始调用defer") log.Printf("时间差:%v", time.Since(start)) log.Printf("结束调用defer") }() time.Sleep(3 * time.Second)
log.Printf("函数结束")
/*
2021/07/18 18:09:01 开始时间为:2021-07-18 18:09:01.7131174 +0800 CST m=+0.004520101
2021/07/18 18:09:04 函数结束
2021/07/18 18:09:04 开始调用defer
2021/07/18 18:09:04 时间差:3.0482416s
2021/07/18 18:09:04 结束调用defer
*/
}
- 因为拷贝的是函数指针
# defer 和return
- f1返回1
```go
func f1() (res int) {
defer func() {
res++
}()
return 0
}
原理解读
- return xxx这条语句不是一条原子操作
- 先给返回值赋值
- return 1要翻译成 res =1 +return
- 然后调用defer语句
- 最后返回调用函数中 ,空的return
```shell script 返回值 = xxx 调用defer 函数 空的return
> f1可以改造成
```go
func f11() (res int) {
res = 0
func() {
res++
}()
return
}
f2改造成 ```go func f22() (res int) { t := 5 res = t // 真正的res返回值 赋值 func() { t = t + 5 }() return }
> f3改造成
```go
func f33() (res int) {
res = 0 // 赋值指令
func(res int) {
res = res + 5 //值传递,深拷贝, 操作的是副本
}(res)
return // 空的return
}
- f4的结果是什么
- 为什么?
- 改造成解释defer和return的顺序函数 ```go package main
import "fmt"
func f4() (t int) {
defer func() {
t = t * 10
}()
return 1
}
func main() { fmt.Println(f4()) }
> f4 改造成这样
```go
func f44() (t int) {
t = 1 // 先给返回值赋值
func() {
t = t * 10 // 匿名函数,引用闭包的自由变量 t ,会直接更改t的值
}()
return
}
全部的示例 ```go package main
import ( "fmt" "log" )
// 返回1 func f1() (res int) { defer func() { res++ }() return 0 }
func f11() (res int) { res = 0 func() { res++ }() return
}
// 匿名不带参 func f2() (res int) { t := 5 defer func() { t = t + 5 }() return t }
// f2可以改造为 func f22() (res int) { t := 5 res = t // 真正的res返回值 赋值 func() { t = t + 5 }() log.Printf("f22.t=%d",t) return }
// 匿名带参数 func f3() (res int) { defer func(res int) { res = res + 5 }(res) return res } func f33() (res int) { res = 0 // 赋值指令 func(res int) { res = res + 5 //值传递,深拷贝, 操作的是副本 }(res)
return // 空的return
}
func f4() (t int) {
defer func() {
t = t * 10
}()
return 1
}
func f44() (t int) { t = 1 // 先给返回值赋值 func() { t = t * 10 // 匿名函数,引用闭包的自由变量 t ,会直接更改t的值 }()
return
}
func main() { fmt.Println(f1()) fmt.Println(f11()) fmt.Println(f2()) fmt.Println(f22()) fmt.Println(f3()) fmt.Println(f33()) fmt.Println(f4()) fmt.Println(f44()) }
```
defer 和 panic
- panic会触发defer出栈执行
- defer 最大的功能是 panic 后依然有效
- 所以defer可以保证你的一些资源一定会被关闭,从而避免一些异常出现的问题。
不用recovery捕获异常
使用recovery捕获异常
- panic之后的defer 还没来得及压栈
defer中含有panic
- 这个异常将会覆盖掉main中的异常panic("panic"),最后这个异常被第二个执行的defer捕获到。
defer中函数参数包含子函数
- 先压栈1,再压栈2
- 子函数在压栈是就要执行获取返回值作为参数
- 因为压栈时需要连同函数地址、函数形参一同进栈
- 所以调用顺序为 压栈f1,调用f3,压栈f2,调用f4 调用f2,f1
运算子函数
- 看起来是根据string调用的函数
- 其实是根据map的映射搞得
- init函数默认被调用,其实是在包导入的时候