0%

Go快速入门详细讲义-下篇.md

第一阶段:Go 快速入门(下篇)

一、内置运算符

1.1 算数运算符

{width=”4.1686034558180225in” height=”2.2823097112860893in”}

1、算数运算符使用

Go快速入门详细讲义-下篇.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
)
func main() {
fmt.Println("10+3=", 10+3)   //10+3= 13
fmt.Println("10-3=", 10-3)   //10-3= 7
fmt.Println("10*3=", 10*3)   //10*3= 30
//除法注意:如果运算的数都是整数,那么除后,去掉小数部分,保留整数部分
fmt.Println("10/3=", 10/3)       //10/3= 3
fmt.Println("10.0/3=", 10.0/3)   //3.3333333333333335
// 取余注意 余数=被除数-(被除数/除数)*除数
fmt.Println("10%3=", 10%3)     //10%3= 1
fmt.Println("-10%3=", -10%3)    // -10%3= -1
fmt.Println("10%-3=", 10%-3)    // 10%-3= 1
fmt.Println("-10%-3=", -10%-3)   // -10%-3= -1
}

2、i++

1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
)
func main() {
var i int = 1
i++
fmt.Println("i=", i)  // i= 2
}

1.2 关系运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
)
func main() {
var n1 int = 9
var n2 int = 8
fmt.Println(n1 == n2)   //false
fmt.Println(n1 != n2)   //true
fmt.Println(n1 > n2)    //true
fmt.Println(n1 >= n2)   //true
fmt.Println(n1 < n2)    //flase
fmt.Println(n1 <= n2)   //flase
flag := n1 > n2
fmt.Println("flag=", flag)  //flag= true
}

1.3 逻辑运算符

{width=”6.383173665791776in” height=”2.3970166229221346in”}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import (
"fmt"
)
func main() {
//演示逻辑运算符的使用 &&
var age int = 40
if age > 30 && age < 50 {
fmt.Println("ok1")
}
if age > 30 && age < 40 {
fmt.Println("ok2")
}
//演示逻辑运算符的使用 ||
if age > 30 || age < 50 {
fmt.Println("ok3")
}
if age > 30 || age < 40 {
fmt.Println("ok4")
}
//演示逻辑运算符的使用
if age > 30 {
fmt.Println("ok5")
}
if !(age > 30) {
fmt.Println("ok6")
}
}

1.4 赋值运算符

{width=”6.16953302712161in” height=”4.93458552055993in”}

1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt"
)
func main() {
d := 8 + 2*8    //赋值运算从右向左
fmt.Println(d)   //24

x := 10
x += 5        //x=x+5
fmt.Println("x += 5 的值:", x)  //24
}

二、条件循环

2.1 if else(分支结构)

1、if 条件判断基本写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"fmt"
)
func main() {
score := 65
if score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}

2、if 条件判断特殊写法

  • if 条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func main() {
//这里的 score 是局部作用域
if score := 56; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
}else {
fmt.Println("C")
fmt.Println(score) // 只能在函数内部打印 score
}
// fmt.Println(score) //undefined: score
}

2.2 for(循环结构)

2.1 for循环

1.1、普通for循环

1
2
3
4
5
6
7
8
package main
import "fmt"
func main() {
// 打印: 0 ~ 9 的数字
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}

1.2 外部定义 i

1
2
3
4
5
6
7
8
9
package main
import "fmt"
func main() {
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}

2、模拟while循环

  • Go 语言中是没有 while 语句的,我们可以通过 for 代替
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main() {
k := 1
for { // 这里也等价 for ; ; {
if k <= 10 {
fmt.Println("ok~~", k)
} else {
break   //break 就是跳出这个 for 循环
}
k++
}
}

3、for range(键值循环)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
str := "abc上海"
for index, val := range str {
fmt.Printf("索引=%d, 值=%c \n", index, val)
}
}
/*
索引=0, 值=a
索引=1, 值=b
索引=2, 值=c
索引=3, 值=上
索引=6, 值=海
*/

2.3 switch case

  • 使用 switch 语句可方便地对大量的值进行条件判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
score := "B"
switch score {
case "A":
fmt.Println("非常棒")
case "B":
fmt.Println("优秀")
case "C":
fmt.Println("及格")
default:
fmt.Println("不及格")
}
}

2.4 break、continue

break跳出循环

continue跳出本次循环

1、break跳出单循环

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main() {
k := 1
for { // 这里也等价 for ; ; {
if k <= 10 {
fmt.Println("ok~~", k)
} else {
break   //break 就是跳出这个 for 循环
}
k++
}
}

2、continue(继续下次循环)

1
2
3
4
5
6
7
8
9
10
package main

func main() {
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
println(i)
}
} /*1 3 5 7 9*/

三、函数

3.1 函数基础

1、函数定义

函数是组织好的、可重复使用的、用于执行指定任务的代码块。

本文介绍了 Go 语言中函数的相关内容。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
ret := intSum(1, 2)
fmt.Println(ret) // 3
}

func intSum(x, y int) int {
return x + y
}

2、函数返回值

  • Go 语言中通过 return 关键字向外输出返回值。
  • 函数多返回值,Go 语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹
    起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
plus, sub := calc(4, 5)
fmt.Println(plus) // 和为:9
fmt.Println(sub)  // 差为:-1
}

func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}

3.2 函数变量作用域

1、全局变量

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

//定义全局变量 num
var num int64 = 10

func main() {
fmt.Printf("num=%d\n", num) //num=10
}

2、局部变量

  • 局部变量是函数内部定义的变量, 函数内定义的变量无法在该函数外使用
  • 例如下面的示例代码 main 函数中无法使用 testLocalVar 函数中定义的变量 x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
// 这里name是函数test的局部变量,在其他函数无法访问
//fmt.Println(name)
test()
}

func test() {
name := "zhangsan"
fmt.Println(name)
}

