0%

go(1)

惊鸿一瞥

Hello,World

1
2
3
4
5
6
7
8
9
package main	

import {
"fmt"
}

func main() {
fmt.Println("Hello, World!")
}

package关键字声明代码所属的包,本例中包名为main。
import关键字导入需要的包
func关键字声明函数
当运行一个Go程序,总是从main包的main函数开始

fmt包:format之缩写,提供格式化输入输出函数

此外,大括号有其唯一的制定格式

基本知识

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 "fmt"

func main() {
//声明常量
const ( days = 365
name = "days"
)
//声明变量
var (
dream = "I have a dream"
new_dream = "I had a dream"
)
//格式化输出
fmt.Println("One year has",days,name)
fmt.Print("One year has ")
fmt.Print(days)
fmt.Print(" days\n")
fmt.Printf("One year has %d days\n",days)
fmt.Println(dream,"\n",new_dream)
}

Println会自动补换行符

Go中运算符和C中大抵相同,但没有++count这种前置增量操作

基本控制结构

在go中,true是唯一真值,false是唯一假值

比较运算符:==,!=,<=,<,>=,>

Go不允许文本和数值直接比较

1
2
3
4
5
6
7
8
9
10
func demo(){
if 判断语句{
//do sth.
}
else if 判断语句{
//do sth.
}
else 判断语句{
//do sth.
}

逻辑与:&&
逻辑或:||
逻辑非:!

Switch实现分支判断:

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

import "fmt"

func main() {
const days = 365

switch days {
case 100:
fmt.Println(100,"days")
case 200,365://使用逗号分隔多个可选值
fmt.Println("hhhh")
fallthrough
default:
fmt.Println("23333333")
}
}

for实现循环结构
关于range关键字:

1
2
3
4
5
//假设array_cut是一个数组或切片
//index和value分别对应了下标和值
for index,value := range array_cut{
//do sth.
}

作用域

go作用域:以大括号分界
虽然没有用到大括号,但default和case都i引入了新的作用域

简短声明:

1
2
3
//两方法等价
var name = 'ubuntu'
name := 'ubuntu'

简短声明作用:
以下三种情况,变量作用域都在大括号内
这种使用方法无法用var声明替代

1
2
3
4
5
6
7
8
9
10
11
for count := 10; count > 0;count--{
//do sth.
}

if count := rand.Intn(3); count == 0{
//do sth.
}

switch num := rand(10); num{
//do sth.
}

类型

浮点数

1
2
3
4
5
6
7
8
9
10
//双精度浮点数
age := 19.2
var age =19.2
var age float64 = 19.2

//单精度浮点数
var age float32 = 19.2

//默认零值
var age

整数

1
2
3
4
5
6
year := 2021
var year = 2021
var year int 2021

//e12表示10的12次方
var distance int64 = 2.33e12

big包

big包提供3种类型:

  • 存储大整数big.Int
  • 存储任意精度浮点数big.Float
  • 存储分数big.Rat
1
2
3
4
5
6
7
8
9
10
//创建int64类型
hhhhh := big.NewInt(2333333)

//创建很大很大的数
hhhhhhh := new(big.Int)
hhhhhhh.SetString("23333333333333333",10)

//Go语言不会为常量推断类型,而是默认无类型(untyped)
//由big包提供底层支持
const distance = 23333333333333333333333333

多语言文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//go语言推断双引号内字面值为string类型
peace := "peace"
var peace = "peace"
var peace string = "peace"

//声明零值
var peace string

//反引号包围的文本不考虑转义字符`
var peace 'hhhhhhh\n'//不会输出换行符,而是输出/n

//反引号可跨越多行,打印时将包含这些行
var peace 'hhhhh
hhhhhhhh
hhhhh'
1
2
3
4
5
6
//可以独立访问字符串中的单个字符,但无法单独修改
hhhh = 'laugh'
h = laugh[4]

//非法操作:
laugh[4] = 'd'

类型转换

类型转换:
转换一个变量,只需用与目标类型同名的函数来包裹该变量

1
2
3
4
5
6
7
8
//字符串拼接
hhhh = 'laugh ' + 'is the power of birth.'

//非法:
hhh ='laugh ' + 'is the power of birth.' + 10

//合法
hhh = 'laugh ' + 'is the power of birth.' + '10'

这部分与c语言大致相同
浮点数转换为整数,没有任何舍入,而是直接截断

关于字符串转换

1
2
3
4
5
6
7
8
9
10
11
12
//根据代码点转换
fmt.Println(string(960),string(940),string(969),string(33))
//输出π ά ω !

//转换为字符串方法:
//使用strconv包的Itoa函数
hhh ='laugh ' + 'is the power of birth.' + strconv.Itoa(10)

//字符串转换为整数
//使用strconv包的Atoi函数
const hhh string = '23333'
number := strconv.Atoi(hhh)

代码点:
一个数字对应一个特殊字符
如:π ά ω !分别对应960 940 969 33

构建块

函数

基本格式:

func function_name( [parameter list] ) [return_types] { 函数体 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func Add(number1 int,number2 int)(sum int){
sum = number1 + number2
return sum
}

func Add(number1 int,number2 int)(sum int){
sum = number1 + number2
return //省略了返回值的名字
}

func Calculate(number1 ,number2 int)(sum,multitude int){//减少了int的使用次数
sum = number1 + number2
multitude = number2*number1
return sum,multitude
}

func Calculate(number1 ,number2 int)(sum,multitude int){
sum = number1 + number2
multitude = number2*number1
return

方法

声明新类型:
格式:

1
type 新类型名称 底层类型

注意:

1
2
3
4
5
6
7
8
9
10
type type1 float64
type type2 float64

var number1 type1 := 10
var number2 type2 := 15
var number3 float64 = 20

//以下操作非法.尽管底层相同,他们仍为不同的类型
numebr1 = number2
number2 = number3

在go语言中,没有类和对象
格式:

1
2
3
func (接受者) 方法名(形参列表) (返回值列表){
//do sth.
}

接受者是某种非内置类型的变量
用于建立类型和方法之间的关联

实例:

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

import "fmt"

type myInt int

func (number1 myInt) calculate(number2 int) (sum int,multitude int){
return int(number1)+number2,int(number1)*number2
}

func main() {
var number1 myInt = 10
var number2 =20
var sum int
var multitude int
sum,multitude = number1.calculate(number2)
fmt.Println(sum,multitude)
}

一等函数

在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
29
30
31
32
33
34
35
36
package main

import "fmt"

type myInt int64

func Add(number1,number2 int) myInt {
return myInt(number1+number2)
}

func Multitude(number1,number2 int) myInt{
return myInt(number1*number2)
}

func main() {
//将函数赋值给变量方法一:
func1 := Add
//将函数赋值给变量方法二
var func2 func(int,int) myInt = Multitude

var number1 int = 10
var number2 int = 20
var result myInt

result = func1(number1,number2)
fmt.Println(result)
result = func2(number1,number2)
fmt.Println(result)

func1 = func2
result = func1(number1,number2)
fmt.Println(result)
func2 = Add
result = func2(number1,number2)
fmt.Println(result)
}

将函数传递给其他函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type myInt int64
//声明函数类型
type myFunc func(int,int) myInt

func Add(number1,number2 int) myInt {
return myInt(number1+number2)
}

func Multitude(number1,number2 int) myInt{
return myInt(number1*number2)
}

func Add_Multitude(number1,number2 int, func1,func2 myFunc) (sum,multitude myInt){
sum = func1(number1,number2)
multitude = func2(number1,number2)
return sum,multitude
}

闭包和匿名函数

匿名函数在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
29
30
31
32
package main

import "fmt"

type myFunc func() int

func func1(func2 myFunc,offset int) myFunc{
return func() int {
return func2() + offset
}
}

func func2() int{
return 10
}

func main() {
//匿名函数赋值给变量
func4 := func(message string){
fmt.Println(message)
}
func4("hhhhhhhhhhhhhhhhhhhhhhh")

//同时声明和调用匿名函数
func(){
fmt.Println("hhhhhhhhh")
} ()

//从函数里返回另一个函数
func3 := func1(func2,20)
fmt.Println(func3())
}

可变参数函数

在这里插入图片描述
声明放在函数形参的最后一位
可以传入多个参数或不传参数

1
2
3
4
5
6
7
func f(name...string) {
//do sth.
}

cut_test := []string{"sss","hhh","2333"}
//将cut_test的多个元素传递给函数
cut = f(cut_test..)

收集器

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var planests [8]string

planest[0] = "Mercury"
mercury := planets[0]
fmt.Println(len(planest))//输出8

//未赋值的元素将默认为零值
var numbers = [5]int{1,2,3}

//也可以不用指定数组长度
var numbers = [...]int{
1,
2,
3,
}//结尾逗号是必需的

数组用于赋值或传递给参数时会产生一个副本
故函数内部对数组的修改不会影响原数组

数组嵌套定义即形成所谓的多维数组

切片

  • 对于数组

切片数组不会导致数组被修改,只是创建了指向数组的视图,把该视图称为切片类型

格外注意的是,切片或切片的切片都是数组的视图,对他们进行修改会导致原数组的修改

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"
"sort"
)

func main() {
numbers := [5]int{1,2,3,4,5}
cut1 := numbers[0:3]
cut2 := numbers[0:2]
fmt.Println(numbers,cut1,cut2)
cut2[1] = 100
fmt.Println(numbers)

//可以切割string类型,将创建另一个字符串,两个字符串相互独立
name := "yangzi"
name1 := name[:4]
fmt.Println(name,name1)
// name 和 name1 都不会改变
name = "hhhhh"
fmt.Println(name,name1)
name1 = "233333"
fmt.Println(name,name1)

//创建完全切片方法一:
//numbers2 := numebrs[:]

//创建完全切片方法二:
numbers3 := []string{"11","40","ann","ccc","bbbb"}

//切片可以带有方法,纯数组不行
sort.StringSlice(numbers3).Sort()
fmt.Println(numbers3)
}
  • 关于sort:
    标准库sort包声明了一种StringSlice类型:
1
type StringSlice []string

该类型还有关联的Sort方法:

1
func (p StringSlice) Sort()

Go语言通过切片和append函数来实现动态数组的功能

切片长度:切片中可见元素的数量
切片容量:切片的底层数组决定了切片的容量

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
45
46
47
package main

import "fmt"

func main(){
//切片的底层数组长度为5
//切片长度为5,容量为5
vocabulary := []string{"terminally","visible","lethal","legally","prescribe"}

//此时,append函数将切片元素复制到新创建的数组里面并将新创建的数组作为切片的底层数组,该数组长度为原底层数组的两倍
//切片长度为8,容量为10
vocabulary = append(vocabulary,"partnership","compassion","publication")

//三索引切片操作:
//三索引操作旨在避免对底层数组的修改
arry_vocabulary := [...]string{"candidness","dose","medication","constitutionally","uphold","legislation",}
//切片长度为4,容量为4
cut_vocabulary := arry_vocabulary[0:4:4]
fmt.Println(cut_vocabulary)
cut_vocabulary = append(cut_vocabulary,"spark")
fmt.Println(cut_vocabulary)
fmt.Println(arry_vocabulary)

//两索引
//长度为2,容量为6(与底层数组相同),可以看到底层数组仍未被修改
cut_vocabulary2 := arry_vocabulary[0:4]
cut_vocabulary2 = append(arry_vocabulary[:],"notable")
fmt.Println(cut_vocabulary2)
fmt.Println(arry_vocabulary)

//三索引但容量大于长度
cut_vocabulary3 := arry_vocabulary[0:4:5]
cut_vocabulary3 = append(cut_vocabulary,"spark")
fmt.Println(cut_vocabulary3)
fmt.Println(arry_vocabulary)

//使用make函数对切片容量实行预分配
//切片长度为0,容量为10
test_cut1 := make([]string,0,10)
test_cut1 = append(test_cut1,"hhhhhhhhhhhhhh")


//切片长度为10,容量为10,元素默认为零值
test_cut2 := make([]string,10)
//此时将向切片中追加第11个元素
test_cut2 = append(test_cut2,"hhhhhhhhh")
}

映射

映射(map):可将键映射到值
与数组和切片使用序列整数作为索引不同的是,映射的键几乎可以是任何类型

可以理解为Python中的字典

Go语言必须为映射的键和值指定类型
格式:

1
map[键的类型]值的类型

示例:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import "fmt"

func main(){
//声明map方法一:
map1 := map[string]int{
"key1":1,
"key2":2,
}

//声明map方法二
map2 := make(map[string]int)
map2["New_York"] = 1
map2["San_Franscisco"] = 2
map2["Tokyo"] = 3

//数组或一些基本类型在赋值给新变量或传递至函数/方法时都会创建相应的副本,但映射不会
map3 := map1
map3["key1"]=2
fmt.Println(map3)
fmt.Println(map1)

//遍历映射
//遍历键
for key := range map2{
fmt.Print(key)
}
fmt.Print("\n")

//遍历值
for _,value := range map2{
fmt.Print(value)
}
fmt.Print("\n")

//遍历键值
for key,value := range map2{
fmt.Println(key,value)
}

//删除键值对
//delete(map_name,key_name)
delete(map2,"New_York")

//判断某一个键值对是否存在
//当键值对不存在,输出对应数据类型的零值
fmt.Println(map2["New_York"])

//使用value, ok := map[key]的写法判断
map2["Shenzheng"] = 0
if value,ok := map2["Shenzheng"]; ok{
fmt.Println(value," is exited.")
}else {
fmt.Println("not exited")
}
}

状态与行为

状态:可以是值或结构package 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
35
36
37
38
39
40
41
42
43
44
45
46
package main

import "fmt"

//定义结构
type city struct{
country,name string
rank int
}

//定义结构
type city struct{
country,name string
rank int
}

func main(){
//声明方法一:
var newYork city
newYork.rank = 1
newYork.name = "New York"
newYork.country = "American"

//声明方法二:
var tokyo = city{country:"Japan",rank:2}
//未给定字段则自动声明为对应的零值
fmt.Println(tokyo.name)
tokyo.name = "Tokyo"

//结构复制
//两个结构变化不会对彼此有任何影响
anotherTokyo := tokyo
anotherTokyo.rank = 3
fmt.Println(anotherTokyo,"\n",tokyo)

//由结构组成的切片
cities := []city{newYork,tokyo}
fmt.Println(cities)

//临时使用的匿名结构
hhhh := struct {
ccc int
ddd string
}{2333333,"2333333333"}
fmt.Println(hhhh)
}

Go面向对象

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
29
30
31
32
33
34
35
package main

type literallyPopulation struct {
literalRate float64
totalPopulation int
}

type country struct {
countryName string
//运用组合
theLiterallyPopulation literallyPopulation
}

//实现构造函数
func newCountry(countryName string, literalRate float64,totalPopulation int) (c country){
l := literallyPopulation{literalRate:literalRate,totalPopulation:totalPopulation}
c = country{countryName:countryName,theLiterallyPopulation: l}
return c
}

func (theLiteralPopulation literallyPopulation) countLiteralPopulation() (float64){
return theLiteralPopulation.literalRate*float64(theLiteralPopulation.totalPopulation)
}

//转发
//手动转发
func (c country) countLiteralPopulation() float64{
return c.theLiterallyPopulation.literalRate*float64(c.theLiterallyPopulation.totalPopulation)
}

func main(){
c := newCountry("hhhh",0.34,10000)
lp := c.countLiteralPopulation()
fmt.Println(lp)
}

实现自动转发:

1
//我代码呢?

接口

用于实现多态

接口是一种类型,是一组method的集合
接口命名一般以-er结尾

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package main

import "fmt"

type shower interface {
showName() string
showRank() int
}

type sayer interface {
sayCharacteristic() string
}

//接口嵌套
type sayAndShower interface {
sayer
shower
}

type city struct {
name string
rank int
characteristic string
}

type country struct {
name string
rank int
characteristic string
}

func (city city) showName() string {
return city.name
}

func (city city) showRank() int {
return city.rank
}

func (city city) sayCharacteristic() string {
return city.characteristic
}

func (country country) showName() string {
return country.name
}

func (country country) showRank() int {
return country.rank
}

func (country country) sayCharacteristic() string {
return country.characteristic
}

func main() {
structCity := city{characteristic: "very big", rank: 23333, name: "city of night"}
structCountry := country{characteristic: "nb", rank: 100, name: "nbcountry"}

//指针类型
var show shower
//var say sayer

show = structCity
fmt.Println(show.showRank())
fmt.Println(show.showName())

show = structCountry
fmt.Println(show.showRank())
fmt.Println(show.showName())

say := structCity
fmt.Println(say.sayCharacteristic())

say = structCountry
fmt.Println(say.sayCharacteristic())

var showAndSay sayAndShower
showAndSay = structCountry
fmt.Println(showAndSay.showRank(), showAndSay.showName(), showAndSay.sayCharacteristic())
}

指针

基本使用

几点需要注意的:

  • 对于指向结构的指针,Go中用.即可完成对方法和字段的访问,且解引用不是必须的
  • Go中数组与指针是两种独立的类型
  • 对于指向数组的指针,Go语言提供了自动解引用的特性,即解引用不是必须的
  • Go语言没有为切片和映射提供了自动解引用的特性
  • 映射是一种隐式指针,所以是指针指向映射的举动往往是多此一举
  • 切片实际上是指向数组的指针
  • 指向切片的指针的作用是修改切片本身,包括切片的长度,容量和起始偏移量
  • 方法的接收者和函数形参在处理指针方面十分类似,都可以实现对原有字段的改变
  • 关于指针和接口:指针接收者能仅能接收指针变量,而值接收者可接受指针或非指针变量

nil

nil是指针,切片,映射和接口的零值
解引用nil指针将引发恐慌(panic)

  • nil与方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

type person struct {
score int
}

func (p *person) getScore() {
p.score++ //此处发生了解引用
}

func main() {
var p *person
p.getScore() //解引用nil指针,引发恐慌
}
  • nil与函数:
1
2
var fn func(a,b int) int
fn(1,2) //解引用nil指针引发惊恐
  • nil与切片:
    不包含任何元素的空切片和nil切片并不相等,但通常可以替换使用
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
package main

import "fmt"

func main() {
var slice []int //值为nil的切片

//关键字range,len()和append()等可以正常处理nil切片
for _, ingredient := range slice {
fmt.Println(ingredient) //不会包含任何输出
}

fmt.Println(len(slice))

slice = append(slice, 1, 2, 3)
fmt.Println(slice)
//关键字range,len()和append()等可以正常处理nil切片
for _, ingredient := range slice {
fmt.Println(ingredient) //不会包含任何输出
}

fmt.Println(len(slice))

//不包含任何元素的空切片和nil切片替换使用
slice = append(slice, 1, 2, 3)
fmt.Println(slice)
}

nil与映射
映射在声明之后若没有使用复合字面量或内置make函数初始化,则其值为默认的nil。
可以对值为nil的map进行读取操作,但不可进行写入操作

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

import "fmt"

func main() {
var map1 map[string]int

//进行读取操作
value, ok := map1["hhhh"]
if ok {
fmt.Println(value)
}

for key, value := range map1 {
fmt.Println(key, value)
}
}

nil与接口

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
package main

import (
"fmt"
)

//空接口妙用之一:以空接口作为函数行参,可以向函数传递任何类型的值
func show(x interface{}){
fmt.Printf("Type:%T Value:%v \n",x,x)
}

func main(){
//空接口可以储存任意类型的值
//定义一个空接口的值
var x interface{}

i:= 100
j:= "hhhhhh"
k:= true

x = i
fmt.Printf("Type:%T Value:%v \n",x,x)

x=j
fmt.Printf("Type:%T Value:%v \n",x,x)

x=k
fmt.Printf("Type:%T Value:%v \n",x,x)

show(i)
show(j)
show(k)

//空接口妙用之二:使用空接口实现保存任何值的特点
var stuInfo = make(map[string]interface{})

stuInfo["name"] = "bai"
stuInfo["age"] = 12
stuInfo["sex"] = "nan"
}

并发

扫一下盲

Go中,独立运行的任务谓之goroutine
goroutine之间的通信依赖通道
通道(channel)是一种类型

A.进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行

协程:比线程更轻量级的存在,一个线程可拥有多个协程。协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。(线程是由操作系统内核管理的)

协程可以理解为一个特殊的函数

一个线程的多个协程的运行是串行的。当一个协程运行时,其它协程必须挂起。

上下文切换

  • 进程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户是无感知的。进程的切换内容包括页全局目录、内核栈、硬件上下文,切换内容保存在内存中。进程切换过程是由“用户态到内核态到用户态”的方式,切换效率低。
  • 线程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户无感知。线程的切换内容包括内核栈和硬件上下文。线程切换内容保存在内核栈中。线程切换过程是由“用户态到内核态到用户态”, 切换效率中等。
  • 协程的切换者是用户(编程者或应用程序),切换时机是用户自己的程序所决定的。协程的切换内容是硬件上下文,切换内存保存在用户自己的变量(用户栈或堆)中。协程的切换过程只有用户态,即没有陷入内核态,因此切换效率高。

python两种并发:1,多线程和多进程来进行并发;2,使用协程进程并发

python大部分库基于前一种并发方式,而go则完全基于后一种来开发

sync.WaitGroup

方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0

例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

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
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func sleepyGopher(i int) {
defer wg.Done()
fmt.Println(i, "awake")
}

func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
//使用go关键字启动一个线程
go sleepyGopher(i)
}
//因为main函数本身也是一个协程,而主协程一旦结束,则其它的协程也会结束
//所以需要sync.WaitGroup来实现并发任务的同步
wg.Wait()
}

