引言

学习前置要求

  1. 具备1种后端编程语言开发经验(C/C++/Java/Python/PHP等)
  2. 具备基本的网络编程能力和并发思想
  3. 了解计算机基本体系结构
  4. 了解Linux基础知识

学习提纲

Golang开发环境

Go官网下载地址:https://golang.org/dl/

Go官方镜像站(推荐):https://golang.google.cn/dl/

根据自己系统,自行选择安装。

记得配置GOPROXY~

Go1.14版本之后,都推荐使用go mod模式来管理依赖了,也不再强制我们把代码必须写在GOPATH下面的src目录了,你可以在你电脑的任意位置编写go代码。

默认GoPROXY配置是:GOPROXY=https://proxy.golang.org,direct,由于国内访问不到 https://proxy.golang.org 所以我们需要换一个PROXY,这里推荐使用https://goproxy.iohttps://goproxy.cn

可以执行下面的命令修改GOPROXY

1
go env -w GOPROXY=https://goproxy.cn,direct

Golang语言特性

Golang的优势

  1. 简单的部署方式
    • 可直接编译成机器码
    • 不依赖其他库
    • 直接运行即可部署
  2. 静态类型语言:编译的时候检查出来隐藏的大多数问题
  3. 语言层面的并发
    • 天生的基因支持
    • 充分的利用多核
  4. 强大的标准库
    • Runtime系统调度机制
    • 高效的GC垃圾回收
    • 丰富的标准库
  1. 简单易学
    • 25个关键字
    • C语言简洁基因,内嵌C语法支持
    • 面向对象特征(继承、多态、封装)
    • 跨平台
  2. “大厂”领军

Golang适合做什么

  1. 云计算基础设施领域

​ 代表项目:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。

  1. 基础后端软件

​ 代表项目:tidb、influxdb、cockroachdb等。

  1. 微服务

​ 代表项目:go-kit、micro、monzo bank的typhon、bilibili等。

  1. 互联网基础设施

​ 代表项目:以太坊、hyperledger等。

Golang的不足

  1. 包管理,大部分包都在 github

  2. 所有 Excepiton 都用 Error 来处理(比较有争议)

  3. C 的降级处理,并非无缝,没有 C 降级到 asm 那么完美(序列化问题)

Golang语法新奇

从main函数初见golang语法

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

import (
"fmt"
"time"
)

// main函数
func main() {
fmt.Println("Hello,Golang~")

time.Sleep(1 * time.Second)
}
  • 第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
  • 下一行 import “fmt” 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
  • 下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。

变量的声明

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

import "fmt"

/*
四种变量的声明方式
*/

// 声明全局变量,方法一、二、三是可以的
var gA int = 100
var gB = 200

//用方法四声明全局变量,会报错
//gC := 300

func main() {
//方法一:声明一个变量,默认的值是0
var a int
fmt.Println("a = ", a)
fmt.Printf("type of a = %T\n", a)

//方法二:初始化一个值
var b int = 100
fmt.Println("b = ", b)
fmt.Printf("type of b = %T\n", b)

var bb string = "abcd"
fmt.Println("bb = ", bb)
fmt.Printf("type of bb = %T\n", bb)

//方法三:在初始化时,可以省去数据类型,通过值自动匹配
var c = 100
fmt.Println("c = ", c)
fmt.Printf("type of c = %T\n", c)

//方法四:(常用)省去var关键字,直接自动匹配
d := 100
fmt.Println("d = ", d)
fmt.Printf("type of d = %T\n", d)

//打印全局变量
fmt.Println("gA = ", gA)
fmt.Println("gB = ", gB)
//fmt.Println("gC = ", gC)

//声明多个变量
var xx, yy int = 1, 2
fmt.Println("xx = ", xx, "yy = ", yy)
var kk, qq = 3, "qwer"
fmt.Println("kk = ", kk, "qq = ", qq)

var (
vv int = 100
jj string = "qazx"
)
fmt.Println("vv = ", vv, "jj = ", jj)

}

常量

​ 常量是一个简单值的标识符,在程序运行时,不会被修改的量

​ 常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

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"

// const 来定义枚举类型
const (
// 可以在const() 添加一个关键字iota,每行的iota都会累积啊1,第一行的iota默认为0
BEIJING = iota
SHANGHAI
SHENZHEN
NANJING
)

const (
a, b = iota + 1, iota + 2 // iota = 0, a = 1, b = 2
c, d // iota = 1, c = 2, d = 3
e, f // iota = 2, e = 3, f = 4

g, h = iota * 2, iota * 3 // iota = 3, g = 6, h = 9
i, k // iota = 4, i = 8, k = 12
)

func main() {

//常量(只读属性)
const length = 10
fmt.Println("length = ", length)

fmt.Println("BEIJING = ", BEIJING)

fmt.Println("a = ", a, "b = ", b)
fmt.Println("c = ", c, "d = ", d)
fmt.Println("e = ", e, "f = ", f)

fmt.Println("g = ", g, "h = ", h)
fmt.Println("i = ", i, "k = ", k)

// iota 只能够配合const() 一起使用, iota只有在const进行累加效果

}

函数

函数返回多个值

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 foo1(a string, b int) int {
fmt.Println("a = ", a)
fmt.Println("b = ", b)
c := 100
return c
}

// 返回多个返回值,匿名的
func foo2(a string, b int) (int, int) {
fmt.Println("a = ", a)
fmt.Println("b = ", b)
return 666, 777
}