3、for 循环语句中定义的变量

  • 我们之前讲过的 for 循环语句中定义的变量,也是只在 for 语句块中生效
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
testLocalVar3()
}
func testLocalVar3() {
for i := 0; i < 10; i++ {
fmt.Println(i) //变量 i 只在当前 for 语句块中生效
}
// fmt.Println(i) //此处无法使用变量 i
}

四、结构体

1.1 什么是结构体

Go语言中没有”类”的概念,也不支持”类”的继承等面向对象的概念。

Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

1.2 结构体定义

1、基本实例化(法1)

  • 只有当结构体实例化时,才会真正地分配内存,也就是必须实例化后才能使用结构体的字段。
  • 结构体本身也是一种类型,我们可以像声明内置类型一样使用 var 关键字声明结构体类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type person struct {
name string
city string
age  int
}

func main() {
var p1 person
p1.name = "张三"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n", p1)  // p1={张三 北京 18}
fmt.Printf("p1=%#v\n", p1) // p1=main.person{name:"张三", city:"北京", age:18}
}

2、new实例化(法2)

  • 我们还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址
  • 从打印的结果中我们可以看出 p2 是一个结构体指针。
  • 注意:在 Golang 中支持对结构体指针直接使用.来访问结构体的成员。
  • p2.name = "张三" 其实在底层是 (*p2).name = "张三"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type person struct {
name string
city string
age  int
}

func main() {
var p2 = new(person)
p2.name = "张三"
p2.age = 20
p2.city = "北京"
fmt.Printf("p2=%#v \n", p2) //p2=&main.person{name:"张三", city:"北京", age:20}
}

3、键值对初始化(法3)

  • 注意:最后一个属性的,要加上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"
type person struct {
name string
city string
age int
}
func main() {
p4 := person{
name: "zhangsan",
city: "北京",
age: 18,
}
// p4=main.person{name:"zhangsan", city:"北京", age:18}
fmt.Printf("p4=%#v\n", p4)
}

1.3 结构体方法和接收者

1、结构体说明

  • 在 go 语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。
  • 所谓方法就是定义了接收者的函数。
    • Go语言中的方法(Method)是一种作用于特定类型变量的函数。
    • 这种特定类型变量叫做接收者(Receiver)。
    • 接收者的概念就类似于其他语言中的this或者 self。
  • 方法的定义格式如下:
1
2
3
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
  • 给结构体 Person 定义一个方法打印 Person 的信息

2、结构体方法和接收者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Person struct {
name string
age  int8
}

func (p Person) printInfo() {
fmt.Printf("姓名:%v 年龄:%v", p.name, p.age) // 姓名:小王子 年龄:25
}
func main() {
p1 := Person{
name: "小王子",
age:  25,
}
p1.printInfo() // 姓名:小王子 年龄:25
}

3、值类型和指针类型接收者

  • 实例1:给结构体 Person 定义一个方法打印 Person 的信息
  • 1、值类型的接收者
    • 当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。
    • 在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收
      者变量本身。
  • 2、指针类型的接收者
    • 指针类型的接收者由一个结构体的指针组成
    • 由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效
      的。
    • 这种方式就十分接近于其他语言中面向对象中的 this 或者 self。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

type Person struct {
name string
age  int
}

//值类型接受者
func (p Person) printInfo() {
fmt.Printf("姓名:%v 年龄:%v\n", p.name, p.age) // 姓名:小王子 年龄:25
}

//指针类型接收者
func (p *Person) setInfo(name string, age int) {
p.name = name
p.age = age
}
func main() {
p1 := Person{
name: "小王子",
age:  25,
}
p1.printInfo() // 姓名:小王子 年龄:25
p1.setInfo("张三", 20)
p1.printInfo() // 姓名:张三 年龄:20
}

1.4、一个结构体实现多个接口

  • Golang 中一个结构体也可以实现多个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
import "fmt"
type AInterface interface {
GetInfo() string
}
type BInterface interface {
SetInfo(string, int)
}
type People struct {
Name string
Age int
}
func (p People) GetInfo() string {
return fmt.Sprintf("姓名:%v 年龄:%d", p.Name, p.Age)
}
func (p *People) SetInfo(name string, age int) {
p.Name = name
p.Age = age
}
func main() {
var people = &People{
Name: "张三",
Age: 20,
}
// people 实现了 AInterface 和 BInterface
var p1 AInterface = people
var p2 BInterface = people
fmt.Println(p1.GetInfo())
p2.SetInfo("李四", 30) // 姓名:张三 年龄:20
fmt.Println(p1.GetInfo()) // 姓名:李四 年龄:30
}

1.5、接口嵌套(继承)

  • 接口与接口间可以通过嵌套创造出新的接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import "fmt"
type SayInterface interface {
say()
}
type MoveInterface interface {
move()
}
// 接口嵌套
type Animal interface {
SayInterface
MoveInterface
}
type Cat struct {
name string
}
func (c Cat) say() {
fmt.Println("喵喵喵")
}
func (c Cat) move() {
fmt.Println("猫会动")
}
func main() {
var x Animal
x = Cat{name: "花花"}
x.move() // 猫会动
x.say() // 喵喵喵
}

五、面向对象

5.1 Golang接口的定义

1、Golang 中的接口

  • 在Go语言中接口(interface)是一种类型,一种抽象的类型。
  • 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细
    节。
  • 实现接口的条件
    • 一个对象只要全部实现了接口中的方法,那么就实现了这个接口。
    • 换句话说,接口就是一个需要实现的方法列表。

2、为什么要使用接口

  • 上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码
  • 如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去
  • 那我们能不能把它们当成“能叫的动物”来处理呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
)

type Cat struct {
Name string
}

func (c Cat) Say() string {
return c.Name + ":喵喵喵"
}

type Dog struct {
Name string
}

func (d Dog) Say() string {
return d.Name + ": 汪汪汪"
}

func main() {
c := Cat{Name: "小白猫"} // 小白猫:喵喵喵
fmt.Println(c.Say())
d := Dog{"阿黄"}
fmt.Println(d.Say()) // 阿黄: 汪汪汪
}

