红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【Golang】Golang快速上手

基于视频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
f1
  • defer定义了在当前函数结束时执行的操作,类似于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:获取输入参数接口中数据的值,若接口为空返回0
  • TypeOf:动态获取输入参数接口中值的类型,若接口为空返回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字段