-
- 10.1. 下划线在import中
- 10.2. 下划线在代码中
-
- 13.1. 整型
- 13.2. 浮点型
- 13.3. 复数
- 13.4. 布尔
- 13.5. 字符串
- 13.5.1. 字符串常用操作
- 13.6. byte和rune类型
- 13.7. 基本类型的强制类型转换
-
- 26.1. 函数返回值步骤
-
- 34.1. method value
-
- 37.1. goproxy protocol
- 37.2. 私有git库免密拉取
-
- 44.1. 互斥锁Mutex
- 44.2. 读写互斥锁RWMutex
- 44.3. sync.WaitGroup
- 44.4. sync.Once
-
- 46.1. 测试
- 46.2. 基准测试benchmark
- 46.3. Main
基础
1. 目录
├─bin # 执行文件
├─pkg # 外部引入package
└─src # go代码
└─project01 # 项目目录
├─coupon# coupon包
└─main # main包,入口文件
2. GOROOT
go bin的路径
3. GOPATH
代码的路径,现在又module管理,都不需要设置了
4. go mod init path
project01是项目的根目录,此时需要调用go mod init project01
让project01成为模块根,当调用其他package时就可以直接用project01/coupon
绝对目录来引用。不需要将代码放置gopath的目录下了
5. 内置函数
- append
6. 代码可见性
- 大写字母开头的就是对其他包(package)可见
- 小写字母开头就是所在包(package)可见,其他package不可见
7. init函数
- init函数不能被其他函数调用,并且在main函数之前自动被调用
- 每个包可以拥有多个init函数
- 每个包中的源文件也可以拥有多个init函数
- 同一个包中多个init函数的执行顺序没有明确定义
- 不同包中的init函数按照导入的依赖关系决定init函数的执行顺序
8. main函数
- main函数只能属于main包
- 一个main包只能有一个main函数
9. 运算符
9.1. 算数运算符
+
,-
,*
,/
,%
9.2. 关系运算符
==
,!=
,>
,>=
,<
,<=
9.3. 逻辑运算符
&&
,||
,!
9.4. 位运算符
运算符 | 描述 |
---|---|
& | 二进制 与 |
| | 二进制 或 |
^ | 二进制 或与,两个不同时位1 |
<< | n<<m n二进制向左移m位,相当于n * 2^m。二进制低位补0 |
>> | n>>m n二进制向右移m位,相遇当n/2^m |
10. 下划线
10.1. 下划线在import中
当下划线在import中时,只引入该包的init()
方法,其他的方法不引入
代码结构
└─project01 # 项目目录
├─hello# hello包
└─test.go
└─main # main包,入口文件
└─main.go
main.go
package main
import _ "project01/hello"
func main() {
hello.Hello()
//编译错误,.\main.go:6:2: undefined: hello
}
test.go
package hello
import (
"fmt"
)
func init() {
fmt.PrintLn(123)
}
func Hello() {
fmt.PrintLn(456)
}
10.2. 下划线在代码中
下划线相当于占位符,把不想要的值赋值给下划线
package main
import (
"os"
)
func main() {
buf := make([]byte, 1024)
f, _ := os.Open("/Users/***/Desktop/text.txt")
}
11. 变量
- 同一作用于不支持重复声明
- 声明后必须使用
- 变量在声明时会根据类型定义自动初始化
11.1. 标准声明
// 单个变量声明
var param type
// 多变量声明
var (
param1 type
param2 type
)
11.2. 类型推导
var name = "is string"
var num = 1
11.3. 短变量声明
在函数内部可以使用更简洁的:=
方式声明并初始化变量
package main
import "fmt"
var num int //全局变量
func main() {
num := 123 //局部变量
fmt.Println(num)
}
12. 常量
- 声明时必须初始化
- 声明后不可更改
//单常量声明
const pi = 3.1415
//多常量声明
const (
pi = 3.14
e = 2.7182
)
//多常量,如果省略值,和前一行赋值一样
const (
pi1 = 3.14
pi2
pi3
)
13. 基本类型
13.1. 整型
- 长度:
int8
,int16
,int32
,int64
- 无符号:
uint8
,uint16
,uint32
,uint64
13.2. 浮点型
float32
,float64
13.3. 复数
-
y = ax+b a是虚部 b是实部
-
complex64
:实部和虚部为32位 -
complex32
:实部和虚部为64位
13.4. 布尔
true
orfalse
13.5. 字符串
- 单行字符串:双引号包"含着的
- 多行字符串:用反引号`包含着
13.5.1. 字符串常用操作
len(str)
:求长度str + str
orfmt.Sprintf()
:拼接字符串strings.Split()
:分割strings.Contains()
:是否包含strings.HasPrefix()
:前缀判断strings.hasSuffix()
:后缀判断strings.Index()
orstrings.LastIndex()
:子串出现的位置strings.Join()
:
13.6. byte和rune类型
组成字符串的单个元素叫做字符。用单引号包含着
//byte,asc2码
var b := 'x'
//rune类型,utf-8字符,一个rune字符由一个或多个byte组成
var a := '发'
13.7. 基本类型的强制类型转换
string(param)
int(param)
# string 转 int
strconv.Atoi(int_param)
strconv.Itoa(str_param)
14. 数组array
golang 数组和以为数组有很大不同:
- 是一种固定长度并且同一个数组中数据类型一致的序列
- 访问越界会报错
- 数组是值类型,赋值和传参会赋值整个数组,而不是指针。当改变副本时,原数组不会改变
14.1. 初始化
14.1.1. 一维数组
//通过数字确定数组长度,长度5
var arr0 [5]int = [5]int{1,2,3,4,5}
var arr1 = [5]int{1,2,3,4,5}
//通过初始化确定长度,长度6
var arr2 = [...]int{1,2,3,4,5,6}
//初始化局部
var arr3 = [5]string{3: "this is 3", 5: "this is 5"}
//结构数组
var arr4 = [...]struct{
name string
sex int8
}{
{"qiu", 1},
{"luo", 0},
}
14.1.2. 多维数组
var arr1 = [...][3]int{
{1,2,3},
{2,3,4}
}
15. slice
- zslice切片是数组的一个引用,切片不储存任何数据,它只是描述了底层数组中的一段,修改切片会修改其底层数组中对应的元素
- 切片是引用类型,零值是
nil
,即没有分配内存地址。 - 切片长度可以改变,是一个可变长度的数组
- 切片的切割遵守 左闭右开( s[0:8] 就是切割index 0 -7的元素)
- 切片拥有长度( len(s) )和容量 ( cap(s) )
- 长度是当前切片内的元素
- 容量是切片的开端到底层数组的末端长度,例如 数组有10个元素,切片等于s[4:8],此时长度是4=(8-4),容量是6=(10-4)
- 切片再切片(设计到底层,具体看示例)
15.1. 初始化
// 不需要定义长度的是slice,需要长度的是数组
//全局
var a []int
//局部
b := []int{}
//初始len长度的slice,最大扩容是cap
c := make([]int, len_int, cap_int)
15.2. 操作
arr := [...]int{1,2,3,4,5,6,7,8,9}
slice1 := arr[:]
value := slice1[n]
len(slice1)
cap(slice1)
-
s[n]
:获取切片中key为n的值 -
s[:]
:从切片/数组位置0到len(s-1)所获得的切片 -
s[start:end]
:从切片/数组位置start到end所获得的切片 -
len()
:获取切片/数组长度 -
cap()
:获取切片最大扩容数
15.3. 内存分布
a1 := [...]int{1,2,3,4,5,6,7,8,9}
s1 := a1[2:4]
s2 := a1[1:4]
//两个内存地址是相同的,说明两个slice.pointer指向同一个数组
fmt.Println(&s1[0], &s2[1]) // 0xc000018240 0xc000018240
15.4. 切片再切片
从上图可以看出切分的结构体是
type slice struct {
*Pointer
Len int
Cap int
}
- *Pointer是指向底层指针的数组
- Len是切片内元素个数
- Cap是切分的容量
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s) //len=6 cap=6 [2 3 5 7 11 13]
// 截取切片使其长度为 0
s1 := s[:0]
printSlice(s1) //len=0 cap=6 []
// 拓展其长度
s2 := s1[1:3]
printSlice(s2) //len=2 cap=5 [3 5]
// 舍弃前两个值
s = s[2:]
printSlice(s) //len=4 cap=4 [5 7 11 13]
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
上述输出解释:
- s[:0],切了0:0所以元素个数是0个,可是从切片开端到数组末端就是底层数组的长度,所以cap是6
- s1[1:3],是其实就是根据s1的Pointer找到底层数组,切底层数组的1:3,所以返回了[3,5],cap从切片开端2开始(3-1 左闭右开)到底层数组末端6,所以返回4
15.5. append
a := [...]string{1,2,3,4,5,6,7,8,9}
s := a[:]
s = append(s, 10)
当往一个满容量的切分中追加元素,会发生扩容,内存地址会发生改变,底层会拷贝出一个一样的数组,Pointer指向新数组地址
15.6. string 转 byte切片数组
str := "abcdef"
arr := [...]byte(str)
16. 指针
&
取地址符,获取变量的内存地址*
根据内存地址取值- 每种指针 根据内存地址内的值的类型 不同还划分了指针类型
*int *int64 *string
等
func main() {
//取指针
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
//根据内存地址取值
c := *b
fmt.Println(c) // 10
//给空指针赋值
var d *int
d = new(int) //一般很少用new
*d = 10
fmt.Println(*d) //10
}
16.1. 空指针
- 当一个指针被定义后,没有分配到任何变量时,他的指为
nil
- 指针的定义
var ptr *string
- 空指针的判断
if ptr == nil
16.2. 指针的初始化和赋值
16.2.1. new
- new内置函数的签名
func new(Type) *Type
new()函数返回的是一个指针
- Type:类型,定义了指针指向类型
- *Type:new函数的返回指针
var p *Type
只是声明了一个指针变量并没有初始化,指针变量需要初始化后才会分配内存空间,之后才能进行赋值操操。new()
函数及分配内存空间
func main() {
a := 3
var ptr *int
ptr = new(int)
ptr = &a
fmt.Println(*ptr) //print 3
}
16.2.2. make
make也是分配内存的作用,区别于new(),它只用于slice
,map
,channel
的内存创建,而且它返回的类型就是这3个类型的本身,而不是他们的指针,因为这3种类型本来就是引用类型,所以不需要返回他们的指针。
map内置函数的签名
func make(t Type, size, ...IntegerType) type
func main() {
s1 := []string{"a", "b", "c", "d", "e", "f"}
s2 := make([]string, 6)
s2 = s1
fmt.Printf("%p\n", &s1) //0xc000004078
fmt.Printf("%p\n", s1) //0xc0000220c0
fmt.Printf("%p\n", s2) //0xc0000220c0
}
引用类型:内存存放的是 数据的内存地址。所以修改数据时,两个指针指向同一个地址时他们的值都会被修改!!!!!!!!!!!!!!
17. map
map是一种无序的key-value数据结构,map是引用类型,必须初始化后才能赋值。
- map定义
map[keyType]valueType
keyType:键类型
valueType: 值类型
- map初始化
make(map[keyType]ValueType [, cap]) //cap是map的容量大小,不设置也会默认给定容量大小
var map1 = make(map[string]int, 8) // 可以不指定长度,可是slice必须指定长度
- 声明时填充元素
func main() {
map1 := map[string]int{
"key1": "value1",
"key2": "value2",
}
}
- map的无序遍历:go并不会按定义时的kv顺序进行遍历
map1 := make(map[string]string)
map1["name"] = "alei"
map1["age"] = "18"
for k,v := range map1 {
fmt.Println("k:"+k)
fmt.Println("v:"+v)
}
- key是否存在
value, boolval := map[]
func main() {
map1 := make(map[string]string)
map1["name"] = "alei"
map1["age"] = "18"
_, exist := map1["key"] // '_'占位符,不需要的值
if exist {
fmt.Println("yes")
} else{
fmt.Println("no") //print no
}
}
- 删除键值对
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
delete(scoreMap, "小明")//将小明:100从map中删除
fmt.Println(scoreMap["小明"]) //print 0
_, isExist := scoreMap["小明"]
if isExist {
fmt.Println("yes")
} else {
fmt.Println("no") //print no
}
}
18. 结构体
18.1. type关键字
18.1.1. 创建新类型
是一个新的类型,并且可以创建自己类型的方法
type MyString string
func (ms MyString) call () {
fmt.Println("is mystring")
}
func main(){
var a MyString = "sas"
a.call() // is mystring
fmt.Printf("type is %T", a) // type is main.MyString
}
18.1.2. 类型别名
只是换了一种类型的名字,无法创建自己的方法
type MyString = string
func main(){
var a MyString = "sas"
fmt.Printf("type is %T", a) // type is string
}
18.1.3. 结构体
18.2. 结构体
结构体是一种自定义数据类型,可以封装多个基础数据类型。go没有类和继承,他通过结构体来实现面向对象
- struct定义
type comsuter struct {
fields_name Type
fields_name Type
}
//实例
type student struct {
name string
age int
sex int
}
18.3. 实例化
- 结构体基本实例化,他也是一种引用类型,所有需要先分配内存才能赋值
type student struct {
name string
age int
sex int
}
- 构造体实例化
返回的是结构体内容,并不是地址
var student1 student
student1.name = "qiu"
student1.age = 18
fmt.Println(student1) //print {qiu 18}
- 匿名结构体基础实例化
var student1 struct{name string, age int, sex int}
new()
创建内存指针,实现实例化。注意new()
返回的是结构体的地址
type student struct {
name string
age int
sex int
}
student1 := new(student)
student1.name = "测试" //student1.name = "博客"其实在底层是(*student1).name = "博客",这是Go语言帮我们实现的语法糖。
student1.age = 18
fmt.Println(student1) //print &{qiu 18 0} 这里是一指针,可是返回的不是内存地址的值
- 取结构体的地址实例化
&struct
相当于new()
实例化操作,注意同new也是返回结构体的地址
type student struct {
name string
age int
sex int
}
student1 := &student
student1.name = "测试" //student1.name = "博客"其实在底层是(*student1).name = "博客",这是Go语言帮我们实现的语法糖。
student1.age = 18
fmt.Println(student1) //print &{qiu 18 0} 这里是一指针,可是返回的不是内存地址的值
18.4. 初始化
初始化已经把实例化合为一步。
- 使用键值对 对结构体进行实例化
type student struct {
name string
age int
sex int
}
func main() {
student1 := student{
name: "qiu",
age : 18,
}
fmt.Println(student1) //print {qiu 18 0}
}
- 使用键值对 对结构体指针进行实例化,效果同上
type student struct {
name string
age int
sex int
}
func main() {
student1 := &student{
name: "qiu",
age : 18,
}
fmt.Println(student1) //print &{qiu 18 0}
}
- 省略键值 ,必须要所有字段都初始化 并且 按顺序
type student struct {
name string
age int
sex int
}
func main() {
student1 := &student{
"qiu",
18,
1,
}
fmt.Println(student1) //print &{qiu 18 0}
}
- 结构体定义和初始化同时定义
package main
import "fmt"
func main() {
struct1 := struct {
a int
b string
}{
a : 10,
b : "string",
}
fmt.Printf("struct1 = %v", struct1)
}
结构体的内存是连续的内存地址
18.5. 构造函数
Go语言没有类的概念,所以没有构造函数。不过可以自己去实现一个构造函数
type student struct {
name string
age int
sex int
}
func newStudent(name string, age int, sex int) student{
return student{
name,
age,
sex,
}
}
func main() {
stu := newStudent("qiu", 17, 1)
fmt.Println(stu.name)
}
18.6. 方法和接收者
go语言的方法是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者。
18.6.1. 定义格式
func (接收者 接收者类型) 方法名(参数列表) 返回参数 {
}
18.6.2. 指针类型的接收者 实例
- 指针类型接收者,可以在任意位置更改唯一属性内容
package main
import "fmt"
type student struct {
name string
age int
}
func (stu *student) setAge (age int) int {
stu.age = age
return age
}
func main () {
a := student{
"qiushenglei",
18,
}
fmt.Println(a.age) //print 18
a.setAge(19)
fmt.Println(a.age) //print 19 指针引用,相当于php的对象引用
}
18.6.3. 值类型的接收者 实例
package main
import "fmt"
type student struct {
name string
age int
}
func (stu student) setAge (age int) int {
stu.age = age
return age
}
func main () {
a := student{
"qiushenglei",
18,
}
fmt.Println(a.age) // print 18
a.setAge(19)
fmt.Println(a.age) // print 18
}
18.6.4. 结构体的嵌套
嵌套结构体的子结构不要使用匿名字段(有重名的情况),否则程序产生歧义,无法编译
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "pprof",
Gender: "女",
Address: Address{
Province: "黑龙江",
City: "哈尔滨",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
18.6.5. 结构体的继承
通过两个结构体嵌套
package main
import "fmt"
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: Animal{
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
18.6.6. 结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
变量结构
- 一个变量有两个属性
- type, 变量的类型,类型又划分为两种
- 静态类型,例如int, string, slice
- 动态类型,interface
- type, 变量的类型,类型又划分为两种
- value, 变量的值
断言
- 当一个参数是动态类型时,对某种类型做特殊处理时,可以使用断言
- 使用方法 var.(Type)
var a interface{} // 必须时interface,否则后面a.(int)会编译失败
a = 3
if value,ok := a.(int); ok {
fmt.Println("a is int, value is ", value)
} else {
fmt.Println("a is not int")
}
反射 reflect
流程控制
19. if条件判断
19.1. 使用1
if语句的init 定义的变量,作用域只在if / else 结构体
if init; condition {
}
//案例
if err := recover() ; err != nil {
fmt.Printf("error = %v", err)
}
19.2. 使用2
if condition [&& | || condition] {
} else if condition {
} else {
}
20. switch
switch从上执行到下,直到匹配某个case为止。可以省略break,默认自动终止。
var
和case里面的数据类型必须保持一致
switch var {
case val1:
case val2:
case val3, val4, val5:
default:
}
// 省略var参数,相当于switch true,此时case应该设置成判断条件
switch {
case a<10:
fmt.Println("A less than 10")
case a<20:
fmt.Println("A less than 20")
default:
fmt.Println("A more than 20")
}
21. select
会阻塞监听 信号
,channel
等
- 如果使用
for
语句嵌套,就会轮询监听 - 如果没有
for
语句嵌套,监听到一个case就跳出select
func reqTask(ctx context.Context, name string) {
for {
select {
// 其实这里还是监听信号, **多个子协程**不用争抢,都会监听到信号,不像channel多协程时,只有一个协程获得
case <-ctx.Done():
fmt.Println("stop", name)
return
default:
fmt.Println(name, "send request")
time.Sleep(1 * time.Second)
}
}
}
22. 循环for
共有3中for的使用方式
- 普通for
//init; condition; post
for a:=0; a<10; a++ {
}
- 替代while
// condition
for a<10 { //替代了while
}
- 死循环
for {
break;
}
23. 循环语句range
range类似于迭代器,返回 【索引,值】 或 【键,值】
range可以对 slice、map、array、string、channel等进行迭代循环。
for k,v := range var1 {
}
函数
24. 函数定义
- 参数需要定义类型
param Type
- 多个连续一样类型的参数,除了最后一个不能省略类型,其他的都可以省略
a, b int
- 如果有返回值,需要定义返回值类型列表
- 可以有0个或多个返回值
package main
import "fmt"
func main() {
map1 := map[string]string{
"a": "a",
"b": "b",
}
str1 := "nihao"
r1, r2 := funcName(1, 2, str1, map1)
fmt.Println(r1, r2)
}
func funcName(a, b int, c string, d map[string]string) (map[string]string, string) {
return d,c
}
25. 函数参数
25.1. 参数传递类型
- 值传递,默认是值传递,当调用方法时,实际是复制了一份参数到函数内,函数内对数据进行修改不会影响外部变量
- 引用传递,当调用方式时,是复制了一份引用类型参数的内存地址并传递到函数中,函数内对数据进行修改会影响到外部变量
package main
import "fmt"
func main() {
map1 := map[string]string{
"a": "a",
"b": "b",
}
str1 := "nihao"
noRet(&str1, map1)
fmt.Println(map1) // print map[a:cccc b:b]
}
func noRet(c *string, d map[string]string) {
d["a"] = "cccc"
fmt.Println(*c) // print nihao
}
25.2. 不定参数传参
不定参数传值就是函数的参数个数和类型是不确定的。它的本质上就是一个slice参数接受了传参,只能定义再函数的最后一个参数
package main
import "fmt"
func main() {
map1 := map[string]string{
"a": "a",
"b": "b",
}
str1 := "nihao"
arr1 := [...]string{"a", "b", "c", "d"}
//r1, r2 := funcName(1, 2, str1, map1)
//fmt.Println(r1, r2)
sendSlice(&str1, map1, 1, 2, 4)
sendInterface(&str1, map1, 1, 2, arr1)
}
func sendSlice(c *string, d map[string]string, e ...int) {
fmt.Printf("%T", e) //print []int[1 2 4] 返回的类型是slice
fmt.Println(e) //print [1, 2, 4]
}
func sendInterface(c *string, d map[string]string, e ...interface{}) {
fmt.Printf("%T", e) //print []interface {} 返回的类型是interface
fmt.Println(e) //print [1 2 [a b c d]]
}
26. 函数返回值
- 如果有返回值,函数体必须定义返回值类型。
- 能有多个返回值
- 在接受返回值时,可以使用
_
占位符忽略某个值 - 没有参数的
return
语句返回各个返回变量的当前值
package main
import "fmt"
func main() {
map1 := map[string]string{
"a": "a",
"b": "b",
}
str1 := "nihao"
r1, r2 := funcName1(1, 2, str1, map1)
_, r3 := funcName2(1, 2, str1, map1) // '_'占位符忽略了e返回值
fmt.Printf("r1 = %v\n", r1) // print r1 = map[a:a b:b]
fmt.Println("r2 = " + r2) // print r2 = nihao
fmt.Println("r3 = " + r3) // print r3 = this is f
}
//隐式返回值
func funcName1(a, b int, c string, d map[string]string) (map[string]string, string) {
return d,c
}
//显示返回值,在函数定义时,定义了参数名
func funcName2(a, b int, c string, d map[string]string) (e map[string]string, f string) {
e = d
f = "this is f"
return
}
26.1. 函数返回值步骤
如果方法返回值有变量申明
- 在方法第一行就会创建变量
- return前,会将return的值赋值给变量
func test () (a string, b int) {
// 虚拟 a:="" b:=0
x:="aaa"
y:=4
defer func() {
fmt.Printf("x:%v y:%v", x, y) //x="aaa", y=4
}()
return x+bbb, b-1 //a="aaabbb", b=3,
}
func test1 () (x string, y int) {
x="aaa"
y=4
defer func() {
fmt.Printf("x:%v y:%v", x, y) //x="aaabbb", y=3 这里还能看来,先执行return前的赋值操作,然后再执行defer
}()
return x+bbb, b-1
}
如果方法返回值没有变量申明
func test () (int, string) {
x := 1
y := "aaa"
defer func() {
fmt.Printf("x:%v y:%v", x, y) //x=1 y=aaa
}()
return x+1 ,y+"bbb"
}
27. 匿名函数
package main
import "fmt"
func main() {
//匿名函数
fn1 := func (a int) int {return a+1}
fmt.Printf("fn1 type is %T, Value is %v\n", fn1, fn1(13))
//匿名函数集合collection
fn2 := [](func (a int) int){
func (a int) int {return a + 1},
func (a int) int {return a + 2},
}
fmt.Printf("fn2 type is %T, Value is %v\n", fn2, fn2[1](14))
//匿名函数结构体,结构体的定义和初始化
fn3 := struct {
p1 func(int) int
p2 func(string) string
}{
p1: func (a int) int {
return a+1
},
p2: func (a string) string {
return "hello,"+a
},
}
fmt.Printf("fn3 type is %T, Value is %v\n", fn3, fn3.p2("Qiu"))
}
28. 闭包
函数内返回了一个匿名函数,通过外部函数的赋值参数调用内部函数,它的堆栈不会被GC回收,保存再堆栈中
package main
import "fmt"
// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {
// 定义2个函数,并返回
// 相加
add := func(i int) int {
base += i
return base
}
// 相减
sub := func(i int) int {
base -= i
return base
}
// 返回
return add, sub
}
func main() {
f1, f2 := test01(10)
// base一直是没有消
fmt.Println(f1(1), f2(2)) //print 11 9
// 此时base是9
fmt.Println(f1(3), f2(4)) //print 12 8
f3, f4 := test01(10)
// base一直是没有消
fmt.Println(f3(1), f4(2)) //print 11 9
// 此时base是9
fmt.Println(f3(3), f4(4)) //print 12 8
}
通过上述结果表明,闭包函数会从外部函数的堆栈获取数据内容,所以base是会f1和f2是共用的
29. 延时调用defer
29.1. defer特性
- defer用于注册延迟调用
- return之前才会被调用(如果return是个表达式,那么先执行表达式后再执行defer)
- 多个defer语句,用到栈的特性,先进后出(FILO)
- defer语句中的变量,在defer声明时就决定了
29.2. defer用途
- 关闭文件句柄
- 资源释放
29.3. 实例
- defer语句中的变量,在defer声明时就已经决定的案例 和 FILO
package main
import "fmt"
func main() {
arr := []string{"a","b","c","d","e","f","g","h"}
for k,v := range arr {
defer fmt.Println(k, v)
}
fmt.Println("before return")
}
//print result
before return
7 h
6 g
5 f
4 e
3 d
2 c
1 b
0 a
- defer匿名函数导致的结果 实例
package main
import "fmt"
func main() {
arr := []string{"a","b","c","d","e","f","g","h"}
for k,v := range arr {
defer func() {fmt.Println(k, v)}()
}
fmt.Println("before return")
}
//print result
before return
7 h
7 h
7 h
7 h
7 h
7 h
7 h
7 h
会发现 为什么全是 7h,因为defer匿名函数的参数并不是通过参数被传入的,被执行时代码已经把业务逻辑跑完了此时k赋值在了7 v赋值成了h,所以结果成了7h.
总结:每次执行“defer”语句时,调用的函数值和参数都会像往常一样被计算并重新保存,但实际的函数不会被调用。
- 待完
30. 异常处理
panic()
抛出异常,然后在defer
中通过recover()
捕获异常
30.1. panic
- panic()是内置函数
- 假如函数F中调用了
panic()
,会立即终止其后的代码,触发函数内defer - 返回函数的调用者G,在G中,调用函数F语句之后的代码也不会执行,发出调用者的defer
- 直至协程退出,并报告错误
- 通过painc 传递 异常error
func panic (error Interface{}) {}
30.2. recover
- recover()是内置函数
- 用来控制一个协程的异常行为,捕获异常,从而影响应用的行为
- 在defer 延迟函数中,通过
recover()
来终止一个协程的panicking过程,从而恢复正常代码的执行【在recover()在defer中说明:panic()执行之前,会触发 defer 执行】 - 捕获painc传递的异常错误error
注意!!!
recover()处理panic(error)的error,defer只能定义在panic()之前,并且recover()定义在defer 调用的函数之中
recover()处理异常后,代码执行不会走到F函数的panic之后,而是走到G调用者的F函数语句之后
func recover() Interface{} {}
30.3. 实例
30.3.1. 基本使用实例
package main
import "fmt"
type excption struct {
err_code int
msg string
}
func main() {
test()
}
func test() {
a := 1
b := 2
defer func() {
if err := recover(); err != nil {
fmt.Printf("error = %v", err)
}
}()
defer func() {
fmt.Println(a)
}()
err := excption{
err_code: 1,
msg: "gan,报错了",
}
panic(err)
fmt.Println(b)
}
//print
1 //说明了先进后出
error = {1 gan,报错了} //抛出了定义的panic
30.3.2. 多panic实例
recover() 只捕获了最后一个 panic
package main
import "fmt"
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
//print
defer panic //说明:recover() 只捕获了最后一个 panic
30.3.3. 无效recover实例
只有在defer调用的函数内直接调用recover(),捕获才有效
package main
import "fmt"
func test() {
defer func() {
fmt.Println(recover()) //有效
}()
defer recover() //无效!
defer fmt.Println(recover()) //无效!
defer func() {
func() {
println("defer inner")
recover() //无效!
}()
}()
panic("test panic")
}
func main() {
test()
}
//print
defer inner
<nil>
test panic
方法
31. 方法定义
方法就是结构体struct实例的函数。
go方法总是绑定对象实例(接收者receiver),并隐式将实例作为第一参数(receiver)
- 只能在当前包内命名 类型定义方法 (类型:结构体或者类型别名)
- 第一参数 receiver可任意命名,如果方法中未使用,可以省略参数名
- receiver 参数类型可以 T 或者是 *T,T类型不能是接口或指针【
T
是值传递相当于copy了一份数据传递,*T
是直接内存指针传递,*T
是会直接修改实例对象的】 - 不支持重载
type Test struct{
a int
b string
}
func (receiver receiverType) funcName (paramList) returnType {}
// T 的值传递
func (t Test) method1 (param []int, param1 map[string]int) map[string]int {
}
// T 的 指针传递
func (t *Test) method1 (param []int, param1 map[string]int) map[string]int {
t.a = 123 //外部实例会被修改
}
32. 匿名字段
-
结构体内只定义引用的结构体的类型,不标记结构体变量
-
可以像 字段成员 那样调用 匿名字段的方法
-
当有多个匿名字段时,他们不能有相同的子方法否则编译报错,因为编译器不知道调用哪个方法。显式的则可以使用
package main
import "fmt"
type User struct {
id int
name string
}
type Manager struct {
User //Manager包含匿名字段User,只定义了一个结构体类型
}
func (self *User) ToString() string { // receiver = &(Manager.User)
return fmt.Sprintf("User: %p, %v", self, self)
}
func main() {
m := Manager{User{1, "Tom"}}
fmt.Printf("Manager: %p\n", &m)
fmt.Println(m.ToString()) //继承了User的ToString方法
}
33. 结构体嵌套的方法集
- 类型 T 的方法集 包含 receiver T 的方法
- 类型 *T 的方法集 包含 receiver T + receiver *T 方法
- 类型 S 包含了 匿名字段T,S 和 *S 方法集包含了 receiver T方法
- S = receiver S + receiver T
- *S = receiver S + receiver *S + receiver T
- 类型 S 包含了 匿名字段*T,S 和 *S 方法集包含了 receiver T + receiver *T方法
- S = receiver S + receiver T + receiver *T
- S = receiver S + receiver *S+ receiver T + receiver *T
package main
import "fmt"
type User struct {
id int
name string
}
func (self *User) Test() {
self.id = 2
}
func main() {
u := User{1, "Tom"}
u.Test()
fmt.Printf("%v, %v\n", u.id, u.name)
}
//print
2, Tom //u调用 receiver *User 被自动转成了&u
u调用 receiver *User 时,会被自动转成 &u,所以方法集有变更,所以上面的结论是否有问题。有待考证
34. 表达式
34.1. method value
instance.method(args)
- receiver被复制,后面的修改不是同一个内存地址
package main
import "fmt"
type User struct {
id int
name string
}
func (self User) Test() {
fmt.Println(self)
}
func main() {
u := User{1, "Tom"}
mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。
u.id, u.name = 2, "Jack"
u.Test()
mValue()
}
//print
{2 Jack}
{1 Tom} //receiver被复制,所以后续的修改不受影响
并发编程
35. 并发、并行
- 并发:一个CPU时间片,多个进/线程交替执行
- 并行:一个时间片个,多个进程在执行(多核CPU才能做到)
36. 协程
- 进程:进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位,对的PCB (Process Control Block)
- 线程:线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,一个进程中可以有多个线程,它们公用PCB,可是他们又有自己的独立TCB(Thread Control Block)
- 协程:用户态的线程
协程结构体
type g struct {
goid int64 //协程id
status uint32 // 协程状态
stack struct {
lo uintptr //该协程拥有的栈低位
hi uintptr //该协程拥有的栈低位
}
sched gobuf //切换时保存的上下文信息
startfunc uintptr // 程序地址
}
type gobuf struct {
sp uintptr //栈指针位置
pc uintptr //运行到的程序位置
}
36.1. 特点
-
用户态自行调度:进/线程切换都是操作系统调度的IO,而go的协程是自己GPM的**runtime(运行时)**层实现
-
可增长的栈:OS线程是固定栈2kb,go协程因为是自己的调度,所以会按需增减
-
**主协程并不会主动等待子协程完成,而会直接退出,退出时子协程也会退出销毁。**但是可以调用
wait()
来阻塞主协程来等待子协程
36.2. Channel
go协程通信:用通信来实现内存共享
channel像数据类型的queue,有着FIFO(先进先出)的特性,保证收发数据的顺序
36.2.1. 类型
channel是引用类型,所以需要定义并初始化(赋予内存),空值是nil
// 定义channel通道类型
var c0 chan int //int类型的chan
var c1 chan string //string类型的chan
var c2 chan []string //切片类型的chan
36.2.2. 创建channel
make(chan 元素类型, [缓冲大小])
c0 := make(chan int)
c0 := make(chan string)
c0 := make(chan []int])
36.2.3. 操作
发送和接受都使用<-
符号
send
var c0 chan int
c0 := make(chan int)
ch <- 10
receive
c1 := make(chan string)
a := <- c1 //从c1取出,并赋值给a
<- c1 //从c1取出,并忽略值
close
channel是会被GC(垃圾回收)回收,不像文件,文件在使用完成后需close(fd)
,而channel不是必须的,当接收方或者发送发一者关闭通道,则通道关闭无法使用
close(c1)
特征:
- 对一个关闭的channel发送值 panic
- 对一个关闭的channel接收值,会一直读取成功,直到管道内数据为空
- 对一个关闭的并且没有值的管道执行接收操作,会得到对应类型的空值
- 关闭一个已关闭的通道会导致panic
36.2.4. 无缓冲通道
c0 := make(chan int)
这样定义并初始化的通道叫做无缓冲通道。
如果当在协程g0
向c0
投递数据时,没有其他的协程在接收c0
管道的数据时,就会产生panic(deadlock死锁),相当于小区内没有快递箱和代售点,导致送货小哥阻塞。如果需要破解则需要在g0
投递消息时,g1
协程已经在监听接收消息
死锁的案例
package main
import (
"fmt"
"time"
)
func main() {
channel := make(chan int)
channel <- 20
for i:=0; i<10; i++ {
go getChanData(channel)
}
time.Sleep(time.Second)
}
func getChanData(channel chan int) {
a := <- channel
fmt.Println(a)
}
//print
fatal error: all goroutines are asleep - deadlock!
无死锁案例,但是其余9个接受数据协程阻塞
package main
import (
"fmt"
"time"
)
func main() {
channel := make(chan int)
for i:=0; i<10; i++ {
go getChanData(channel, i)
}
channel <- 20
time.Sleep(5*time.Second)
}
func getChanData(channel chan int, chan_num int) {
a := <- channel
fmt.Printf("chan_%v: %v", chan_num, a)
}
//print
chan_0: 20 //其余9个协程都没有打印数据,说明全部阻塞监听通道,当主协程退出,其他协程则退出并销毁
36.2.5. 有缓冲通道
在初始化通道时,赋予channel的容量,则是一个有缓冲通道,指定了通道的可存入数据长度make(chan int, 10)
可以通过内置函数len(c1)
来获取chan当前存入的元素数量,cap(c1)
获取通道的容量
module
当go命令查找一个新模块时,它会去检查goproxy坏境变量,它是一个由逗号分隔的列表
-
一个代理路径:需要通过goproxy protocol进行通信
-
direct:需要一个版本控制系统, git,svn等
-
off:不需要通信,GOPRIVATE 和 GONOPROXY环境变量也是用来控制这个行为
官网:https://go.dev/ref/mod#modules-overview
37. goproxy
在大陆地区我们无法直接通过 go get
命令获取到一些第三方包,
- 最常见的就是
golang.org/x
下面的各种优秀的包 - 自己私有gitlab仓库代码的引用
goproxy 是一个开源项目,当用户请求一个依赖库时,如果它发现本地没有这份代码就会自动请求源,然后缓存到本地,用户就可以从 goproxy.io 请求到数据。此时就用到了goproxy protocol
37.1. goproxy protocol
-
获取源目标版本列表: b a s e / base/ base/module/@v/list,例如http://goproxy.xiaoe-tools.com/talkcheap.xiaoeknow.com/eboss/gin_awesome_pkg/@v/list
-
获取源目标go.mod: b a s e / base/ base/module/@v/$version.mod
-
获取源目标信息: b a s e / base/ base/module/@v/$version.info
-
获取源目标信息source code : b a s e / base/ base/module/@v/$version.zip
37.2. 私有git库免密拉取
文档:https://cloud.tencent.com/developer/article/1422220
athens文档:https://gomods.io/zh/intro/components/
cd ~
vim .netrc
// .netrc文件内容
machine github.com // 请求地址
login MY_USERNAME // 账号
password MY_PASSWORD // 密码
垃圾回收GC
38. 常见垃圾GC方法
- 引用计数reference counting:php的GC,每个对象都有一个被引用的计数器,每被引用一次则被引用对象计数器+1,当引用对象失效,则被引用对象计数器-1,当计数器是0时,可回收
- 优点:实时性好,当计数器变0,触发GC清理内存
- 缺点:当AB对象是嵌套引用,则AB对象的计数器不会变成0,导致无法被回收
- 标记-清除Mark and Sweep:每次启动垃圾回收都会暂停当前所有的正常代码执行,运行一个扫描线程重新运行系统获得链路,通过链路判断对象是否可被触达,如果能触发说明对象当前正在被使用,不可回收;反之,没有触达到的对象则认为无用,可以回收。
- 三色标记法:是mark and sweep的优化版,go语言用的此方法
39. STW (Stop The World)
STW,stop the world;让程序暂停,GC扫描标记标记GCROOTS的对象引用。这样会让系统有卡顿现象
40. 减少STW时的三色标记法的缺陷弥补
不启动STW的情况下:
- A:扫描并标记完对象颜色(状态)后,突然添加白色(未引用状态)对象的引用,GC还是会清除白色对象,导致对象丢失
- B:扫描并标记完对象颜色(状态)后,突然减去黑色(已引用状态)对象的引用,GC还是会清除黑色对象,导致对象未被清除
GMP
包使用
41. 信号接收
监听终端信号,退出死循环
signals := make(chan os.Signal, 1) // os信号通道
signal.Notify(signals, os.Interrupt) // 监听 os.Interrupt终端信号 ,写入通道中
consumed := 0
ConsumerLoop: // 标签代码块必须包着 select、for循环
for {
select {
case msg := <-partitionConsumer.Messages():
log.Printf("ID %d ; Consumed message offset %d next offset: %d\n", goroutineId, msg.Offset, partitionConsumer.HighWaterMarkOffset())
log.Printf("ID %d ; Consumed message key: %v value:%v \n", goroutineId, string(msg.Key), string(msg.Value))
consumed++
case <-signals: // select 通道,如果有数据直接break ConsumerLoop标签
break ConsumerLoop
}
}
42. 终端输入
// fmt包
fmt.Scanln()
fmt.Println("请输入您的姓名,年龄,薪水,是否通过考试, 使用空格隔开")
fmt.Scanf("%s %d %f %t", &name, &age, &sal, &isPass)
}
// os包
var buffer [512]byte
n, err := os.Stdin.Read(buffer[:])
if err != nil {
fmt.Println("read error:", err)
return
}
fmt.Println("count:", n, ", msg:", string(buffer[:]))
43. 读文件
import os
// 按行读取
file, err := os.Open("app-2019-06-01.log")
if err != nil {
log.Fatal(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lineText := scanner.Text()
}
// 读取整个文件
b, err := ioutil.ReadFile("app-2019-06-01.log") // just pass the file name
if err != nil {
fmt.Print(err)
}
str := string(b) // convert content to a 'string'
fmt.Println(str) // print the content as a 'string'
44. 协程锁
44.1. 互斥锁Mutex
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"time"
)
var mutex sync.Mutex //互斥锁
func main() {
go user1()
go user2()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
select {
case <-signalChan:
fmt.Println("catch interrupt signal")
break
}
}
func printer(str string) {
//fmt.Println("will get lock")
mutex.Lock() //加锁
defer mutex.Unlock() //解锁
//fmt.Println("get lock ok")
for _, ch := range str {
fmt.Printf("%c", ch)
time.Sleep(time.Millisecond * 300)
}
}
func user1() {
printer("hello ")
}
func user2() {
printer("world")
}
//打印结果
worldhello 或者 helloworld: 两个单词是有序的,说明某个协程会在mutex.Lock()进行自旋等待获取锁
44.2. 读写互斥锁RWMutex
- 当获取写锁,并未释放时,读锁阻塞等待
mutex := &sync.RWMutex{}
mutex.Lock()
// Update 共享变量
mutex.Unlock()
mutex.RLock()
// Read 共享变量
mutex.RUnlock()
44.3. sync.WaitGroup
sync.WaitGroup也是一个经常会用到的同步原语,它的使用场景是在一个goroutine等待一组goroutine执行完成。
sync.WaitGroup拥有一个内部计数器。当计数器等于0时,则Wait()方法会立即返回。否则它将阻塞执行Wait()方法的goroutine直到计数器等于0时为止。
要增加计数器,我们必须使用Add(int)方法。要减少它,我们可以使用Done()(将计数器减1),也可以传递负数给Add方法把计数器减少指定大小,Done()方法底层就是通过Add(-1)实现的。
var wg sync.WaitGroup
for i:=0;i<3;i++ {
wg.Add(i)
go func(i int) {
defer wg.Done()
fmt.Printf("this is %d goroutine", i)
}(i)
}
wg.Wait()
44.4. sync.Once
确保代码块只被调用一次
once := &sync.Once{}
for i := 0; i < 4; i++ {
i := i
go func() {
once.Do(func() {
fmt.Printf("first %d\n", i)
})
}()
}
golang并发变成之同步原语 – 网管叨bi叨
45. 主协程退出,通知子协程
ctx := context.WithCancel(context.Background())
go func() {
for {
select {
// 主进程退出,通知consumer关闭
case <-ctx.Done():
_ = client.Close()
//logger.Infof("quit: kafka consumer %s", groupId)
return
}
}
}()
46. 测试用例
46.1. 测试
- 文件名以
_test.go
结尾 - 测试用例方法以
Test
开头 - 测试用例的入参只能是
*testing.T
- 命令
go test -run=funcname
- 子测试使用
t.Run()
func TestDB(t *testing.T) {
// 参数结构体
type args struct {
AppID string
}
// 用例定义
tests := []struct {
name string
args args
want Config
}{
{name: "case1", args: args{AppID: "app029yPjQc9131"}, want: Config{AppID: "app029yPjQc9131"}},
{name: "case2", args: args{AppID: "app029yPjQc9132"}, want: Config{AppID: "app029yPjQc9131"}},
}
for _, tt := range tests {
// 子测试
t.Run(tt.name, func(t *testing.T) {
if got, _ := opeDB(tt.args.AppID); got != tt.want {
t.Errorf("opeDB() = %v, want %v", got, tt.want)
}
})
}
}
46.2. 基准测试benchmark
- 用例方法以
Benchmark
开头 - 执行命令
go test -bench=funcname -run=none
- -bench=?, 模糊匹配benchmark防范
- -run=?,模糊匹配测试单元方法(一般不会有none这种方法名)
- 基准函数会运行目标代码 b.N 次。在基准执行期间,会调整 b.N 直到基准测试函数持续足够长的时间。
// 串行执行压测
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
foo()
}
}
// 输出`BenchmarkHello 10000000 282 ns/op` 循环了10000000,每次循环耗时282纳秒
// goroutine 并发执行
func BenchmarkSprints(b *testing.B) {
// 起GOMAXPROCS个数的协程
b.RunParallel(func(pb *testing.PB) {
// 每个协程 执行b.N次
for pb.Next() {
// do something
fmt.Sprint("代码轶事")
}
})
}
46.3. Main
有时测试用例方法内是需要一些全局变量的,所以需要在测试用例执行方法前初始化变量。而testing.M
就可以达到
func TestMain(m *testing.M) {
fmt.Println("start test main")
// 初始化连接
initProviders()
// 触发测试用例
m.Run()
// 用例完成后,工作
teardown()
}
47. context
- 主协程控制子协程关闭
// 控制关闭
func reqTask(ctx context.Context, name string) {
for {
select {
// 其实这里还是监听信号, **多个子协程**不用争抢,都会监听到信号,不像channel多协程时,只有一个协程获得
case <-ctx.Done():
fmt.Println("stop", name)
return
default:
fmt.Println(name, "send request")
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 1.生成控制子协程关闭context
ctx, cancel := context.WithCancel(context.Background())
go reqTask(ctx, "worker1")
go reqTask(ctx, "worker2")
time.Sleep(1 * time.Second)
// 调用1回调的cancel() 关闭子协程
cancel()
time.Sleep(1 * time.Second)
}
- 参数透传
// 参数透传
type Options struct{ Interval time.Duration }
func reqTask(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println("stop", name)
return
default:
fmt.Println(name, "send request")
op := ctx.Value("options").(*Options)
time.Sleep(op.Interval * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
vCtx := context.WithValue(ctx, "options", &Options{1})
go reqTask(vCtx, "worker1")
go reqTask(vCtx, "worker2")
time.Sleep(3 * time.Second)
cancel()
time.Sleep(3 * time.Second)
}
- 定时关闭:多长时间段(time duration)关闭
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go handle(ctx, 1500*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("main", ctx.Err())
}
}
func handle(ctx context.Context, duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println("handle", ctx.Err())
case <-time.After(duration):
fmt.Println("process request with", duration)
}
}
- 超时关闭:具体时间关闭
func contextDeadline() {
d := time.Now().Add(5000 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
fmt.Println("block")
select {
case <-time.After(6 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
fmt.Println("deadline")
}
fmt.Println("block out")
}
超时关闭:是具体时间关闭
定时关闭:是多长时间段(time duration)关闭
48. time
- 定时触发
// time.AfterFunc 在duration时间过后,触发func,
time.AfterFunc(next.duration, func() {
if !next.dateTime.IsZero() {
for {
if time.Now().Unix() >= next.dateTime.Unix() {
break
}
}
}
s.run(job)
time.AfterFunc(next.duration, func() {}) //触发完后,在加一个定时任务
})