// 返回多个返回值, 有形参名称的
func foo3(a string, b int) (r1 int, r2 int) {
fmt.Println("---- foo3 ----")
fmt.Println("a = ", a)
fmt.Println("b = ", b)

//r1 r2 属于foo3的形参, 初始化默认的值是0
//r1 r2 作用域空间 是foo3 整个函数体的{}空间
fmt.Println("r1 = ", r1)
fmt.Println("r2 = ", r2)

//给有名称的返回值变量赋值
r1 = 1000
r2 = 2000

return
}

func main() {
c := foo1("ab", 200)
fmt.Println("c = ", c)

ret1, ret2 := foo2("haha", 999)
fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)

ret1, ret2 = foo3("foo3", 333)
fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)

}

init函数与import

  • init函数

​ init 函数可在package main中,可在其他package中,可在同一个package中出现多次。

  • main函数

​ main 函数只能在package main中

  • 执行过程

​ golang里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。

​ 虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数

​ go程序会自动调用init()main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

​ 程序的初始化和执行都起始于main包。

​ 如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。

​ 当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。

​ 等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

​ 下图详细地解释了整个执行过程:

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

import "fmt"

// 当前lib1包提供的API
func Lib1Test() {
fmt.Println("lib1Test()...")
}

func init() {
fmt.Println("lib1.init() ... ")
}
1
2
3
4
5
6
7
8
9
10
11
12
package lib2

import "fmt"

// 当前lib2包提供的API
func Lib2Test() {
fmt.Println("lib2Test()...")
}

func init() {
fmt.Println("lib2.init() ... ")
}
1
2
3
4
5
6
7
8
9
10
11
package main

import (
"GolangStudy/init/lib1"
"GolangStudy/init/lib2"
)

func main() {
lib1.Lib1Test()
lib2.Lib2Test()
}

​ 如果导入了一个包,但没使用,会报错,所以可以声明匿名包,匿名包不能调用方法,但是会执行init()方法

​ 针对每个包也可以起别名。

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

import (
_ "GolangStudy/init/lib1"
mylib2 "GolangStudy/init/lib2"
)

func main() {
//lib1.Lib1Test()
mylib2.Lib2Test()
}

函数参数

​ 函数如果使用参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

​ 调用函数,可以通过两种方式来传递参数:

值传递

​ 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

引用传递(指针传递)

​ Go 语言中指针是很容易学习的,Go 语言中使用指针可以更简单的执行一些任务。

​ 我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。引用传递指针参数传递到函数内,以下是交换函数 swap() 使用了引用传递:

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 a int = 100
var b int= 200

fmt.Printf("交换前,a 的值 : %d\n", a )
fmt.Printf("交换前,b 的值 : %d\n", b )

/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)

fmt.Printf("交换后,a 的值 : %d\n", a )
fmt.Printf("交换后,b 的值 : %d\n", b )
}

func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}

defer

​ defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。

​ defer作用:

  • 释放占用的资源
  • 捕捉处理异常
  • 输出日志

如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。(压栈)

defer和return,return先执行,defer后执行。

recover错误拦截

​ 运行时panic异常一旦被引发就会导致程序崩溃。

​ Go语言提供了专用于“拦截”运行时panic的内建函数“recover”。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。

注意:recover只有在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
package main

import "fmt"

func Demo(i int) {
//定义10个元素的数组
var arr [10]int
//错误拦截要在产生错误前设置
defer func() {
//设置recover拦截错误信息
err := recover()
//产生panic异常 打印错误信息
if err != nil {
fmt.Println(err)
}
}()
//根据函数参数为数组元素赋值
//如果i的值超过数组下标 会报错误:数组下标越界
arr[i] = 10
}

func main() {
Demo(10)
//产生错误后 程序继续
fmt.Println("程序继续执行...")
}

slice和map

​ Go 语言切片是对数组的抽象。

​ Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组“),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

定义切片

​ 你可以声明一个未指定大小的数组来定义切片:

1
myArray := []int

​ 切片不需要说明长度。或使用make()函数来创建切片:

1
2
3
var slice1 []int = make([]int, len)
// 也可以简写为
slice1 := make([]type, len)

​ 也可以指定容量,其中capacity为可选参数。

1
make([]T, length, capacity)

​ 这里 length 是数组的长度并且也是切片的初始长度。

切片初始化

1
2
3
4
slice1 := []int{1,2,3} //声明并初始化,默认值为1,2,3,长度len是3
var slice2 []int //未分配空间
var slice4 []int = make([]int, 3) //通过make开辟空间,初始化值为0
slice5 := make([]int, 3)

len()cap()函数

​ 切片是可索引的,并且可以由 len() 方法获取长度

​ 切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少

切片的追加与截取

​ 切片的扩容机制:append的时候,如果长度增加后超过最大容量,则会自动将容量增加2倍

​ 切片的截取:

1
2
s := []int{1,2,3,4}
s1 := s[0:2] // [0,2)

map

​ map和slice类似,只不过是数据结构不同,下面是map的一些声明方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 第一种声明方式
var myMap1 map[string]string
myMap1 = make(map[string]string, 10)
myMap1["one"] = "php"
myMap1["two"] = "golang"
myMap1["three"] = "java"

// 第二种声明方式
myMap2 := make(map[int]string)
myMap2[1] = "php"
myMap2[2] = "golang"
myMap2[3] = "java"

//第三种声明方式
myMap3 := map[string]string{
"one" : "php",
"two" : "golang",
"three" : "java"
}

struct

1
2
3
4
5
6
7
8
9
10
type Book struct {
title string
auth string
}

func main() {
var book Book
book.title = "Golang"
book.auth = "zhangsan"
}

面向对象特征

类的封装与表示

  • 如果类名首字母大写,表示其他包也能够访问

  • 如果说类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问

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 Hero struct {
//如果说类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问
Name string
Ad int
level int
}