/*
小白猫:喵喵喵
阿黄: 汪汪汪
*/

3、定义一个Usber接口

  • 定义一个 Usber 接口让 Phone 和 Camera 结构体实现这个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import "fmt"

//1.接口是一个规范
type Usber interface {
start()
stop()
}

//2.如果接口里面有方法的话,必要要通过结构体或者通过自定义类型实现这个接口
type Phone struct {
Name string
}
//3.手机要实现usb接口的话必须得实现usb接口中的所有方法
func (p Phone) start() {
fmt.Println(p.Name, "启动")
}
func (p Phone) stop() {
fmt.Println(p.Name, "关机")
}

func main() {
p := Phone{
Name: "华为手机",
}
var p1 Usber   // golang中接口就是一个数据类型
p1 = p       // 表示手机实现Usb接口
p1.start()
p1.stop()
}
/*
华为手机 启动
华为手机 关机
*/

4、go中类

  • Go语言中没有”类”的概念,也不支持”类”的继承等面向对象的概念。

  • Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

5.2 空接口

1、空接口说明

  • golang中空接口也可以直接当做类型来使用,可以表示任意类型

  • Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。

  • 空接口表示没有任何约束,因此任何类型变量都可以实现空接口。

  • 空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。

2、空接口作为函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"

//空接口作为函数的参数
func show(a interface{}) {
fmt.Printf("值:%v 类型:%T\n", a, a)
}
func main() {
show(20)                      // 值:20 类型:int
show("你好golang")            // 值:你好golang 类型:string
slice := []int{1, 2, 34, 4}
show(slice)                     // 值:[1 2 34 4] 类型:[]int
}

3、切片实现空接口

1
2
3
4
5
6
7
package main
import "fmt"

func main() {
var slice = []interface{}{"张三", 20, true, 32.2}
fmt.Println(slice)  // [张三 20 true 32.2]
}

4、map 的值实现空接口

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"

func main() {
// 空接口作为 map 值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "张三"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
// [age:18 married:false name:张三]
}

5.3 类型断言

  • 一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。
  • 这两部分分别称为接口的动态类型和动态值。
  • 如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言
  • 其语法格式:x.(T)
    • x : 表示类型为 interface{}的变量
    • T : 表示断言 x 可能是的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"

func main() {
var x interface{}
x = "Hello golnag"
v, ok := x.(string)
if ok {
fmt.Println(v)
}else {
fmt.Println("非字符串类型")
}
}

5.4 值接收者和指针接收者

1、值接收者

  • 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。
  • 在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变
    量本身。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import "fmt"
type Usb interface {
Start()
Stop()
}
type Phone struct {
Name string
}
func (p Phone) Start() {
fmt.Println(p.Name, "开始工作")
}
func (p Phone) Stop() {
fmt.Println("phone 停止")
}
func main() {
phone1 := Phone{       // 一:实例化值类型
Name: "小米手机",
}
var p1 Usb = phone1      //phone1 实现了 Usb 接口 phone1 是 Phone 类型
p1.Start()

phone2 := &Phone{     // 二:实例化指针类型
Name: "苹果手机",
}
var p2 Usb = phone2      //phone2 实现了 Usb 接口 phone2 是 *Phone 类型
p2.Start()            //苹果手机 开始工作
}

2、指针接收者

  • 指针类型的接收者由一个结构体的指针组成
  • 由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
  • 这种方式就十分接近于其他语言中面向对象中的this 或者self 。
  • 例如我们为Person 添加一个SetAge 方法,来修改实例变量的年龄。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
import "fmt"
type Usb interface {
Start()
Stop()
}
type Phone struct {
Name string
}
func (p *Phone) Start() {
fmt.Println(p.Name, "开始工作")
}
func (p *Phone) Stop() {
fmt.Println("phone 停止")
}
func main() {
/*错误写法
phone1 := Phone{
Name: "小米手机",
}
var p1 Usb = phone1
p1.Start()
*/
//正确写法
phone2 := &Phone{     // 指针类型接收者只能传入指针类型,不能传入值类型
Name: "苹果手机",
}
var p2 Usb = phone2 //phone2 实现了 Usb 接口 phone2 是 *Phone 类型
p2.Start()
//苹果手机 开始工作
}

3、值类型接收者使用时机

  • 1、需要修改接收者中的值
  • 2、接收者是拷贝代价比较大的大对象
  • 3、保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

六、并发编程

6.1 并发介绍

1、并发和并行

1
2
A. 多线程程序在一个核的cpu上运行,就是并发。
B. 多线程程序在多个核的cpu上运行,就是并行。
  • 并发:本质还是串行

{width=”6.16953302712161in” height=”2.9602318460192474in”}

  • 并行:任务分布在不同CPU上,同一时间点同时执行

{width=”3.126453412073491in” height=”1.6674409448818897in”}

  1. 协程和线程
  • 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

  • 线程:一个线程上可以跑多个协程,协程是轻量级的线程。

6.2 goroutine

1、多线程编程缺点

  • 在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池

  • 并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换

2、gouroutine

  • Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。

  • Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。

  • Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

  • 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine 当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

6.3 协程基本使用

1、启动一个协程

  • 主线程中每个100毫秒打印一次,总共打印2次
  • 另外开启一个协程,打印10次
  • 情况一:打印是交替,证明是并行的
  • 情况二:开启的协程打印两次,就退出了(因为主线程退出了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"time"
)

func test() {
for i := 0; i < 10; i++ {
fmt.Println("test() 你好golang")
time.Sleep(time.Millisecond * 100)
}
}

func main() {
go test() //表示开启一个协程
for i := 0; i < 2; i++ {
fmt.Println("main() 你好golang")
time.Sleep(time.Millisecond * 100)
}
}

/*
main() 你好golang
test() 你好golang
main() 你好golang
test() 你好golang
test() 你好golang
*/