使用互斥锁同步协程

  • 并发要面临资源竞争问题

最后得到的结果与预期不相符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}
  • 此时引入互斥锁解决数据同步的问题
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"
"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 sub() {
for i := 0; i < 5000; i++ {
//互相竞争资源的步骤必须使用同一把锁
lock.Lock()
x = x - 1
lock.Unlock()
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Println(x)
}

能不用锁就不用锁,因为开锁解锁消耗时间,影响性能

读写锁

对于绝大多数web系统,读多写少

我们必须要在读和写上加锁,但当读的操作很多时,一个读的操作上锁对其它读的操作影响不容小觑。

例如:

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
package main

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

var wg sync.WaitGroup
var lock sync.Mutex

func read() {
defer wg.Done()
lock.Lock()
fmt.Println("读")
time.Sleep(time.Second * 1)
fmt.Println("读毕")
lock.UnLock()
}

func main() {
for i := 0; i < 1000 ; i++{
wg.Add(1)
go read()
}
wg.Wait()
}

此时,我们需要一把读之间不会相互竞争,而读写之间会相互竞争的锁。

引入读写锁。

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
package main

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

var wg sync.WaitGroup
var lock sync.RWMutex

func read() {
defer wg.Done()
lock.RLock()
fmt.Println("读")
time.Sleep(time.Second * 1)
fmt.Println("读毕")
lock.RUnlock()
}