func (this Hero) GetName() {
fmt.Println("Name = ", this.Name)
}

func (this Hero) SetName(newName string) {
this.Name = newName
}

func (this Hero) show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Ad = ", this.Ad)
fmt.Println("level = ", this.level)
}

func main() {
//创建一个对象
hero := Hero{Name: "xiaoxin", Ad: 101, level: 1}
hero.show()
hero.SetName("zhangsan")
hero.show()
}
1
2
3
4
5
6
Name =  xiaoxin
Ad = 101
level = 1
Name = xiaoxin
Ad = 101
level = 1

方法的接收者是值类型,只能读,不能改~

1
2
3
4
5
6
7
8
9
10
11
12
13
func (this *Hero) GetName() {
fmt.Println("Name = ", this.Name)
}

func (this *Hero) SetName(newName string) {
this.Name = newName
}

func (this *Hero) show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Ad = ", this.Ad)
fmt.Println("level = ", this.level)
}

类的继承

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

import "fmt"

type Human struct {
name string
age int
}

func (this *Human) Eat() {
fmt.Println("Human.Eat()...")
}

func (this *Human) Walk() {
fmt.Println("Human.Walk()...")
}

// ======================================

type SuperMan struct {
Human //SuperMan类继承了Human类的方法
level int
}

// 重定义父类方法
func (this *SuperMan) Eat() {
fmt.Println("SuperMan.Eat()...")
}

func (this *SuperMan) Walk() {
fmt.Println("SuperMan.Walk()...")
}

func (this *SuperMan) show() {
fmt.Println("name = ", this.name)
fmt.Println("age = ", this.age)
fmt.Println("level = ", this.level)
}

func main() {
human := Human{"小新", 24}
human.Eat()
human.Walk()

//定义一个子类对象
superMan := SuperMan{Human{"小新不吃蔬菜", 24}, 1}
//superMan.name = "abcd"
//superMan.age = 22
superMan.Eat()
superMan.Walk()

superMan.show()
}

类的多态

示例

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

import "fmt"

// 本质是一个指针
type Animal interface {
Sleep()
GetColor() string //获取动物的颜色
GetType() string //获取动物的种类
}

// 具体的类
type Cat struct {
color string
}

func (this *Cat) Sleep() {
fmt.Println("Cat is sleeping")
}

func (this *Cat) GetColor() string {
return this.color
}

func (this *Cat) GetType() string {
return "Cat"
}

type Dog struct {
color string
}

func (this *Dog) Sleep() {
fmt.Println("Dog is sleeping")
}

func (this *Dog) GetColor() string {
return this.color
}

func (this *Dog) GetType() string {
return "Dog"
}

func showAnimal(animal Animal) {
animal.Sleep()
fmt.Println("color = ", animal.GetColor())
fmt.Println("type = ", animal.GetType())
}

func main() {
var animal Animal
animal = &Cat{"white"}
animal.Sleep()
animal = &Dog{"black"}
animal.Sleep()

cat := Cat{"Green"}
dog := Dog{"Yellow"}
showAnimal(&cat)
showAnimal(&dog)
}

基本要素

  • 有一个父类接口
  • 有子类,实现了全部接口方法
  • 父类类型的变量(指针)指向(引用)子类的具体数据变量

interface与类型断言

​ Golang的语言中提供了断言的功能。Golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为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
package main

import "fmt"

// interface{}是万能数据类型
func myFunc(arg interface{}) {
fmt.Println("myFunc is called ...")
fmt.Println(arg)

//如何区分arg类型
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Println("arg is string type, value = ", value)
fmt.Printf("value type is %T\n", value)
}
}

type Book struct {
name string
}

func main() {
book := Book{"Golang"}
myFunc(book)
myFunc(100)
myFunc("Java")
myFunc(false)
}

反射reflect

编程语言中反射的概念

​ 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。

​ 每种语言的反射模型都不同,并且有些语言根本不支持反射。Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用。

​ 多插一句,Golang的gRPC也是通过反射实现的

interface和反射

​ 学习反射之前,先来看看Golang关于类型设计的一些原则:

  • 变量包括(type, value)两部分
  • type 包括 static typeconcrete type。简单来说 static type是你在编码时看见的类型(如int、string),concrete typeruntime系统看见的类型
  • 类型断言能否成功,取决于变量的concrete type,而不是static type。因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer

​ 接下来要讲的反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说

​ 在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:

1
(value, type)

​ value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。

​ interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。

  • reflect.TypeOf()
  • reflect.ValueOf()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"reflect"
)

func reflectNum(arg interface{}) {
fmt.Println("type: ", reflect.TypeOf(arg))
fmt.Println("value: ", reflect.ValueOf(arg))
}

func main() {
var num float64 = 1.2345
reflectNum(num)
}

结构体标签

基本使用

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"
"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++ {
tagInfo := t.Field(i).Tag.Get("info")
tagDoc := t.Field(i).Tag.Get("doc")
fmt.Println("info:", tagInfo, " doc:", tagDoc)
}
}

func main() {
var re resume
findTag(&re)
}

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

import (
"encoding/json"
"fmt"
)

type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"`
}

func main() {
movie := Movie{"喜剧之王", 2000, 10, []string{"星爷", "张柏芝"}}

// 编码的过程 结构体 ——> json
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marsha error", err)
return
}
fmt.Printf("jsonStr = %s\n", jsonStr)

// 解码的过程 json ——> 结构体
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json unmarshal error", err)
return
}
fmt.Printf("%v\n", myMovie)
}
1
2
jsonStr = {"title":"喜剧之王","year":2000,"rmb":10,"actors":["星爷","张柏芝"]}
{喜剧之王 2000 10 [星爷 张柏芝]}