2、WaitGroup

  • 主线程退出后所有的协程无论有没有执行完毕都会退出
  • 所以我们在主进程中可以通过WaitGroup等待协程执行完毕
    • sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。
    • 例如当我们启动了N 个并发任务时,就将计数器值增加N。
    • 每个任务完成时通过调用Done()方法将计数器减1。
    • 通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
1
2
3
4
var wg sync.WaitGroup       // 第一步:定义一个计数器
wg.Add(1)               // 第二步:开启一个协程计数器+1
wg.Done()               // 第三步:协程执行完毕,计数器-1
wg.Wait()               // 第四步:计数器为0时推出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main
import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup           // 第一步:定义一个计数器

func test1() {
for i := 0; i < 10; i++ {
fmt.Println("test1() 你好golang-", i)
time.Sleep(time.Millisecond * 100)
}
wg.Done() //协程计数器-1   // 第三步:协程执行完毕,计数器-1
}

func test2() {
for i := 0; i < 2; i++ {
fmt.Println("test2() 你好golang-", i)
time.Sleep(time.Millisecond * 100)
}
wg.Done() //协程计数器-1
}

func main() {
wg.Add(1)  //协程计数器+1       第二步:开启一个协程计数器+1
go test1() //表示开启一个协程
wg.Add(1)  //协程计数器+1
go test2() //表示开启一个协程

wg.Wait() //等待协程执行完毕...   第四步:计数器为0时推出
fmt.Println("主线程退出...")
}

/*
test2() 你好golang- 0
test1() 你好golang- 0
.....
test1() 你好golang- 8
test1() 你好golang- 9
主线程退出...
*/

3、开启多个协程

  • 在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine。
  • 这里使用了 sync.WaitGroup 来实现等待 goroutine 执行完毕
  • 多次执行上面的代码,会发现每次打印的数字的顺序都不一致。
  • 这是因为 10 个 goroutine是并发执行的,而 goroutine 的调度是随机的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {

for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}

/*
Hello Goroutine! 0
Hello Goroutine! 8
Hello Goroutine! 2
Hello Goroutine! 7
Hello Goroutine! 6
Hello Goroutine! 1


 

Hello Goroutine! 5
Hello Goroutine! 9
Hello Goroutine! 3
Hello Goroutine! 4
*/

6.4.Channel

1.Channel 管道

1.1 Channel说明

  • 共享内存交互数据弊端
    • 单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
    • 虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。
    • 为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
  • channel好处
    • Go 语言中的通道(channel)是一种特殊的类型。
    • 通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
    • 每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
    • 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。
    • channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

1.2 channel类型

  • channel 是一种类型,一种引用类型
  • 声明管道类型的格式如下:
1
2
3
4
var 变量 chan 元素类型
var ch1 chan int // 声明一个传递整型的管道
var ch2 chan bool // 声明一个传递布尔型的管道
var ch3 chan []int // 声明一个传递 int 切片的管道

1.3 创建channel

  • 声明的管道后需要使用 make 函数初始化之后才能使用。
  • 创建 channel 的格式如下:make(chan 元素类型, 容量)
1
2
3
4
5
6
// 创建一个能存储 10 个 int 类型数据的管道
ch1 := make(chan int, 10)
// 创建一个能存储 4 个 bool 类型数据的管道
ch2 := make(chan bool, 4)
// 创建一个能存储 3 个[]int 切片类型数据的管道
ch3 := make(chan []int, 3)

2.channel操作

  • 管道有发送(send)、接收(receive)和关闭(close)三种操作。
  • 发送和接收都使用<-符号。
  • 现在我们先使用以下语句定义一个管道:
1
ch := make(chan int, 3)

2.1 发送(将数据放在管道内)

  • 将一个值发送到管道中。
1
ch <- 10  // 把 10 发送到 ch 中

2.2 接收(从管道内取值)

  • 从一个管道中接收值。
1
2
x := <- ch     // 从 ch 中接收值并赋值给变量 x
<-ch // 从 ch 中接收值,忽略结果

2.3 关闭管道

  • 我们通过调用内置的 close 函数来关闭管道: close(ch)
  • 关于关闭管道需要注意的事情是,只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭管道。
  • 管道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的。
  • 关闭后的管道有以下特点:
    • 对一个关闭的管道再发送值就会导致 panic。
    • 对一个关闭的管道进行接收会一直获取值直到管道为空
    • 对一个关闭的并且没有值的管道执行接收操作会得到对应类型的零值。
    • 关闭一个已经关闭的管道会导致 panic。

2.4 管道阻塞

2.4.1 无缓冲的管道
  • 如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道
  • 无缓冲的管道又称为阻塞的管道。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"

func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
/*
-- 面这段代码能够通过编译,但是执行的时候会出现以下错误
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
E:/_000/Go/Code/demo1.go:6 +0x5f
*/

img

2.4.2 有缓冲的管道
  • 解决上面问题的方法还有一种就是使用有缓冲区的管道。
  • 我们可以在使用 make 函数初始化管道的时候为其指定管道的容量
  • 只要管道的容量大于零,那么该管道就是有缓冲的管道,管道的容量表示管道中能存放元素的数量。
  • 就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
1
2
3
4
5
6
7
8
9
package main
import "fmt"

func main() {
ch := make(chan int, 5)
ch <- 10
ch <- 12
fmt.Println("发送成功")
}

img

3.从channel取值

3.1 优雅的从channel取值

  • 当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。
  • 当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。
  • 那如何判断一个通道是否被关闭了呢?
  • for range的方式判断通道关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"fmt"
)

func f1(ch1 chan int) {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}

func f2(ch1 chan int, ch2 chan int) {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}

// channel 练习
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go f1(ch1)
// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go f2(ch1, ch2)
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}

3.2 Goroutine结合Channel管道

  • 需求 1:定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据,要求同步进行。
  • 1、开启一个 fn1 的的协程给向管道 inChan 中写入 100 条数据
  • 2、开启一个 fn2 的协程读取 inChan 中写入的数据
  • 3、注意:fn1 和 fn2 同时操作一个管道
  • 4、主线程必须等待操作完成后才可以退出
  • 注:for range的方式判断通道关闭,推出程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup
func main() {
intChan := make(chan int,10)
wg.Add(2)
go write(intChan)
go read(intChan)
wg.Wait()
fmt.Println("读取完毕...")
}

func write(intChan chan int) {
defer wg.Done()
for i:=0;i<10;i++{
intChan <- i
}
close(intChan)
}

func read(intChan chan int) {
defer wg.Done()
for v := range intChan {
fmt.Println(v)
time.Sleep(time.Second)
}
}

4.单向管道

  • 有的时候我们会将管道作为参数在多个任务函数间传递
  • 很多时候我们在不同的任务函数中使用管道都会对其进行限制
  • 比如限制管道在函数中只能发送或只能接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
)
func main() {
//1. 在默认情况下下,管道是双向
//var chan1 chan int //可读可写

//2 声明为只写
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2<- 20
//num := <-chan2 //error
fmt.Println("chan2=", chan2)

//3. 声明为只读
var chan3 <-chan int
num2 := <-chan3
//chan3<- 30 //err
fmt.Println("num2", num2)
}

5.Goroutine池

  • 本质上是生产者消费者模型
  • 在工作中我们通常会使用可以指定启动的goroutine数量–worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。
  • 一个简易的work pool示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker:%d start job:%d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("worker:%d end job:%d\n", id, j)
results <- j * 2
}
}


func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 开启3个goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 5个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// 输出结果
for a := 1; a <= 5; a++ {
<-results
}
}

6.5.select

select 多路复用

6.1 select说明

  • 传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock,在实际开发中,可能我们不好确定什么关闭该管道。
  • 这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。
  • 为了应对这种场景,Go 内置了 select 关键字,可以同时响应多个管道的操作。
  • select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。
  • 每个 case 会对应一个管道的通信(接收或发送)过程。
  • select 会一直等待,直到某个 case 的通信操作完成时,就会执行 case 分支对应的语句。
  • 具体格式如下:
1
2
3
4
5
6
7
8
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}

6.2 select 的使用

  • 使用 select 语句能提高代码的可读性。
  • 可处理一个或多个 channel 的发送/接收操作。
  • 如果多个 case 同时满足,select 会随机选择一个。
  • 对于没有 case 的 select{}会一直等待,可用于阻塞 main 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
import (
"fmt"
"time"
)

func main() {
// 在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang中给我们提供的select多路复用

//1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2.定义一个管道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//使用select来获取channel里面的数据的时候不需要关闭channel
for {
select {
case v := <-intChan:
fmt.Printf("从 intChan 读取的数据%d\n", v)
time.Sleep(time.Millisecond * 50)
case v := <-stringChan:
fmt.Printf("从 stringChan 读取的数据%v\n", v)
time.Sleep(time.Millisecond * 50)
default:
fmt.Printf("数据获取完毕")
return //注意退出...
}
}
}

6.6.并发安全和锁

1.并发安全与锁

1.1 并发安全

  • 有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。
  • 类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。
  • 下面开启两个协程,对变量x加一操作,分别加5000次,理想结果是10000,实际三次结果都不相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
import (
"fmt"
"sync"
)

var x int64
var wg sync.WaitGroup

func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}

/*
第一次结果:6125
第二次结果:6217
第三次结果:10000

*/

1.2 互斥锁

  • 互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。
  • Go语言中使用sync包的Mutex类型来实现互斥锁。
  • 使用互斥锁来修复上面代码的问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"sync"
)

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x) // 10000
}

1.3 读写互斥锁

  • 互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的
  • 当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。
  • 读写锁在Go语言中使用sync包中的RWMutex类型。
  • 读写锁分为两种:读锁和写锁
    • 当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
    • 当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
  • 注意:读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main
import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup
var mutex sync.RWMutex
//写的方法
func write() {
mutex.Lock()
fmt.Println("执行写操作")
time.Sleep(time.Second * 2)
mutex.Unlock()
wg.Done()
}

//读的方法
func read() {
mutex.RLock()
fmt.Println("---执行读操作")
time.Sleep(time.Second * 2)
mutex.RUnlock()
wg.Done()
}

func main() {
for i := 0; i < 10; i++ { //开启10个协程执行读操作
wg.Add(1)
go write()
}
for i := 0; i < 10; i++ { // 开启10个协程执行写操作
wg.Add(1)
go read()
}
wg.Wait()
}

2.sync其他方法

2.1 sync.WaitGroup

  • 在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。
  • sync.WaitGroup有以下几个方法:
方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0
  • sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。
  • 例如当我们启动了N 个并发任务时,就将计数器值增加N。
  • 每个任务完成时通过调用Done()方法将计数器减1。
  • 通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
  • 我们利用sync.WaitGroup将上面的代码优化一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1)
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
wg.Wait()
}

2.2sync.Once

  • 在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
  • Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once
  • sync.Once只有一个Do方法,其签名如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}

// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}

2.3 sync.Map

  • Go语言中内置的map不是并发安全的。请看下面的示例
  • 下面的代码开启少量几个goroutine的时候可能没什么问题
  • 当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。
  • 原因:
    • 因为 map 变量为 指针类型变量,并发写时,多个协程同时操作一个内存
    • 类似于多线程操作同一个资源会发生竞争关系,共享资源会遭到破坏
    • 因此golang 出于安全的考虑,抛出致命错误:fatal error: concurrent map writes。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"strconv"
"sync"
)

var m = make(map[string]int)

func get(key string) int {
return m[key]
}

func set(key string, value int) {
m[key] = value
}

func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
  • 像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map
  • 开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。
  • 同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"strconv"
"sync"
)

var m = sync.Map{}

func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}

6.7.goroutine原理

1.设计思路