func write() {
defer wg.Done()
lock.Lock()
fmt.Println("写")
time.Sleep(time.Second * 10)
fmt.Println("写毕")
lock.Unlock()
}

func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go read()
}

wg.Add(1)
go write()

wg.Wait()
}

协程间的通信机制

channel提供了一种定向通信机制(python和java常用消息队列实现)

分类:有无缓冲;双向还是单向

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
45
46
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup
var lock sync.Mutex

func consumer(data chan int) {
defer wg.Done()
lock.Lock()
//第一种取法
/* msg,ok := <- data
if(ok){
fmt.Println(msg)
}*/
//使用 for range 来取:
for msg := range data {
fmt.Println(msg)
}

lock.Unlock()
}

func main() {
//定义时必须指定类型
//channel为引用类型,默认为nil
var msg chan int
//使用make初始化的有三种:chan map slice
//若缓冲空间满了还往其中放值会引发错误
//msg = make(chan int) //这种存储方式无缓冲
msg = make(chan int, 1) //第二种存储方式:有缓冲;第二个参数指定缓冲空间
msg <- 1
//利用一个协程来腾空缓冲空间
wg.Add(1)
go consumer(msg)
msg <- 2
//fmt.Println(data)

//关闭通道 1,关闭的channel不能再向其中发送数据 2,消费者可继续消费数据,直到数据被消费完
close(msg)
wg.Wait()
}

  • 无缓冲通道的使用