Golang高阶

goroutine

协程并发

​ 协程:coroutine。也叫轻量级线程

​ 与传统的系统级线程和进程相比,协程最大的优势在于“轻量级”。可以轻松创建上万个而不会导致系统资源衰竭。而线程和进程通常很难超过1万个。这也是协程别称“轻量级线程”的原因。

​ 一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源

​ 多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能也并不完整,比如仅仅提供协程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。

​ 在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少!但能达到进程、线程并发相同的效果。

​ 在一次并发任务中,进程、线程、协程均可以实现。从系统资源消耗的角度出发来看,进程相当多,线程次之,协程最少。

Go并发

Go 在语言级别支持协程,叫goroutine。Go 语言标准库提供的所有系统调用操作(包括所有同步IO操作),都会出让CPU给其他goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不需要依赖于CPU的核心数量。

​ 有人把Go比作21世纪的C语言。第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持并发。同时,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制

​ Go语言为并发编程而内置的上层API基于顺序通信进程模型CSP(communicating sequential processes)。这就意味着显式锁都是可以避免的,因为Go通过相对安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。

​ Go语言中的并发程序主要使用两种手段来实现

  • goroutine
  • channel

什么是Goroutine

​ goroutine是Go语言并行设计的核心,有人称之为go程。 Goroutine从量级上看很像协程,它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

​ 一般情况下,一个普通计算机跑几十个线程就有点负载过大了,但是同样的机器却可以轻松地让成百上千个goroutine进行资源竞争。

创建Goroutine

只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。

​ 在并发编程中,我们通常想将一个过程切分成几块,然后让每个goroutine各自负责一块工作,当一个程序启动时,主函数在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用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
package main

import (
"fmt"
"time"
)