1.1 设计描述

  • 启动服务之时先初始化一个 Goroutine Pool 池,这个 Pool 维护了一个类似栈的 LIFO 队列 ,里面存放负责处理任务的 Worker
  • 然后在 client 端提交 task 到 Pool 中之后,在 Pool 内部,接收 task 之后的核心操作是
    • 检查当前 Worker 队列中是否有可用的 Worker,如果有,取出执行当前的 task;
    • 没有可用的 Worker,判断当前在运行的 Worker 是否已超过该 Pool 的容量
    • 每个 Worker 执行完任务之后,放回 Pool 的队列中等待

img

1.2 Pool struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type sig struct{}

type f func() error

// pool从客户端获取任务,它限制goroutines总数,并且回收再使用
type Pool struct {
capacity int32 // 协程池容量
running int32 // 正在运行的goroutine数量
expiryDuration time.Duration // 为每个worker设置一个过期时间
workers []*Worker // 存放空闲 worker,请求进入 Pool先检查workers若有则取出绑定任务执行
release chan sig // 当关闭该 Pool 支持通知所有 worker 退出运行以防 goroutine 泄露
lock sync.Mutex // 同步操作锁
once sync.Once // 确保 Pool 关闭操作只会执行一次
}

1.3 初始化 Pool 并启动定期清理过期 worker 任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 新建一个线程池实例
func NewPool(size int) (*Pool, error) {
return NewTimingPool(size, DefaultCleanIntervalTime)
}

// 产生一个带有自定义定时器的线程池实例
func NewTimingPool(size, expiry int) (*Pool, error) {
if size <= 0 {
return nil, ErrInvalidPoolSize
}
if expiry <= 0 {
return nil, ErrInvalidPoolExpiry
}
p := &Pool{
capacity: int32(size),
freeSignal: make(chan sig, math.MaxInt32),
release: make(chan sig, 1),
expiryDuration: time.Duration(expiry) * time.Second,
}
// 启动定期清理过期worker任务,独立goroutine运行,进一步节省系统资源
p.monitorAndClear()
return p, nil
}

1.4 提交任务到 Pool

  • 第一个 if 判断当前 Pool 是否已被关闭,若是则不再接受新任务,否则获取一个 Pool 中可用的 worker,绑定该 task 执行。
1
2
3
4
5
6
7
8
9
// Submit submit a task to pool
func (p *Pool) Submit(task f) error {
if len(p.release) > 0 {
return ErrPoolClosed
}
w := p.getWorker()
w.task <- task
return nil
}

1.5 获取可用 worker(核心)

  • p.getWorker() 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 返回一个可用的worker来运行这些任务。
func (p *Pool) getWorker() *Worker {
var w *Worker
waiting := false // 标志变量,判断当前正在运行的worker数量是否已到达Pool的容量上限
p.lock.Lock() // 加锁,检测队列中是否有可用worker,并进行相应操作
idleWorkers := p.workers
n := len(idleWorkers) - 1
if n < 0 { // 当前队列中无可用worker
// 判断运行worker数目已达到该Pool的容量上限,置等待标志
waiting = p.Running() >= p.Cap()
} else { // 当前队列有可用worker,从队列尾部取出一个使用
w = idleWorkers[n]
idleWorkers[n] = nil
p.workers = idleWorkers[:n]
}

p.lock.Unlock() // 检测完成,解锁
if waiting { // Pool容量已满,新请求等待
for { // 利用锁阻塞等待直到有空闲worker
p.lock.Lock()
idleWorkers = p.workers
l := len(idleWorkers) - 1
if l < 0 {
p.lock.Unlock()
continue
}
w = idleWorkers[l]
idleWorkers[l] = nil
p.workers = idleWorkers[:l]
p.lock.Unlock()
break
}
// 当前无空闲worker但是Pool还没有满,则可以直接新开一个worker执行任务
} else if w == nil {
w = &Worker{
pool: p,
task: make(chan f, 1),
}
w.run()
// 运行worker数加一
p.incRunning()
}
return w
}

1.6 执行任务

  • 结合前面的 p.Submit(task f)p.getWorker() ,提交任务到 Pool 之后,获取一个可用 worker
  • 每新建一个 worker 实例之时都需要调用 w.run() 启动一个 goroutine 监听 worker 的任务列表 task ,一有任务提交进来就执行;
  • 所以,当调用 worker 的 sendTask(task f) 方法提交任务到 worker 的任务队列之后,马上就可以被接收并执行
  • 当任务执行完之后,会调用 w.pool.putWorker(w *Worker) 方法将这个已经执行完任务的 worker 从当前任务解绑放回 Pool 中,以供下个任务可以使用
  • 至此,一个任务从提交到完成的过程就此结束,Pool 调度将进入下一个循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Worker是运行任务的实际执行者,它启动一个接受任务并执行函数调用的goroutine
type Worker struct {
pool *Pool // 每个pool对应一个worker
task chan f // 任务是一项应该完成的工作
recycleTime time.Time // 当将一个worker放回队列时,recycleTime将被更新。
}

// Run启动一个goroutine以重复执行函数调用的过程
func (w *Worker) run() {
go func() {
// 循环监听任务列表,一旦有任务立马取出运行
for f := range w.task {
if f == nil {
// 退出goroutine,运行worker数减一
w.pool.decRunning()
return
}
f()
// worker回收复用
w.pool.putWorker(w)
}
}()
}

1.7 worker回收(goroutine 复用)

1
2
3
4
5
6
7
8
// putWorker将一个worker放回空闲池,回收goroutines
func (p *Pool) putWorker(worker *Worker) {
// 写入回收时间,亦即该worker的最后一次结束运行的时间
worker.recycleTime = time.Now()
p.lock.Lock()
p.workers = append(p.workers, worker)
p.lock.Unlock()
}

1.8 动态扩容或者缩小池容量

1
2
3
4
5
6
7
8
9
10
11
12
13
// ReSize更改此池的容量
func (p *Pool) ReSize(size int) {
if size == p.Cap() {
return
}
atomic.StoreInt32(&p.capacity, int32(size))
diff := p.Running() - size
if diff > 0 {
for i := 0; i < diff; i++ {
p.getWorker().task <- nil
}
}
}