对于一个无缓冲的channel,在启动一个消费者之前放数据就会报错

在放一个数据到channel中时,会阻塞数据,阻塞之前会放一把锁;只有当取值时,这把锁才会解开

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"
"sync"
)

var wg sync.WaitGroup
var lock sync.Mutex

func consumer(data chan int) {
defer wg.Done()
lock.Lock()
//当通道未关闭时,ok返回true,否则,返回false
for {
msg, ok := <-data
if !ok {
break
}
fmt.Println(msg)
}
lock.Unlock()
}

func main() {

var msg chan int

msg = make(chan int)

//使用无缓冲通道时,在向管道发送信息之前,另一端必须要有消费者
go consumer(msg)
msg <- 1
msg <- 3
close(msg)
wg.Wait()
}

双向和单向的channel

能使用单向channel就尽量使用单向channel

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
45
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup
var lock sync.Mutex

//只能从中取值,不能从中放值
func consumer(data <-chan int) {
defer wg.Done()
lock.Lock()
for {
msg, ok := <-data
if !ok {
break
}
fmt.Println(msg)
}
lock.Unlock()
}

//定义一个只能往其中放值,不能从中取值的channel
func producer(data chan<- int) {
defer wg.Done()
for i := 0; i < 1000; i++ {
data <- i
}
}