func newTask() {
i := 0
for {
i++
fmt.Printf("new Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}

}
func main() {
// 创建一个go程,去执行newTask()流程
go newTask()

i := 0
for {
i++
fmt.Printf("main Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}

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

import (
"fmt"
"time"
)

func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}

func main() {
//创建一个 goroutine,启动另外一个任务
go newTask()

fmt.Println("main goroutine exit")
}

Goexit函数

​ 调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 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
package main

import (
"fmt"
"runtime"
"time"
)

func main() {

//用go创建承载一个形参为空,返回值为空的一个函数
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)
}
}
1
2
B.defer
A.defer

channel

简介

​ channel是Go语言中的一个核心类型,可以把它看成管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。

​ channel是一个数据类型主要用来解决go程的同步问题以及go程之间数据共享(数据传递)的问题。

​ goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

引⽤类型 channel 可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

定义channel变量

​ 和map类似,channel也一个对应make创建的底层数据结构的引用

​ 当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。

​ 定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建:

chan 是创建channel所需使用的关键字。Type 代表指定channel收发数据的类型。

1
2
make(chan Type)  //等价于make(chan Type, 0)
make(chan Type, capacity)

​ 当参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。

​ 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() {
//定义一个channel
c := make(chan int)

go func() {
defer fmt.Println("goroutine 结束")

fmt.Println("goroutine 正在运行 ...")

c <- 666 //将666发送给c
}()

num := <-c
fmt.Println("num = ", num)
defer fmt.Println("main_goroutine 结束")
}

​ 默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。

无缓冲的channel

​ 无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何数据值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。

​ 这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

阻塞:由于某种原因数据没有到达,当前go程(线程)持续处于等待状态,直到条件满足,才解除阻塞。

同步:在两个或多个go程(线程)间,保持数据内容一致性的机制。

​ 下图展示两个 goroutine 如何利用无缓冲的通道来共享一个值:

有缓冲的channel

​ 有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同。

​ 只有通道中没有要接收的值时,接收动作才会阻塞。

​ 只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

​ 这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

​ 示例图如下:

​ 有缓冲的 channel 创建格式:

1
make(chan Type, capacity)

​ 如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。

​ 借助函数 len(ch) 求取缓冲区中剩余元素个数,cap(ch) 求取缓冲区元素容量大小。

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 main() {
c := make(chan int, 3) //有缓冲的channel
fmt.Println("len(c) = ", len(c), ", cap(c) = ", cap(c))

go func() {
defer fmt.Println("子go程结束")

for i := 0; i < 3; i++ {
c <- i
fmt.Println("子go程正在运行:发送的元素=", i, ",len(c) = ", len(c), ", cap(c) = ", cap(c))
}
}()

time.Sleep(2 * time.Second)

for i := 0; i < 3; i++ {
num := <-c
fmt.Println("num = ", num)
}

fmt.Println("main 结束")
}
1
2
3
4
5
6
7
8
9
len(c) =  0 , cap(c) =  3
子go程正在运行:发送的元素= 0 ,len(c) = 1 , cap(c) = 3
子go程正在运行:发送的元素= 1 ,len(c) = 2 , cap(c) = 3
子go程正在运行:发送的元素= 2 ,len(c) = 3 , cap(c) = 3
子go程结束
num = 0
num = 1
num = 2
main 结束

关闭channel

​ 如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭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
package main

import "fmt"

func main() {
c := make(chan int)

go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以关闭一个channel
close(c)
}()

for {
//ok如果为true表示channel没有关闭,如果为false表示channel已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}

fmt.Println("Main Finished ...")
}
1
2
3
4
5
6
0
1
2
3
4
Main Finished ...

如果子go程不关闭,会产生死锁。

注意:

  • channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
  • 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
  • 关闭channel后,可以继续从channel接收数据;
  • 对于nil channel,无论收发都会被阻塞。

range

​ 可以使用 range 来迭代不断操作channel:

1
2
3
for data := range c {
fmt.Println(data)
}

单向channel

​ 默认情况下,通道channel是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。但是,我们经常见一个通道作为参数进行传递而只希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。

​ 单向 channel 变量的声明非常简单,如下:

1
2
3
var ch1 chan int       // ch1是一个正常的channel,是双向的
var ch2 chan <- float64 // ch2是单向channel,只用于写float64数据
var ch3 <- chan int // ch3是单向channel,只用于读int数据
  • chan <- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
  • <- chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。

可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel。

select

​ Go里面提供了一个关键字select,通过select可以监听channel上的数据流动

​ 有时候我们希望能够借助channel发送或接收数据,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。

​ select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。

​ 与switch语句相比,select有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个IO操作,大致的结构如下:

1
2
3
4
5
6
7
8
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}

​ 在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

​ 如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

​ 如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:

  • 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。
  • 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。
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"

func fibonacii(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 < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()

fibonacii(c, quit)

}

Go Modules

什么是 Go Modules

​ Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,成长于 Go1.12,丰富于Go1.13,正式于 Go1.14 推荐在生产上使用。

​ Go moudles 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go moudles 了,而 Go modules 的出现也解决了在 Go1.11 前的几个常见争议问题:

  1. Go 语言长久以来的依赖管理问题。
  2. “淘汰”现有的 GOPATH 的使用模式。
  3. 统一社区中的其它的依赖管理工具(提供迁移功能)。

GOPATH 工作模式

​ Go Modoules 的目的之一就是淘汰GOPATH, 那么GOPATH是个什么?为什么在 Go1.11 前就使用 GOPATH,而 Go1.11 后就开始逐步建议使用 Go modules,不再推荐 GOPATH 的模式了呢

What is GOPATH

1
2
3
4
$ go env

GOPATH="/Users/liuwq/go"
...

​ 我们输入go env命令行后可以查看到 GOPATH 变量的结果,我们进入到该目录下进行查看,如下:

1
2
3
4
5
6
7
8
9
go
├── bin
├── pkg
└── src
├── github.com
├── golang.org
├── google.golang.org
├── gopkg.in
....

​ GOPATH目录下一共包含了三个子目录,分别是:

  • bin:存储所编译生成的二进制文件。
  • pkg:存储预编译的目标文件,以加快程序的后续编译速度。
  • src:存储所有.go文件或源代码。在编写 Go 应用程序,程序包和库时,一般会以$GOPATH/src/github.com/foo/bar的路径进行存放。

​ 因此在使用 GOPATH 模式下,我们需要将应用代码存放在固定的$GOPATH/src目录下,并且如果执行go get来拉取外部依赖会自动下载并安装到$GOPATH目录下。

GOPATH 模式的弊端

​ 在 GOPATH 的 $GOPATH/src 下进行 .go 文件或源代码的存储,我们可以称其为 GOPATH 的模式,这个模式拥有一些弊端:

  • 无版本控制概念 :在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。

  • 无法同步一致第三方版本号:在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。

  • 无法指定当前项目引用的第三方版本号:你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是github.com/foo/bar

Go Modules 模式

​ 接下来用 Go Modules 的方式创建一个项目, 建议为了与 GOPATH 分开,不要将项目创建在GOPATH/src下。

go mod 命令

命令 作用
go mod init 生成 go.mod 文件
go mod download 下载 go.mod 文件中指明的所有依赖
go mod tidy 整理现有的依赖
go mod graph 查看现有的依赖结构
go mod edit 编辑 go.mod 文件
go mod vendor 导出项目所有的依赖到vendor目录
go mod verify 校验一个模块是否被篡改过
go mod why 查看为什么需要依赖某模块

go mod 环境变量

​ 可以通过 go env 命令来进行查看:

1
2
3
4
5
6
7
8
$ go env
GO111MODULE="auto"
GOPROXY="https://proxy.golang.org,direct"
GONOPROXY=""
GOSUMDB="sum.golang.org"
GONOSUMDB=""
GOPRIVATE=""
...
GO111MODULE

​ Go语言提供了GO111MODULE这个环境变量来作为 Go modules 的开关,其允许设置以下参数:

  • auto:只要项目包含了 go.mod 文件的话启用 Go modules,目前在 Go1.11 至 Go1.14 中仍然是默认值。
  • on:启用 Go modules,推荐设置,将会是未来版本中的默认值。
  • off:禁用 Go modules,不推荐设置。
GOPROXY

​ 这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。

​ GOPROXY 的默认值是:https://proxy.golang.org,direct

proxy.golang.org国内访问不了,需要设置国内的代理。

​ GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理。

1
$ go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct

​ 而在刚刚设置的值中,我们可以发现值列表中有 “direct” 标识,它又有什么作用呢?

​ 实际上 “direct” 是一个特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),场景如下:当值列表中上一个 Go 模块代理返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,也就是回到源地址去抓取,而遇见 EOF 时终止并抛出类似 “invalid version: unknown revision…” 的错误。

GOSUMDB

​ 它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。

​ GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理(详见:Proxying a Checksum Database)。

​ 因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心。

​ 另外若对 GOSUMDB 的值有自定义需求,其支持如下格式:

  • 格式 1:<SUMDB_NAME>+<PUBLIC_KEY>
  • 格式 2:<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>

​ 也可以将其设置为“off”,也就是禁止 Go 在后续操作中校验模块版本。

GONOPROXY/GONOSUMDB/GOPRIVATE

​ 这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。

​ 更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。

​ 而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议是直接使用 GOPRIVATE。并且它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如:

1
$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"

​ 设置后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。如果不想每次都重新设置,我们也可以利用通配符,例如:

1
$ go env -w GOPRIVATE="*.example.com"

​ 这样子设置的话,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 example.com 本身

使用Go Modules初始化项目

开启Go Modules

1
$ go env -w GO111MODULE=on

初始化项目

创建项目目录

1
2
$ mkdir -p /Users/liuwq/Documents/GoProjects/modules_test
$ cd modules_test

执行 Go modules 初始化

1
2
$ go mod init github.com/liuwq/modules_test
go: creating new go.mod: module github.com/liuwq/modules_test

​ 在执行 go mod init 命令时,我们指定了模块导入路径为 github.com/aceld/modules_test。接下来我们在该项目根目录下创建 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
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"github.com/aceld/zinx/znet"
"github.com/aceld/zinx/ziface"
)

//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter
}

//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
//先读取客户端的数据
fmt.Println("recv from client : msgId=", request.GetMsgID(),
", data=", string(request.GetData()))

//再回写ping...ping...ping
err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
if err != nil {
fmt.Println(err)
}
}

func main() {
//1 创建一个server句柄
s := znet.NewServer()

//2 配置路由
s.AddRouter(0, &PingRouter{})

//3 开启服务
s.Serve()
}

​ OK,我们先不要关注代码本身,我们看当前的 main.go 也就是我们的liuwq/modules_test项目,是依赖一个叫github.com/aceld/zinx库的。 znetziface只是zinx的两个模块。

​ 接下来我们在本项目的根目录执行:

1
2
$ go get github.com/aceld/zinx/znet
$ go get github.com/aceld/zinx/ziface

​ 我们会看到 我们的go.mod被修改,同时多了一个go.sum文件。

查看go.mod文件

  • module:用于定义当前项目的模块路径
  • go:标识当前Go版本,即初始化版本
  • require:当前项目依赖的一个特定的必须版本
  • // indirect: 示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动 go get 拉取下来的,也有可能是你所依赖的模块所依赖的。我们的代码很明显是依赖的"github.com/aceld/zinx/znet""github.com/aceld/zinx/ziface",所以就间接的依赖了`github.com/aceld/zinx

查看go.sum文件

​ 在第一次拉取模块依赖后,会发现多出了一个 go.sum 文件,其详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值,以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。

截屏2023-05-24 15.46.03

我们可以看到一个模块路径可能有如下两种:

  • h1:hash情况
1
github.com/aceld/zinx v1.1.21 h1:8zoZ+hcEAd7gDsl8xOKPaWPEs9vZDRQOvhjG3vuvAnQ=
  • go.mod hash 情况
1
github.com/aceld/zinx v1.1.21/go.mod h1:nITkdASGtkLSwNKZ5yj88IpcCHTCFCP6cL12JWms1Fo=

​ h1 hash 是 Go modules 将目标模块版本的 zip 文件开包后,针对所有包内文件依次进行 hash,然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。

​ 而 h1 hash 和 go.mod hash 两者,要不就是同时存在,要不就是只存在 go.mod hash。那什么情况下会不存在 h1 hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 h1 hash,就会出现不存在 h1 hash,只存在 go.mod hash 的情况。

修改模块的版本依赖关系

1
$ go mod edit -replace=zinx@v0.0.0-20200306023939-bc416543ae24=zinx@v0.0.0-20200221135252-8a8954e75100

实战-即时通信系统

构建基础Server

main.go

1
2
3
4
5
6
package main

func main() {
server := NewServer("127.0.0.1", 8888)
server.Start()
}

server.go

server类型

1
2
3
4
type Server struct {
Ip string
Port int
}

方法

创建一个server对象
1
2
3
4
5
6
7
8
// 创建一个Server接口
func NewServer(ip string, port int) *Server {
server := &Server{
Ip: ip,
Port: port,
}
return server
}
启动Server服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 启动服务器接口
func (this *Server) Start() {
//socket listen
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
if err != nil {
fmt.Println("net.Listen err: ", err)
return
}
//close listen socket
defer listener.Close()

for {
//accept
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener accept err:", err)
continue
}
//do handler
go this.Handler(conn)
}

}
处理链接业务
1
2
3
4
func (this *Server) Handler(conn net.Conn) {
// ...当前链接的业务
fmt.Println("连接建立成功~")
}

测试

  1. 编译
1
go build -o server main.go server.go
  1. 启动服务器
1
./server
  1. 模拟
1
nc 127.0.0.1 8888
  1. 结果

用户上线功能

架构

user.go

user类型

1
2
3
4
5
6
type User struct {
Name string
Addr string
C chan string
conn net.Conn
}

方法

创建一个user对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个用户的API
func NewUser(conn net.Conn) *User {
userAddr := conn.RemoteAddr().String()
user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn,
}

//启动监听当前User channel消息的goroutine
go user.ListenMessage()

return user
}
监听user对应的channel消息
1
2
3
4
5
6
7
// 监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (this *User) ListenMessage() {
for {
msg := <-this.C
this.conn.Write([]byte(msg + "\n"))
}
}

server.go

server类型

​ 新增OnlineMap和Message属性

1
2
3
4
5
6
7
8
9
10
11
type Server struct {
Ip string
Port int

// 在线用户列表
OnlineMap map[string]*User
mapLock sync.RWMutex

// 消息广播的channel
Message chan string
}
1
2
3
4
5
6
7
8
9
10
// 创建一个Server接口
func NewServer(ip string, port int) *Server {
server := &Server{
Ip: ip,
Port: port,
OnlineMap: make(map[string]*User),
Message: make(chan string),
}
return server
}