1.9 定期清理过期 Worker

  • 定期检查空闲 worker 队列中是否有已过期的 worker 并清理
  • 因为采用了 LIFO 后进先出队列存放空闲 worker,所以该队列默认已经是按照 worker 的最后运行时间由远及近排序
  • 可以方便地按顺序取出空闲队列中的每个 worker 并判断它们的最后运行时间与当前时间之差是否超过设置的过期时长
  • 若是,则清理掉该 goroutine,释放该 worker,并且将剩下的未过期 worker 重新分配到当前 Pool 的空闲 worker 队列中,进一步节省系统资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//  定期清理过期 Worker
func (p *Pool) periodicallyPurge() {
heartbeat := time.NewTicker(p.expiryDuration)
for range heartbeat.C {
currentTime := time.Now()
p.lock.Lock()
idleWorkers := p.workers
if len(idleWorkers) == 0 && p.Running() == 0 && len(p.release) > 0 {
p.lock.Unlock()
return
}
n := 0
for i, w := range idleWorkers {
if currentTime.Sub(w.recycleTime) <= p.expiryDuration {
break
}
n = i
w.task <- nil
idleWorkers[i] = nil
}
n++
if n >= len(idleWorkers) {
p.workers = idleWorkers[:0]
} else {
p.workers = idleWorkers[n:]
}
p.lock.Unlock()
}
}

2.pool使用

2.1 公共池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"sync"
"time"

"github.com/panjf2000/ants/v2"
)

func demoFunc() {
time.Sleep(10 * time.Millisecond)
fmt.Println("Hello World!")
}

func main() {
// 在retrieveWorker()中可能有一些调用者在等待,因此我们需要唤醒它们来防止那些无限阻塞的调用者
defer ants.Release()
var wg sync.WaitGroup
syncCalculateSum := func() {
demoFunc()
wg.Done()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
_ = ants.Submit(syncCalculateSum)
}
wg.Wait()
fmt.Printf("running goroutines: %d\n", ants.Running())
fmt.Printf("finish all tasks.\n")
}

/*
Hello World!
Hello World!
running goroutines: 1000
finish all tasks.
*/

2.2 方法绑定池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"github.com/panjf2000/ants/v2"
"sync"
)

func myFunc(i interface{}) {
fmt.Printf("run with %d\n", i)
}

func main() {
defer ants.Release()
var wg sync.WaitGroup
// 使用池和函数,设置goroutine pool的容量为10,超时时间为1秒。
p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
myFunc(i)
wg.Done()
})
defer p.Release()
// 逐个提交任务
for i := 0; i < 1000; i++ {
wg.Add(1)
_ = p.Invoke(int32(i))
}
wg.Wait()
fmt.Printf("running goroutines: %d\n", p.Running())
}

/*
run with 976
run with 990
run with 971
running goroutines: 10
*/

七、常用模块

一、fmt

1.1 常用占位符

动词 功能
%v 按值的本来值输出
%+v 在 %v 的基础上,对结构体字段名和值进行展开
%#v 输出 Go 语言语法格式的值
%T 输出 Go 语言语法格式的类型和值
%% 输出 %% 本体
%b 整型以二进制方式显示
%o 整型以八进制方式显示
%d 整型以十进制方式显示
%x 整型以 十六进制显示
%X 整型以十六进制、字母大写方式显示
%U Unicode 字符
%f 浮点数
%p 指针,十六进制方式显示

1.2 Print

  • Println:

    • 一次输入多个值的时候 Println 中间有空格
    • Println 会自动换行,Print 不会
  • Print:

    • 一次输入多个值的时候 Print 没有 中间有空格
    • Print 不会自动换行
  • Printf

    • 是格式化输出,在很多场景下比 Println 更方便
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
fmt.Print("zhangsan", "lisi", "wangwu")   // zhangsanlisiwangwu
fmt.Println("zhangsan", "lisi", "wangwu") // zhangsan lisi wangwu

name := "zhangsan"
age := 20
fmt.Printf("%s 今年 %d 岁\n", name, age)     // zhangsan 今年 20 岁
fmt.Printf("值:%v --> 类型: %T", name, name) // 值:zhangsan --> 类型: string
}

1.3 Sprint

  • Sprint系列函数会把传入的数据生成并返回一个字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"

func main() {
s1 := fmt.Sprint("枯藤")
fmt.Println(s1) // 枯藤
name := "枯藤"
age := 18
s2 := fmt.Sprintf("name:%s,age:%d", name, age) // name:枯藤,age:18
fmt.Println(s2)
s3 := fmt.Sprintln("枯藤") // 枯藤 有空格
fmt.Println(s3)
}

二、time

2.1 时间类型

  • 我们可以通过 time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。
  • 注意:%02d 中的 2 表示宽度,如果整数不够 2 列就补上 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"time"
)

func main() {
now := time.Now()   //获取当前时间
fmt.Printf("current time:%v\n", now)

year := now.Year()     //年
month := now.Month()   //月
day := now.Day()       //日
hour := now.Hour()        //小时
minute := now.Minute()     //分钟

2.2 时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now()            //获取当前时间
timestamp1 := now.Unix()     //时间戳
timestamp2 := now.UnixNano() //纳秒时间戳
fmt.Printf("current timestamp1:%v\n", timestamp1)  // current
timestamp1:1623560753
fmt.Printf("current timestamp2:%v\n", timestamp2)  // current
timestamp2:1623560753965606600
}
  • 使用time.Unix() 函数可以将时间戳转为时间格式
1
2
3
4
5
6
7
8
9
10
11
12
func timestampDemo2(timestamp int64) {
timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
fmt.Println(timeObj)
year := timeObj.Year()     //年
month := timeObj.Month()   //月
day := timeObj.Day()       //日
hour := timeObj.Hour()     //小时
minute := timeObj.Minute()  //分钟
second := timeObj.Second()  //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute,
second)
}

2.3 时间间隔

  • time.Duration 是time 包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单
    位。
  • time.Duration 表示一段时间间隔,可表示的最长时间段大约290年。
  • time包中定义的时间间隔类型的常量如下:
1
2
3
4
5
6
7
8
const (
   Nanosecond  Duration = 1
   Microsecond          = 1000 * Nanosecond
   Millisecond          = 1000 * Microsecond
   Second               = 1000 * Millisecond
   Minute               = 60 * Second
   Hour                 = 60 * Minute
)

2.4 时间格式化

  • 时间类型有一个自带的方法Format 进行格式化
  • 需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S
  • 而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。
  • 补充:如果想格式化为12小时方式,需指定PM 。
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now()
// 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan")) // 2021-06-13
13:10:18.143 Sun Jun
}
  • 字符串转时间类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"time"
)

func main() {
loc, _ := time.LoadLocation("Asia/Shanghai")
// 按照指定时区和指定格式解析字符串时间
timeObj, err := time.ParseInLocation("2006-01-02 15:04:05", "2019-08-04 14:15:20",
loc)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(timeObj) // 2019-08-04 14:15:20 +0800 CST
}

2.5 时间操作函数

  • Add
    • 我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求
    • Go 语言的时间对象有提供Add 方法如下
  • Sub
    • 求两个时间之间的差值
1
2
3
4
5
6
7
package main

import (
"fmt"
"time"
)

三、encoding-json包

3.1 struct与json

  • 比如我们 Golang 要给 App 或者小程序提供 Api 接口数据,这个时候就需要涉及到结构体和Json 之间的相互转换
  • GolangJSON 序列化是指把结构体数据转化成 JSON 格式的字符串
  • Golang JSON 的反序列化是指把 JSON 数据转化成 Golang 中的结构体对象
  • Golang 中 的 序 列 化 和 反 序 列 化 主 要 通 过 "encoding/json" 包 中 的 json.Marshal() 和 json.Unmarshal()方法实现

1、struct转Json字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
ID int
Gender string
name string     //私有属性不能被 json 包访问
Sno string
}
func main() {
var s1 = Student{
ID: 1,
Gender: "男",
name: "李四",
Sno: "s0001",
}
fmt.Printf("%#v\n", s1)  // main.Student{ID:1, Gender:"男", name:"李四",
Sno:"s0001"}
var s, _ = json.Marshal(s1)
jsonStr := string(s)
fmt.Println(jsonStr)   // {"ID":1,"Gender":"男","Sno":"s0001"}
}

2、Json字符串转struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
ID int
Gender string
Name string
Sno string
}
func main() {
var jsonStr = `{"ID":1,"Gender":"男","Name":"李四","Sno":"s0001"}`
var student Student   //定义一个 Monster 实例
err := json.Unmarshal([]byte(jsonStr), &student)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
// 反序列化后 student=main.Student{ID:1, Gender:"男", Name:"李四", Sno:"s0001"}
student.Name=李四
fmt.Printf("反序列化后 student=%#v student.Name=%v \n", student, student.Name)
}

3.2 struct tag

1、Tag标签说明

  • Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
  • Tag 在结构体字段的后方定义,由一对反引号包裹起来
  • 具体的格式如下:
1
key1:"value1" key2:"value2"
  • 结构体 tag 由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。
  • 同一个结构体字段可以设置多个键值对 tag,不同的键值对之间使用空格分隔。
  • 注意事项:
    • 为结构体编写 Tag 时,必须严格遵守键值对的规则。
    • 结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,
      通过反射也无法正确取值。
    • 例如不要在 key 和 value 之间添加空格。

2、Tag结构体转化Json字符串

1
2
3
4
5
6
7
8
9
10
11
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
ID int `json:"id"`       //通过指定 tag 实现 json 序列化该字段时的 key
Gender string `json:"gender"`
Name string
Sno string

3、Json字符串转成Tag结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
ID int `json:"id"` //通过指定 tag 实现 json 序列化该字段时的 key
Gender string `json:"gender"`
Name string
Sno string
}
func main() {
var s2 Student
var str = `{"id":1,"gender":"男","Name":"李四","Sno":"s0001"}`
err := json.Unmarshal([]byte(str), &s2)
if err != nil {
fmt.Println(err)
}
// main.Student{ID:1, Gender:"男", Name:"李四", Sno:"s0001"}
fmt.Printf("%#v", s2)
}

四、Flag

4.1 Flag

  • Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。

flag.Parse()

通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。

支持的命令行参数格式有以下几种:

  • -flag xxx (使用空格,一个-符号)

  • –flag xxx (使用空格,两个-符号)

  • -flag=xxx (使用等号,一个-符号)

  • –flag=xxx (使用等号,两个-符号)

其中,布尔类型的参数必须使用等号的方式指定。

Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–”之后停止。

2、其他函数

  • flag.Args() ////返回命令行参数后的其他参数,以[]string类型

  • flag.NArg() //返回命令行参数后的其他参数个数

  • flag.NFlag() //返回使用的命令行参数个数

4.2 完整示例

1、main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import (
"flag"
"fmt"
"time"
)

func main() {
//定义命令行参数方式1
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "延迟的时间间隔")

//解析命令行参数
flag.Parse()
fmt.Println(name, age, married, delay)
//返回命令行参数后的其他参数
fmt.Println(flag.Args())
//返回命令行参数后的其他参数个数
fmt.Println(flag.NArg())
//返回使用的命令行参数个数
fmt.Println(flag.NFlag())
}

2、查看帮助

1
2
3
4
5
6
7
8
9
C:\aaa\gin_demo>  go run main.go --help
 -age int
       年龄 (default 18)
 -d duration
       延迟的时间间隔
 -married
       婚否
 -name string
       姓名 (default "张三")

3、flag参数演示

1
2
3
4
5
C:\aaa\gin_demo>  go run main.go -name pprof --age 28 -married=false -d=1h30m
pprof 28 false 1h30m0s
[]
0
4

4、非flag命令行参数

1
2
3
4
5
C:\aaa\gin_demo>go run main.go a b c
张三 18 false 0s
[a b c]
3
0