func main() {
var msg chan int

msg = make(chan int)

//使用无缓冲通道时,在向管道发送信息之前,另一端必须要有消费者
//普通的channel可直接转换为单向的channel,反之则不行
go consumer(msg)
go producer(msg)

close(msg)
wg.Wait()
}

select

select作用域通讯场景,伪随机选择一组可能的send和receive操作去执行。

考虑有多个case等待处理的情况,比如同时有多个channnel可以接收数据,那么select会伪随机选择一个case去处理;若无case需要处理,那么select会选择default取处理;如果没有default,那么select会阻塞,直到某个case需要处理。

select应用场景:

  • timeout超时机制
  • 判断channel是否被阻塞
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
//timeout超时机制
package main

import (
"fmt"
"time"
)

func main() {
var msg chan int

msg = make(chan int, 1)

go func() {
time.Sleep(time.Second * 2)
msg <- 1
}()
select {
case res := <-msg:
fmt.Println(res)
case <-time.After(time.Second):
fmt.Println("timeout 1")
}
close(msg)
}
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
//判断channel是否被阻塞
//若所有的channel都被阻塞了,那么就会执行default
package main

import (
"fmt"
"time"
)

func main() {
msg1 := make(chan int, 1)
msg2 := make(chan int, 1)

go func() {
time.Sleep(time.Second * 3)
msg1 <- 1
}()
go func() {
time.Sleep(time.Second * 2)
msg2 <- 1
}()

select {
case res := <-msg1:
fmt.Println(res)
case res := <-msg2:
fmt.Println(res)
default:
fmt.Println("timeout")
}
close(msg1)
close(msg2)
}