在处理客户端上线的Handler创建并添加用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (this *Server) Handler(conn net.Conn) {
// ...当前链接的业务

user := NewUser(conn)

// 用户上线,将用户加入到OnlineMap中
this.mapLock.Lock()
this.OnlineMap[user.Name] = user
this.mapLock.Unlock()

// 广播当前用户上线消息
this.BroadCast(user, "已上线")

// 当前handler阻塞
select {}
}

新增广播消息方法

1
2
3
4
5
// 广播消息的方法
func (this *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
this.Message <- sendMsg
}

新增监听广播消息channel方法

1
2
3
4
5
6
7
8
9
10
11
12
// 监听Message广播消息channel的goroutine,一旦有消息,就发送给在线的所有User
func (this *Server) ListenMessage() {
for {
msg := <-this.Message
//将msg发送给全部的在线User
this.mapLock.Lock()
for _, cli := range this.OnlineMap {
cli.C <- msg
}
this.mapLock.Unlock()
}
}

⽤一个goroutine单独监听Message

1
2
//启动监听message的goroutine
go this.ListenMessage()

测试

  1. 编译并运行
1
2
go build -o server main.go server.go user.go
./server
  1. 启动多个客户端
1
nc 127.0.0.1 8888
  1. 结果

用户消息广播机制

server.go

完善handler处理业务方法,启动一个针对当前客户端的读goroutine。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 接受客户端发送的消息
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
this.BroadCast(user, "下线")
return
}
if err != nil && err != io.EOF {
fmt.Println("Conn Read err:", err)
return
}

//提取用户的消息(去除'\n')
msg := string(buf[:n-1])

//将得到的消息进行广播
this.BroadCast(user, msg)
}
}()

测试

启动服务后,开启多个客户端进行测试

用户业务层封装

user.go

user类型新增server关联

1
2
3
4
5
6
7
8
type User struct {
Name string
Addr string
C chan string
conn net.Conn

server *Server
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建一个用户的API
func NewUser(conn net.Conn, server *Server) *User {
userAddr := conn.RemoteAddr().String()
user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn,
server: server,
}

//启动监听当前User channel消息的goroutine
go user.ListenMessage()

return user
}

新增Online方法

1
2
3
4
5
6
7
8
9
10
// 用户的上线业务
func (this *User) Online() {
// 用户上线,将用户加入到OnlineMap中
this.server.mapLock.Lock()
this.server.OnlineMap[this.Name] = this
this.server.mapLock.Unlock()

// 广播当前用户上线消息
this.server.BroadCast(this, "已上线")
}

新增Offline方法

1
2
3
4
5
6
7
8
9
10
// 用户的下线业务
func (this *User) Offline() {
// 用户下线,将用户从OnlineMap删除
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.mapLock.Unlock()

// 广播当前用户下线消息
this.server.BroadCast(this, "下线")
}

新增DoMessage方法

1
2
3
4
5
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
//将得到的消息进行广播
this.server.BroadCast(this, msg)
}

server.go

将之前user的业务进行替换

在线用户查询

消息格式“who”

user.go

  1. 提供SendMsg向对象客户端发送消息API
1
2
3
func (this *User) SendMsg(msg string) {
this.conn.Write([]byte(msg))
}
  1. DoMessage()方法中,加上对“who”指令的处理,返回在线用户信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
if msg == "who" {
//查询当前在线用户都有哪些
this.server.mapLock.Lock()
for _, user := range this.server.OnlineMap {
onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"
this.SendMsg(onlineMsg)
}
this.server.mapLock.Unlock()
} else {
//将得到的消息进行广播
this.server.BroadCast(this, msg)
}
}

修改用户名

消息格式“rename|小新”

user.go

DoMessage()方法中,加上对“rename|小新”指令的处理,返回在线用户信息

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
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
if msg == "who" {
//查询当前在线用户都有哪些
this.server.mapLock.Lock()
for _, user := range this.server.OnlineMap {
onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"
this.SendMsg(onlineMsg)
}
this.server.mapLock.Unlock()
} else if len(msg) > 7 && msg[:7] == "rename|" {
//消息格式:rename|小新
newName := strings.Split(msg, "|")[1]
//判断name是否存在
_, ok := this.server.OnlineMap[newName]
if ok {
this.SendMsg("用户名已存在!")
} else {
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.OnlineMap[newName] = this
this.server.mapLock.Unlock()

this.Name = newName
this.SendMsg("您已经更新用户名为:" + this.Name + "\n")
}
} else {
//将得到的消息进行广播
this.server.BroadCast(this, msg)
}
}

测试

超时强踢功能

​ 用户的任意消息表示为用户活跃,长时间不发消息则认为超时,就要强制关闭用户连接。

server.go

  1. 在用户的Hander() goroutine 中,添加用户活跃channel,一旦有消息,就向该channel发送数据
  1. 在用户的Hander() goroutine 中,添加定时器功能,超时则强踢
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 当前handler阻塞
for {
select {
case <-isLive:
//当前用户是活跃的,应该重置定时器
//不做任何事情,为了激活select,更新下面的定时器
case <-time.After(time.Second * 10):
//已经超时
//将当前的User强制关闭
user.SendMsg("您已被踢~")
//销毁用的资源
close(user.C)
//关闭连接
conn.Close()
//退出当前的Handler
return
}
}

测试

私聊功能

消息格式“to|小新|你好…”

user.go

