基于视频8小时转职Golang工程师 by 刘丹冰做的笔记
也有看了一点官方文档的部分补充
Go开发环境
/golang文件夹下
- src:源码仓库
- bin:编译环境,将该文件夹配置到环境变量中
Linux
在家目录下的~/.bashrc中配置环境变量:
1
2
3
4
# 设置路径
export GOROOT=/usr/local/go
export GOPATH=$HOME/go # 工作路径,可以自定义
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin运行source ~/.bashrc更新环境
Windows
在环境变量中Path中添加go根目录bin文件夹作为GOROOT
在环境变量中添加GOPATH作为工作目录,之后可以使用modules独自管理
基本语法
程序书写
1
2
3
4
5
6
package main // 导包, 一般当前有main函数就要导入main包
import "fmt"
// main函数
func main(){
fmt.Println("hello world!")
}golang中有无分号均可以编译,编译器会自动添加分号
import是导包语句,需要使用相应内容时需要导入对应的包
1
2
3
4
5
import "包名" // 导入单个
import (
"包1"
"包2"
) // 导入多个- 函数的左花括号一定要和当前函数名在同一行,习惯需要改
编译运行
go run 源代码:编译+运行go build 源代码:编译,编译后生成的可执行文件可以直接运行go fmt 源代码:格式化
变量
使用
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 main(){
// 声明一个变量并初始化, 默认值为0
var a int
var b int = 10
var c float64
var d string = "abc"
fmt.Println("a=", a)
fmt.Println("b=", b)
fmt.Println("c=", c)
fmt.Println("d=", d)
// 初始化时可以省去数据类型,通过值自动匹配当前变量的数据类型
var e = 10
var f = "acd"
// 格式化输出,%T指输出当前变量的数据类型
fmt.Printf("type of e = %T\n", e)
fmt.Printf("type of f = %T\n", f)
// 省略var关键字,自动匹配,使用:=自动声明+赋值, 这种方法无法声明全局变量
g := 100
fmt.Printf("type of g = %T\n", g)
fmt.Println("g=", g)
// 声明多个变量
var a1, a2 int = 10, 20 // 相同类型
fmt.Println("a1=",a1,"a2=",a2)
var a3, a4 = "aa", 10 // 不同类型
fmt.Println("a3=",a3,"a4=",a4)
var (
a5 int = 100
a6 bool = true
)
fmt.Println("a5=",a5,"a6=",a6)
}常用的是方法四,直接自动匹配,但是方法四不支持全局变量
- 变量定义了必须用,不然会报错
- 用
_定义匿名变量,可以不使用
实现
GoLang中的变量是由两部分构成的一个pair,分别是变量的类型type和值value
类型又分为静态类型(static type)和具体类型(concrete type)
- 静态类型为基本类型,如int, string等
- 具体类型指interface具体指向的数据类型,或者是系统看得见的类型
常量
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"
func main() {
// 定义常量
const length int = 10
fmt.Println("length=", length)
// 定义枚举类型
const (
A = 0
B = 1
C = 2
)
// iota自动累加1,第一行默认为0
const (
X = 10*iota
Y
Z
)
fmt.Println("X = ", X)
fmt.Println("Y = ", Y)
fmt.Println("Z = ", Z)
}- 使用const定义常量,定义后不能修改
- const括号可以定义枚举,枚举内使用iota实现自增
- 注意:iota自增论行算,若某一行同时定义两个变量,他们对应的iota值是一样的
- iota可以用在表达式里,表达式计算出的结果就是对应的枚举值
控制语句
选择控制
if-else if-else:
1
2
3
4
5
6
7
8
9
if expression1 {
}else if expression2 {
}else if expression3 {
}else {
}不要单行开
switch-case:
1
2
3
4
5
6
7
8
switch expr {
case case1:
statement1
case case2:
statement2
default:
default statement
}expr可以没有
可以通过
fallthrough来表示执行相邻的下一个分支可以在switch后case前写一些简单语句,如声明新变量
1
2
3
4
5
6
7
8
9
10
11
func main() {
switch num := f(); {
case num >= 0 && num <= 1:
num++
case num > 1:
num--
fallthrough
case num < 0:
num += num
}
}循环控制
Golang中只有for循环
1
2
3
for init statement; expression; post statement {
execute statement
}当只保留循环条件expression时就和while一样了
1
2
3
for expression {
execute statement
}下面是一个死循环
1
2
3
for {
execute statement
}可以用for range迭代器来遍历一些可以迭代的数据
1
2
3
for index, value := range iterable {
// body
}你甚至可以迭代一个整型值,字面值,常量,变量
函数
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
package main
import "fmt"
// 定义函数
func fool(a string, b int) int {
fmt.Println("a=", a)
fmt.Println("b=", b)
return 100
}
// 返回多个匿名值
func fool2(a string, b int) (int, int) {
fmt.Println("a=", a)
fmt.Println("b=", b)
return 100, 200
}
// 返回多个有名值
func fool3(a string, b int) (r1 int, r2 int) {
fmt.Println("a=", a)
fmt.Println("b=", b)
r1 = 300
r2 = 400
return r1, r2
}
func main(){
c := fool("abc", 111)
fmt.Println("c=", c)
ret1, ret2 := fool3("eee", 114514)
fmt.Println("ret1 = ", ret1, ",ret2 = ", ret2)
}- 定义函数变量名在前,类型在后
- 有名返回值中定义的变量属于局部变量,生存域为当前大括号
包
包可以区分命令空间
go中创建一个包需要创建一个文件夹,在文件夹的go文件中使用package声明包名称,通常文件夹名和包名相同且同一文件下只有一个包
创建
1
2
3
4
5
6
7
8
9
10
package pkg1
import "fmt"
func Test() {
fmt.Println("test package")
}
func init() {
fmt.Println("pkg1 init")
}流程:创建pkg1文件夹->创建pkg1.go文件->文件中声明包
- 包中的init()函数为初始化函数,导包自动调用
- 包中的所有API都需要开头大写
导入
1
2
3
4
5
6
7
package main
import "pkg1"
func main() {
pkg1.Test()
}- 导入的包路径是从
GOPATH开始的相对目录 - 一个文件夹下只能有一个package,一个package的文件不能在多个文件夹下,若有同名则他们彼此无关,使用时需要给每个目录定一个别名
- 导入的包必须使用
导包逻辑
import别名与匿名导包
使用别名 包路径给导入的包起别名
使用_ 包路径表示一个匿名导包,这样导入的包不使用也不会报错,此时无法使用当前包的方法,但是会执行init
使用. 包路径可以直接将包内方法导入导当前包,不需要用.来执行
1
2
3
4
5
import (
_ "pkg1"
p2 "pkg2"
. "pkg3"
)指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
// 传入指针并修改值
func swap(a *int, b *int) {
t := *a
*a = *b
*b = t
}
func main() {
var a int = 1
var b int = 2
fmt.Println("original a = ", a, ", b = ", b)
swap(&a, &b)
fmt.Println("after a = ", a, ", b = ", b)
}- 定义:
*数据类型,二级指针**数据类型 &用于取地址,*用于解引用- 其实大体和C/C++一样
defer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
func f1() {
fmt.Println("f1")
}
func f2() {
fmt.Println("f2")
}
func f3() {
fmt.Println("f3")
}
func main() {
defer f1()
defer f2()
defer f3()
fmt.Println("hello")
}结果是:
1
2
3
4
hello
f3
f2
f1defer定义了在当前函数结束时执行的操作,类似于cpp的析构函数
当有多个defer时,执行操作顺序按照入栈顺序依次出栈执行,满足LIFO
若同时出现return和defer,执行顺序是return先于defer
数组与切片
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 printArray(array [10]int){
for i :=0; i<len(array); i++ {
fmt.Println(array[i])
}
}
// 传动态数组
func printSlice(array []int){
for _, value := range array {
fmt.Println("value = ", value)
}
}
func main() {
// 固定长数组, 默认为0
var array1 [10]int
array2 := [10]int{1,2,3,4}
// 数组遍历
for i :=0; i<len(array1); i++ {
fmt.Println(array1[i])
}
// 这种遍历根据类型返回多个值, 如数组返回下标与值
for i, v := range array2 {
fmt.Println("index = ", i,", value = ", v)
}
printArray(array2)
// 动态数组
array3 := []int{1,2,3,4}
printSlice(array3)
}- 静态数组需要定义长度,可以用
{}在内部初始化值,默认值为0 - 静态数组传参时需要传完整的类型,且是按值传递
- 切片不需要定义长度,用
{}在内部初始化值 - 切片传参时只需要传入
名字 []类型,且传入的是切片内用于存储该数组地址的变量,类似于传引用
切片详解
切片的声明方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明并初始化
slice1 := []int{1,2,3}
fmt.Println(len(slice1))
// 声明空切片并开空间
var slice2 []int
slice2 = make([]int, 3)
fmt.Println(len(slice2))
// 声明的同时开空间
var slice3 []int = make([]int, 3)
fmt.Println(len(slice3))
// :=+make自动推导
slice4 := make([]int, 3)
fmt.Println(len(slice4))- 声明切片,并初始化
- 声明一个空切片,并开辟空间
- 声明的同时开空间
- 使用:=和make自动推导
注意:判空使用slice==nil
追加与截取
1
2
3
4
5
6
7
8
9
10
11
12
// 容量追加
var num = make([]int, 3, 5)
fmt.Printf("len=%d, cap=%d, slice=%v\n", len(num), cap(num), num)
// num切片追加元素1, 此时len=4, cap=5
num = append(num, 1)
fmt.Printf("len=%d, cap=%d, slice=%v\n", len(num), cap(num), num)
num = append(num, 1)
// 向容量满的切片追加元素,动态扩增
num = append(num, 2)
fmt.Printf("len=%d, cap=%d, slice=%v\n", len(num), cap(num), num)- 切片的容量和长度是两个概念,一个切片有两个指针指向长度的头尾,切片本身的变量名就是指向底层首地址的一个指针
- 长度指左右指针之间的距离
- 容量指左指针至切片末尾的距离
- 其中多出的容量无法访问,但确实有这么一块空间提前开辟了
- 使用make构建切片时,可以指定容量:
make(类型, 长度, 容量) - 使用
cap(切片)获取数组容量 - 使用
append(数组, 元素)向切片追加元素,若切片容量满的情况下追加元素,切片会动态扩容到之前容量的两倍
1
2
3
4
5
6
7
8
9
s := []int{1,2,3}
// 中括号截取,左闭右开, 底层指向同一个切片
s1 := s[0:2]
fmt.Printf("len=%d, cap=%d, slice=%v\n", len(s1), cap(s1), s1)
// copy函数深拷贝,可以将底层slice一同拷贝
s2 := make([]int, 3)
copy(s2, s) // s中的值依次拷贝到s2
fmt.Printf("len=%d, cap=%d, slice=%v\n", len(s2), cap(s2), s2)- 使用中括号截取切片,左闭右开,截取出的切片和原切片底层是同一个数组
- 使用copy函数实现深拷贝,可以将其底层数组一块拷贝
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
package main
import "fmt"
func main() {
// 声明类型,key与value均为string, 默认没有空间
var mymap1 map[string]string
// 使用make给map分配空间
mymap1 = make(map[string]string, 10)
mymap1["lang1"] = "java"
mymap1["lang2"] = "cpp"
mymap1["lang3"] = "python"
mymap1["lang4"] = "go"
fmt.Println(mymap1)
// :=自动推导声明
mymap2 := make(map[int]string)
mymap2[1] = "java"
mymap2[2] = "cpp"
fmt.Println(mymap2)
// 多行声明
mymap3 := map[string]string{
"one": "java",
"two": "cpp",
}
fmt.Println(mymap3)
}- go的map底层是哈希表,无序
- 四种声明方式如上,注意若不通过make声明,map是没有空间的
- 给一个容量已满的map添加元素,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
package main
import "fmt"
// 传参
func printmap(city map[string]string)
{
for key, val := range city {
fmt.Printf("key=%s, val=%s\n", key, val)
}
}
func main() {
city := make(map[string]string)
city["China"] ="Beijing"
city["US"] = "Washington"
city["Japan"] = "Tokyo"
// 遍历
for key, val := range city {
fmt.Printf("key=%s, val=%s\n", key, val)
}
// 删除
delete(city, "Japan")
// 修改
city["US"] = "DC"
printmap(city)
}map常用方法如上
传参时指定对应map类型,值传递,但这里传递的是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
package main
import "fmt"
// 声明数据结构的别名
type myint int
// 定义结构体
type Book struct {
title string
author string
}
// 传参 值传递
func printBook(book1 Book) {
fmt.Println(book1)
}
// 传参 传指针
func chgBook(book1 *Book) {
book1.author = "he"
}
func main() {
// 使用结构体
var book1 Book
book1.title = "Golang"
book1.author = "who"
printBook(book1)
chgBook(&book1)
printBook(book1)
}- 使用
type 别名 原名定义一个数据类型的别名 - 使用时和CPP一样
- 传参时是值传递,若需要修改则传指针
面向对象
定义类
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"
// 类,若类名,属性名,方法名首字母大写,表示其他包能够访问
type person struct {
Name string
Ad int
Level
}
// 对应方法, 结构体值传递
func (this *person) GetName() {
fmt.Println(this.Name)
}
func (this *person) SetName(newName string) {
this.Name = newName
}
func main() {
// 定义对象
p := person{Name:"zhangsan", Ad:100, Level:1}
p.GetName()
p.SetName("lisi")
p.GetName()
}- 用结构体当作类,内部填属性
- 方法定义的时候在func后面指定是哪个类
- 默认对类是值传递
- 类名、属性名、方法名大写,说明是公用的,其他包可以访问,否则只能本包用
继承
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"
type Human struct {
name string
sex string
}
func (this *Human) Eat() {
fmt.Println("eating")
}
func (this *Human) Walk() {
fmt.Println("walking")
}
// 继承
type SuperMan struct {
Human // 继承Human类
level int
}
// override
func (this *SuperMan) Eat() {
fmt.Println("superman eat")
}
// 新方法
func (this *SuperMan) Fly() {
fmt.Println("flying")
}
func main() {
h :=Human{"zhangsan", "female"}
h.Eat()
h.Walk()
s := SuperMan{Human{"wangwu", "male"}, 100}
s.Walk()
s.Eat()
s.Fly()
}- 继承直接在属性里写上父类
- 可以重写也可以定义新方法
- 这样创建类的时候要嵌套着写父类,或者直接var创建变量后,统一赋值
多态
GoLang的多态基于接口实现
创建一个接口:
1
2
3
4
5
6
// 接口
type AnimalIF interface {
Sleep()
GetColor() string
GetType() string
}只要添加interface标识符即可,本质上相当于一个父类指针
形成多态的基本要素:
- 有一个父类(接口)定义方法
- 有子类,需要实现父类的全部接口方法
- 父类类型的变量要指向子类的具体数据变量
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
package main
import "fmt"
// 接口
type AnimalIF interface {
Sleep()
GetColor() string
GetType() string
}
// 具体类1, 只要重写接口里面的所有方法就相当于实现了这个接口
type Cat struct {
color string
}
func (this *Cat) Sleep() {
fmt.Println("Cat is sleep")
}
func (this *Cat) GetColor() string {
return this.color
}
func (this *Cat) GetType() string {
return "Cat"
}
// 具体类2
type Dog struct {
color string
}
func (this *Dog) Sleep() {
fmt.Println("Dog is sleep")
}
func (this *Dog) GetColor() string {
return this.color
}
func (this *Dog) GetType() string {
return "Dog"
}
func showAnimal(animal AnimalIF) {
animal.Sleep()
fmt.Println("color = ", animal.GetColor())
fmt.Println("type = ", animal.GetType())
}
func main() {
var animal AnimalIF // 本质是父类指针
animal = &Cat{"Green"}
animal.Sleep() // 调用的是Cat的方法
animal = &Dog{"Yellow"}
animal.Sleep() // 调用的是Dog的方法
cat := Cat{"Green"}
dog := Dog{"Yellow"}
showAnimal(&cat)
showAnimal(&dog)
}空接口
类型:interface{}
GoLang中的基本类型基本都实现了interface{},所以我们可以用这种类型引用任意的数据类型(类似于java中的Object)
并且,空接口自带类型断言机制,语法类似:变量名.(数据类型),返回两个值value和ok,前者为变量的值,后者为是否是这个数据类型
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 myFunc(arg interface{}) {
fmt.Println("called")
fmt.Println(arg)
// 类型断言,返回value与ok两个值,若触发断言则ok为true
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string")
} else {
fmt.Println("arg is string, value=",value)
fmt.Printf("value type is %T\n", value)
}
}
type Book struct {
auth string
}
func main() {
book:=Book{"GoLang"}
myFunc(book)
myFunc(100)
myFunc("abc")
myFunc(3.14)
}反射
GoLang中的语言底层由一个pair实现,见上面
1
2
3
4
5
6
func main() {
var s string
a = "abc"
var allType interface{}
allType = a
}此时allType本身pair永远不变,type和value分别变成两个指针
若appType赋值,相当于把这两个指针指向对应的type和value
机制
GoLang中的reflect包提供了反射机制,可以根据形参获得对应的type和value
主要包含两个重要函数:
ValueOf:获取输入参数接口中数据的值,若接口为空返回0TypeOf:动态获取输入参数接口中值的类型,若接口为空返回nil
其他更多方法可以查阅文档
用法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
39
40
41
42
package main
import "fmt"
import "reflect"
// 反射的基本用法
func reflectNum(arg interface{}) {
fmt.Println("type: ", reflect.TypeOf(arg))
fmt.Println("value: ", reflect.ValueOf(arg))
}
type User struct {
Id int
Name string
Age int
}
func reflectStru(input interface{}) {
// 获取整个类型的type和value
inputType := reflect.TypeOf(input)
inputValue := reflect.ValueOf(input)
// 进而获取每个字段的type和value
for i:=0; i<inputType.NumField(); i++ {
field:= inputType.Field(i)
value:= inputValue.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
// 还可以获取方法
for i:=0; i<inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}
func (this User) Call() {
fmt.Println("user is called")
fmt.Printf("%v\n", this)
}
func main() {
var num float64 = 1.2345
reflectNum(num)
user :=User{1,"lihua",18}
reflectStru(user)
}对于基本的类型,我们可以直接传给
interface{}arg,此时接口值内部保存了动态类型和动态值两部分信息,可以使用TypeOf取出动态类型,使用ValueOf取出动态值更复杂一点,对于一个空接口,我们不知道传入的具体情况,可以用反射来获取接口内各个字段的具体类型和值
- 首先获取整个类型的type和value
- 通过
NumField()方法可以获得结构体字段数量,NumMethod()方法可以获得结构体方法数量 - 通过
inputType.Field(i)获取第i个字段的类型信息,类型为StructField,包含了字段名、字段类型等,如field.Name是字段名,field.Type是字段类型 - 通过
inputValue.Field(i)获取第i个字段的值,类型为Value,再使用.Interface()把反射类型转换为普通的interface{} - 最后通过
Interface{}把反射的value值转换会普通的接口类型
结构体标签
我们可以给结构体打标签,标签是键值对的形式
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 "reflect"
// 给结构体上标签
type resume struct {
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex"`
}
// 解析标签信息
func findTag(str interface{}) {
// 找到结构体全部元素
t := reflect.TypeOf(str).Elem()
for i:=0; i<t.NumField(); i++ {
// 获得对应key名的标签
tagstring := t.Field(i).Tag.Get("info")
tagdoc := t.Field(i).Tag.Get("doc")
fmt.Println("info: ", tagstring)
fmt.Println("info: ", tagstring)
}
}
func main() {
var re resume
findTag(&re)
}直接在定义结构体成员时,在后面的反括号中添加标签键值对即可
使用反射相关方法
.Tag().Get()可以读取标签
使用结构体标签可以编码/解码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
25
26
27
28
29
30
31
32
33
34
35
package main
import (
"fmt"
"encoding/json"
)
// 带标签,让json知道
type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{"test", 2000, 10, []string{"zhangsan","lisi"}}
// 编码
jsonStr, err := json.Marshal(movie)
if err!=nil {
fmt.Println("JSON marshal error ", err)
return
}
fmt.Printf("jsonStr: %s\n", jsonStr)
// 解码
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err!=nil {
fmt.Println("json unmarshal error ", err)
return
}
fmt.Printf("%v\n", myMovie)
}- JSON相关需要导入库
"encoding/json" - 编码使用
json.Marshal,解码使用json.Unmarshal - 自动生成的JSON键根据你的结构体标签
json:"xxx"来定
GoRoutine
协程
多进程或多线程解决了单线程的阻塞问题,达到了宏观的并发
但是多线程的切换是有切换成本的,这部分是cpu浪费时间成本
总的来说,传统多线程有着高消耗cpu和高内存占用的缺点
为了解决以上问题,新的多线程机制将一个线程分割为线程(内核部分)与协程(用户部分),且一一绑定
这种关系下,一个线程对应一个协程,并发时还需要切换线程,有高切换成本
改进一下,可以通过一个协程调度器用一个线程绑定多个协程,即为n对1的协程关系,这种可以有效解决高消耗cpu的问题
但这种模型会导致其中一个协程阻塞会影响后面协程的进行
最后,我们可以让线程与协程产生n比m的关系,通过协程调度器合理分配,因此协程调度器更好,对并发优化的更好,占用就越少
GoLang对协程的处理
GoLang首先对协程优化,将co-routine改为Goroutine,并优化内存到几kb,因此可以大量生产,且Goroutine可以灵活调度,可以灵活切换
早期调度器
早期的调度器会让想要获取协程的线程获取锁和全局Go协程队列,按照队列的进出获取Goroutine并尝试执行,结束后放回队尾,并尝试还锁
缺点:
这种调度模式创建、销毁和调度G都需要每个线程获取锁,形成锁竞争
线程转移G会造成延迟和额外的系统负载
系统调用导致频繁的线程阻塞和取消阻塞操作增加开销
GMP模型
GMP即Goroutine,Machine thread和processor
Gorouting优先放入某本地队列,满了后放入全局队列
一个P同一时间只能执行一个Go,最大并行数量为GOMAXPROCS个
调度器的设计策略
复用线程
work stealing机制:
当前G1正在被M1的P执行,本地队列中有两个正在等待
此时M2会从M1偷一个G来执行,此为work stealing
hand off机制:
当前G1正在被M1的P执行,G2在M1的队列等待,G3在M2的队列等待,即将操作
若此时G1阻塞,我们会创建/唤醒一个线程M3,将M1的P和队列移到M3,CPU切换到M2/M3执行
若G1持续阻塞,则M1处于睡眠,若G1不再执行,M1就会被销毁
利用并行
我们可以限定P的个数GOMAXPROCS=CPU核数/2
抢占
以往的co-routine和对应的cpu绑定后,若有其他co-routine想要使用该cpu,就只能等待上一个主动释放
现在的go-routine和对应cpu绑定只允许绑定一段时间(10ms),时间到后其他go-routing可以抢占该cpu
全局G队列
依照work stealing机制,若其他本地队列中没有G,而全局G队列中有,它也会从全局G队列中获取,这个过程需要加锁与解锁
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
package main
import (
"fmt"
"time"
)
// 从goroutine
func newTask() {
i:=0
for {
i++
fmt.Printf("new Goroutine : i = %d\n", i)
}
}
// 主goroutine
func main() {
// 创建一个go程,去执行newTask
go newTask()
i:=0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(1*time.Second)
}
}- 使用go关键字后面接匿名/有名函数就可以创建goroutine
- 其中main是主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
package main
import (
"fmt"
"runtime"
"time"
)
func main(){
// 创建一个匿名方法,作为goroutine
go func() {
defer fmt.Println("A.defer")
func () {
defer fmt.Println("B.defer")
// 退出当前goroutine
runtime.Goexit()
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
time.Sleep(1*time.Second)
}
}- 创建的匿名方法前加go就可以直接创建goroutine
- 使用
runtime.Goexit()退出当前goroutine - 注意:作为协程的方法的返回值(若有)无法被变量接收,因为执行是并发的,返回值会被丢弃。协程之间的通信需要用到channel
channel
定义
用于goroutine之间的信息交换
语法:
1
2
make(chan Type)
make(chan Type, capacity)指定channel传输类型与缓冲量
用法
- 向channel中写数据:
channel<-value - 接受并将其丢弃:
<-channel - 从channel中接收数据并赋值:
x:=<-channel - 同时检查管道是否关闭或为空:
x, ok := <-channel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
func main() {
// 定义channel
c := make(chan int)
go func() {
defer fmt.Println("goroutine结束")
fmt.Println("goroutine正在进行")
// 将值发给channel
c <- 666
}()
// 从c中接受数据并赋值给num
num := <-c
fmt.Println("num =",num)
fmt.Println("main goroutine结束")
}运行结果:
1
2
3
4
goroutine正在进行
goroutine结束
num = 666
main goroutine结束阻塞逻辑
channel具有同步两个不同goroutine的能力,保证了一个阻塞队列
若main goroutine提前到达获取channel数据的语句,会进入阻塞,直到sub goroutine传入数据并唤醒阻塞
若sub goroutine提前到达发送channel数据的语句,也会进入阻塞,直到main goroutine开始读取数据并唤醒阻塞,发送数据
channel缓冲
若使用无缓冲的channel,两个goroutine先进入通道的需要阻塞等待另一方,直到数据交换完成都解锁
若使用有缓存的channel,channel允许让我们在管道内存数据
左右侧的goroutine可以一直往channel内放入值和取出值,这两个操作既不同步,也不互相阻塞,直到通道满了或为空,可能会让对应的一方阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int ,3)
fmt.Println("len=", len(c), ",cap=", cap(c))
go func() {
defer fmt.Println("子goroutine结束")
for i:=0; i<4; i++ {
c <-i
fmt.Println("发送元素=",i,"len=", len(c), ",cap=", cap(c))
}
}()
time.Sleep(2*time.Second)
for i:=0; i<4; i++ {
num := <-c
fmt.Println("num=",num)
}
}输出:
1
2
3
4
5
6
7
8
len= 0 ,cap= 3
发送元素= 0 len= 1 ,cap= 3
发送元素= 1 len= 2 ,cap= 3
发送元素= 2 len= 3 ,cap= 3
num= 0
num= 1
num= 2
num= 3删除
- 使用
close()方法可以关闭channel - 读取channel值时,可以多返回一个布尔值,表示channel是否关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i:=0; i<5; i++ {
c<-i
}
close(c)
}()
for {
// ok为true表示channel没有关闭,可以继续读,否则表示已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main finished")
}- 当确实没有任何发送数据时,或你想显式的结束range循环时,才去关闭channel
- 对一个关闭的channel发送数据会爆panic
- 关闭channel后,可以继续接收数据
- 对于一个nil channel,无论收发都会阻塞
与range
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i:=0; i<5; i++ {
c<-i
}
close(c)
}()
// range迭代不断操作channel
for data:= range c{
fmt.Println(data)
}
fmt.Println("main finished")
}可以使用range不断迭代操作channel
与select
select可以完成监控多个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
package main
import "fmt"
func fib(c, quit chan int) {
x, y:=1,1
for {
select {
case c <- x:
// 若c可写,则case进入
x, y= y, x+y
case <- quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i:=0; i<10; i++ {
fmt.Println(<-c)
}
quit <-0
}()
fib(c, quit)
}格式如下:
1
2
3
4
5
6
7
8
select {
case <- chan1:
// 若chan1读到数据,则执行该行处理语句
case chan2 <- 1:
// 若成功向chan2写入数据,则执行该行处理语句
default:
// 若都没有成功,则执行这里的语句
}GoModules
Go Modules是go语言的依赖解决方案,它淘汰了原有的GOPATH的使用模式
基础环境
必须的环境变量:
GO111MODULE:Go Modules的开关,允许设置如下参数- auto:只要项目包含go.mod文件的话就启用Gomodules
- on:启用Gomodules
- off:禁用Gomodules
GOPROXY:设置Go模块代理,用于在后续拉取模块版本时直接通过镜像站快速拉取GOSUMDB:值是一个Go checksum database,用于拉取模块版本时保证拉取到的模块版本数据未经过篡改GONOPROXY/GONOSUMDB/GOPRIVATE:用在内部仓库中,设置GOPRIVATE后,导包不会走PROXY,也不会通过SUMDB校验
通过Modules初始化项目
首先创建一个项目文件夹,cd进入
输入go mod init 模块名,模块名决定导包怎么写
初始化后出现go.mod文件,存储go版本号与模块名
写main文件,若源代码依赖了某个库,我们可以在终端中手动使用go get 模块名就可以拉下来,或者在go run的时候自动拉下来
拉下来的库在go.mod中会写入require,包含以来的模块名,版本,若有//indirect表示间接依赖
在go.sum中罗列出当前项目直接或间接依赖的所有模块版本,包括模块名、版本和哈希校验码(表示整体项目文件的全部文件的校验和哈希)
若校验码不存在,表示依赖的库可能用不上
改变模块依赖关系
如果想回退当前项目用到的包的版本,可以使用go mod edit -replace=之前依赖的版本=要替换的版本
回退后可以在go.mod看到对应的replace字段