场景:

假设在有一个goroutine,希望它在一定时间内监控cpu信息,之后退出

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 stop chan bool = make(chan bool)
var wg sync.WaitGroup

func cpuInfo() {
defer wg.Done()
for {
select {
case <-stop:
fmt.Println("monitor closes")
return
default:
time.Sleep(time.Second)
fmt.Println("monitor is running")
}
}
}

func main() {
wg.Add(1)
go cpuInfo()
time.Sleep(6 * time.Second)
stop <- true
wg.Wait()
fmt.Println("goroutine finishes")
}

context

考虑更复杂的场景,若有多个监控协程在运行,还想用上面的select加channel的方式来指定时间退出的操作,我们就需要向管道中传多个值。

这时引入context:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

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

var wg sync.WaitGroup

func cpuInfo(ctx context.Context) {
defer wg.Done()
for {
select {
//检验context是否cancel
case <-ctx.Done():
fmt.Println("cpu monitor finishes")
return
default:
time.Sleep(time.Second)
fmt.Println("cpu monitor is running")
}
}
}

func memoryInfo(ctx context.Context) {
defer wg.Done()
//若父context cancel,则子context也会cancel
ctx2, _ := context.WithCancel(ctx)
for {
select {
case <-ctx2.Done():
fmt.Println("memory monitor finishes")
return
default:
time.Sleep(time.Second)
fmt.Println("memory monitor is running")
}
}
}