DoMessage()方法中,加上对“to|小新|你好…”指令的处理,返回在线用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
} else if len(msg) > 5 && msg[:3] == "to|" {
//消息格式:to|小新|你好...
//获取对方用户名
remoteName := strings.Split(msg, "|")[1]
if remoteName == "" {
this.SendMsg("消息格式不正确,请使用\"to|小新|你好呀\"格式。\n")
return
}
//根据用户名,得到对方的User对象
remoteUser, ok := this.server.OnlineMap[remoteName]
if !ok {
this.SendMsg("发送对象的用户名不存在\n")
return
}
//获取消息内容,通过对方的User对象将消息内容发送过去
content := strings.Split(msg, "|")[2]
if content == "" {
this.SendMsg("消息内容为空,请重新发送!\n")
return
}
remoteUser.SendMsg(this.Name + "对您说:" + content)
}

测试

客户端实现

客户端类型定义与链接

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
type Client struct {
ServerIp string
ServerPort int
Name string
conn net.Conn
}

func NewClient(serverIp string, serverPort int) *Client {
//创建客户端对象
client := &Client{
ServerIp: serverIp,
ServerPort: serverPort,
}

//连接服务器
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
if err != nil {
fmt.Println("net.Dial error:", err)
return nil
}

client.conn = conn

//返回对象
return client
}

func main() {
client := NewClient("127.0.0.1", 8888)
if client == nil {
fmt.Println(">>>>>>连接服务器失败>>>>>>")
return
}
fmt.Println(">>>>>>连接服务器成功>>>>>>")

//启动客户端的业务
select {}

}

解析命令行

init函数初始化命令行参数

1
2
3
4
5
6
7
8
var serverIp string
var serverPort int

// ./client -ip 127.0.0.1
func init() {
flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置服务器IP地址(默认是127.0.0.1)")
flag.IntVar(&serverPort, "port", 8888, "设置服务器端口(默认为8888)")
}

main函数解析命令行

1
2
3
4
//解析命令行
flag.Parse()

client := NewClient(serverIp, serverPort)

测试

菜单显示

client新增flag属性

新增menu()方法,获取用户输入的模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (client *Client) menu() bool {
var flag int

fmt.Println("1.公聊模式")
fmt.Println("2.私聊模式")
fmt.Println("3.更新用户名")
fmt.Println("0.退出")

fmt.Scanln(&flag)

if flag >= 0 && flag <= 3 {
client.flag = flag
return true
} else {
fmt.Println(">>>>>>请输入合法的范围内数字>>>>>>")
return false
}
}

新增Run()主业务循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (client *Client) Run() {
for client.flag != 0 {
for client.menu() != true {
}
//根据不同的模式处理不同的业务
switch client.flag {
case 1:
//公聊模式
fmt.Println("公聊模式选择...")
case 2:
//私聊模式
fmt.Println("私聊模式选择...")
case 3:
//更新用户名
fmt.Println("更新用户名选择...")
break
}
}
}

main()中调用Run()

1
2
//启动客户端的业务
client.Run()

更新用户名

新增UpdateName()更新用户名

1
2
3
4
5
6
7
8
9
10
11
12
func (client *Client) UpdateName() bool {
fmt.Println(">>>>>>请输入用户名")
fmt.Scanln(&client.Name)

sendMsg := "rename|" + client.Name + "\n"
_, err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn.Write err:", err)
return false
}
return true
}

加入到Run业务分支中

1
2
3
4
5
case 3:
//更新用户名
client.UpdateName()
break
}

添加处理server回执消息方法DealResponse()

1
2
3
4
5
// 处理server回应的消息,直接显示输出即可
func (client *Client) DealResponse() {
//一旦client.conn有数据,就直接copy到stdout标准输出上,永久阻塞监听
io.Copy(os.Stdout, client.conn)
}

开启一个go程,去承载DealResponse()

1
2
//单独开启一个goroutine去处理server的回执消息
go client.DealResponse()

测试

公聊模式

新增PublicChat()公聊模式业务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (client *Client) PublicChat() {
//提示用户输入消息
var chatMsg string
fmt.Println(">>>>>>请输入聊天内容,exit退出.")
fmt.Scanln(&chatMsg)

for chatMsg != "exit" {
//发送服务器
if len(chatMsg) != 0 {
sendMsg := chatMsg + "\n"
_, err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn Write err:", err)
break
}
}
chatMsg = ""
fmt.Println(">>>>>>请输入聊天内容,exit退出.")
fmt.Scanln(&chatMsg)
}
}

加入到Run的分支中

1
2
3
4
case 1:
//公聊模式
client.PublicChat()
break

测试

私聊模式

查询当前都有哪些用户在线

1
2
3
4
5
6
7
8
func (client *Client) SelectUsers() {
sendMsg := "who\n"
_, err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn Write err:", err)
return
}
}

新增私聊模式业务

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
func (client *Client) PrivateChat() {
var remoteName string
var content string

client.SelectUsers()
fmt.Println(">>>>>>请输入聊天对象[用户名],exit退出.")
fmt.Scanln(&remoteName)

for remoteName != "exit" {
fmt.Println(">>>>>>请输入消息内容,exit退出.")
fmt.Scanln(&content)

for content != "exit" {
//消息不为空则发送
if len(content) != 0 {
sendMsg := "to|" + remoteName + "|" + content + "\n\n"
_, err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn Write err:", err)
break
}
}
content = ""
fmt.Println(">>>>>>请输入消息内容,exit退出.")
fmt.Scanln(&content)
}
client.SelectUsers()
fmt.Println(">>>>>>请输入聊天对象[用户名],exit退出.")
fmt.Scanln(&remoteName)
}
}

添加到Run业务分支

1
2
3
case 2:
//私聊模式
client.PrivateChat()

测试

Golang生态拓展

《8小时转职Golang工程师-生态拓展》