func indicatorInfo(ctx context.Context) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("indicator monitor finishes")
return
default:
time.Sleep(time.Second)
fmt.Println("indicator monitor is running")
}
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())

wg.Add(3)
go cpuInfo(ctx)
go memoryInfo(ctx)
go indicatorInfo(ctx)
time.Sleep(6 * time.Second)
cancel()
wg.Wait()
fmt.Println("all monitors finish")
}

几种context:

1
2
3
context.WithCancel() //可取消,手动cancel
context.WithDeadline() //可取消,在某一个时间点之前取消,也可手动取消
context.WithTimeout() //可取消,设置超时间,也可在timeout之前手动取消

异常处理

随便说说

  • go异常处理思想:
    若一个函数可能出现异常,那么就应把异常作为返回值,没有异常就返回nil

关于defer

  • defer之后只能是函数调用,不能是表达式

  • 若有多个defer,则会创建一个defer栈,后注册的defer先执行

  • 向defer中传递的函数是值,不是引用;压入的不仅是函数,还有参数

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

    import (
    "fmt"
    )

    func main() {
    f := func() {
    fmt.Println("hhhh")
    }
    defer f()
    f = func() {
    fmt.Println("23333")
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //输出1
    package main

    import (
    "fmt"
    )

    func main() {
    x := 1
    f := func(a int) {
    fmt.Println(a)
    }
    defer f(x)
    x++
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //输出2
    package main

    import (
    "fmt"
    )

    func main() {
    x := 1
    f := func(a *int) {
    fmt.Println(a)
    }
    defer f(&x)
    x++
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //输出2,这是由于闭包的特性
    package main

    import (
    "fmt"
    )

    func main() {
    x := 1
    //使用的是全局的值,上面几个例子优先使用局部变量
    f := func() {
    fmt.Println(x)
    }
    defer f()
    x++
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //输出1,优先使用局部变量
    package main

    import (
    "fmt"
    )

    func main() {
    x := 1
    f := func(x int) {
    fmt.Println(x)
    }
    defer f(x)
    x++
    }

panic和recover

错误和异常概念区分:

  • 错误:错误你可以预判

  • 异常:不可预判,往往是由于代码不严谨

错误:

错误的场景:

  • 文件IO
  • 数据库连接
  • 空指针引用
  • 下标越界
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 (
"errors"
"fmt"
)

func f(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("hhhhh")
}
return a / b, nil
}

func main() {
a := 1
b := 0
r, err := f(a, b)
if err != nil {
fmt.Println(err)
}
fmt.Println(r)
}

抛出异常和捕捉异常:

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 f(a, b int) int {
if b == 0{
panic("hhhhh")
}
return a / b
}

func main() {
a := 1
b := 0
//recover必须执行
defer func() {
err := recover()
if err != nil {
fmt.Println(err)
}
}()
//这样无效
//defer recover()
fmt.Println(f(a, b))
}
  • 在任一子协程中使用panic抛出错误会导致主协程挂掉,同时导致其它的协程挂掉

  • 在一个协程中再开一个协程,父协程捕获不到子协程抛出的异常,所以会出现上述问题

  • 一种处理方法:单独写一个函数用于捕获异常,每开一个协程就defer这个函数