前言:该文档阅读需要有一定的编程语言基础,这里将默认读者已经学习过一门或多门编程语言

补充:该文档更倾向于个人笔记,如果想学习,可以读 Go 语言圣经:Go 语言圣经中文版

# Go 语言介绍

Go 语言的三个作者:Rob Pike (罗伯。派克),Ken Thompson (肯。汤普森) 和 Robert Griesemer (罗伯特。格利茨默)

Rob Pike:曾是贝尔实验室 (Bell Labs) 的 Unix 团队,和 Plan 9 操作系统计划的成员。他与 Thompson 共事多年,并共创出广泛使用的 UTF-8 字元编码。

Ken Thompson:主要是 B 语言、C 语言的作者、Unix 之父。 1983 年图奖 (Turing Award) 和 1998 年美国国家技术奖 (National Medal ofTechnology) 得主。他与 Dennis Ritchie 是 Unix 的原创者。Thompson 也发明了 后来衍生出 C 语言的 B 程序语言。

Robert Griesemer:在开发 Go 之前是 Google V8、Chubby 和 HotSpot JVM 的主要贡献者。

Go 语言出现的目的在于平衡开发速度与运行速度,相较于 C/C++,Go 语言能够实现更快速的开发,而相较于 Java,Go 语言能实现更快速的运行。当然,这只是通常而言,并不绝对。

# Go 语言环境安装

打开 Go 语言的官网下载

# Windows(win11)

  1. 找到 windows 系统对应的包进行下载,例如这里选择种类 (Kind) 为压缩包 (Archive),系统 (OS) 为 Windows,64 位 (x86-64) 进行下载
  2. 下载好后将压缩包进行解压
  3. 文件资源管理器 ==> 右键此电脑 ==> 属性 ==> 高级系统设置 ==> 环境变量
  4. 新建变量 GO_HOME,变量值设置为刚刚解压的 go 文件
  5. 编辑 Path 变量,在 Path 变量中新建:% GO_HOME%\bin
  6. 按 win+r 键,输入 cmd,打开命令窗口,使用 go version 检查环境是否配置完成,如果出现 go 语言的版本,则表明配置成功

# Linux(Unbuntu)

  1. 使用 uname -a (其他 Linux 系统指令自行查找) 指令查看系统指令集,我这里是 x86_64

  2. 找到 Linux 系统对应的包进行下载,例如这里选择种类 (Kind) 为压缩包 (Archive),系统 (OS) 为 Linux,指令集 x86_64

  3. 打开到下载路径,然后使用指令

    1
    sudo tar -zxvf go压缩包名(自行替换) -C /usr/local

    进行解压,解压目标路径为 /usr/local

  4. 使用 vim 修改 /etc/profile 文件:

    1
    sudo vim /etc/profile

    在文件最后添加内容:

    1
    2
    export GOROOT=/usr/local/go
    export PATH=$PATH:$GOROOT/bin

    保存退出。

  5. 调用指令:

    1
    source /etc/profile
  6. 使用 go version 命令检查是否配置成功,如果出现 go 语言的版本,则标明配置成功。

# 编译运行

使用 go build FileName.go 来编译 go 文件,编译后会出现相应的可执行文件,进行执行即可,例如编写文件 Hello.go

1
2
3
4
5
6
7
package main

import "fmt"

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

然后对其进行编译运行:

1
2
3
4
#编译
go build Hello.go
#运行,Windows系统下会生成Hello.exe,直接双击执行即可
./Hello

也可以使用 go run 指令直接运行:

1
go run Hello.go

但对于代码较多的文件并不推荐使用 go run,最好还是先编译再执行。

# Go 基础语法

# 注意点

  • 注释的方式和 C 语言一致。
  • 运算符、流程控制等不再赘述,与其他语言基本保持一致。
  • 需要注意的是使用 switch 时,不再需要使用 break 来避免穿透现象,Go 语言的 switch 没有穿透现象。
  • Go 语言中没有 while 和 do while 循环,仅有 for 循环。
  • Go 语言的变量声明出来就必须调用,不调用会报错!
  • Go 语言结尾不需要添加分号来标志语句结束。
  • Go 语言流程控制语句,大括号必须跟在关键词后面,不能另起一行,例如:只有 if condition {正确
  • Go 语言的 if 语句,for 循环,switch 语句不需要使用小括号,如 switch 直接写 switch key {}

# 变量声明与赋值

声明一个变量 s1,并指定其类型为 string 类型

1
var s1 string

直接赋值,让编译器自行推导其类型

1
var s2 = "String"

短变量声明,这种声明方式仅能在函数的内部使用

1
s3 := "String"

匿名变量,用于接受一些不需要使用的值,例如接受数组索引

1
_//下划线表示匿名变量

# for range

for 循环的另一种使用方式,和 python 类似,例如:

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

import (
//导入fmt包,这个包应该是用于io流的
"fmt"
)

func main() {
//声明数组arr
arr := [...]int{1, 2, 3, 4, 5, 6, 7, 8}
//使用for range遍历arr数组,使用匿名变量接收数组索引,i接收数组的值
for _, i := range arr {
//打印
fmt.Println(i)
}
}

# 数组与切片

声明一个长度为 5 的 int 数组

1
var arr1 [5]int

声明并初始化一个 int 数组,其中… 表示让编译器自行判断数组长度

1
var arr2 = [...]int{1,2,3,4,5,6}

短变量声明法声明一个数组

1
arr3 := [...]int{1,2,3,4,5,6}

多维数组不再赘述
需要注意的是,Go 语言中的数组是值类型而非引用类型,换言之,传递时进行的是值传递而非指针传递


声明一个 int 切片,切片类似于 C 语言的数组,属于引用类型,传递时是指针传递而非值传递

1
var slice1 []int

使用 make 函数初始化一个切片,其变量类型 int 可变,len 为切片长度,cap 为切片容量,cap 可省略

1
var slice2 []int = make([]int, len, cap)

短变量声明法,声明一个切片

1
slice3 := make([]int, len)

从数组中直接切出一个切片,区间为 [startIndex, endIndex),左闭右开。

1
2
3
s := arr[startIndex:endIndex]
//省略startIndex表示从0开始切,省略endIndex表示切到结尾,两者可以都省略。
//从arr中切出的切片s本质是使用指针指向arr的位置,所以修改切片s时可以修改到数组arr。

# 函数

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

import "fmt"

/*
func 函数名(参数列表) (返回值){
函数体
}
*/
//案例1,空参数,空返回:
func sayHello() {
fmt.Println("Hello World!")
}

// 案例2,固定参数:
func cout(str string) (ret bool) {
fmt.Println(str)
ret = true
return
}

// 案例3,可变参数:
func intSum(num ...int) int {//使用...来表示可变参数
//可变参数可以与固定参数同时出现,但必须置后,例如:a int, b ...int
ret := 0
for _, arg := range num {
ret = ret + arg
}
return ret
}

//案例4,多个返回值:
func calc(a, b int)(sum, sub int){
//当a,b的类型相同时,可以进行简写,sum和sub同理
sum = a + b
sub = a - b
return
}

func main() {
sayHello()
ok := cout("Hello World!")
fmt.Println(ok)
//可变参数
fmt.Println(intSum(1, 2))
fmt.Println(intSum(1, 2, 3, 4, 5))
//忽略返回的sum,仅接受sub并打印
_, sub := calc(20, 10)
fmt.Println(sub)
}

Go 语言的函数中并不存在默认参数。

# 函数指针与回调函数

与 C 语言的函数指针和回调函数类似,写个案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"
func add(x, y int) int{
return x + y
}

func calc(x, y int, op func(int, int) int) int{
//这是一个回调函数,参数op是一个函数,该函数的参数列表为两个int类型,返回值是int类型
return op(x, y)
}

func main(){
v := add//函数指针
v(1, 2)//使用函数指针调用函数
fmt.Println(calc(100, 200, add))//调用回调函数
}

# defer: 延迟执行

defer 语句会在函数将要结束时才执行,具有延迟调用的特性 (类似于析构)。

其采用栈结构,例如:

1
2
3
4
5
6
7
8
9
package main
import "fmt"
func main(){
fmt.Println("Start...")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("End...")
}

最后输出结果为:

1
2
3
4
5
Start...
End...
3
2
1

defer 通常用于处理资源释放问题,例如资源清理,文件关闭,解锁及记录时间等。

# 匿名函数与闭包

匿名函数,懂的都懂。与函数唯一的区别在于不写名字,例如:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
func() {
fmt.Println("Hello World!")
}() //结尾使用()调用匿名函数
}

闭包 = 函数 + 外层变量调用,举个例子:

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

import "fmt"

func a(name string) func() {
//定义一个函数a,其需要一个类型为string的name参数,返回值是一个函数
return func() {
fmt.Println("Hello ", name)
}
}

func main() {
r := a("张三")
r()
}

可以看到,在这个示例中,函数 a 中的匿名函数调用到了它外层的函数 a 的变量,这样的使用方式叫做闭包。

再来看看示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"strings"
)

func CheckSuffix(suffix string) func(file string) string {
//定义一个函数CheckSuffix用于检测文件名后缀,该函数需要一个suffix(后缀)的string参数
//其返回值为一个函数,返回值函数需要一个file(文件)的string参数,该函数返回值为string类型
return func(file string) string {
//使用strings提供的HasSuffix检测file的后缀是否是suffix,如果是,则返回true,不是则返回false
if !strings.HasSuffix(file, suffix) {
//如果后缀不对,则将其添加后缀再返回
return file + suffix
}
//如果后缀正确,则直接返回file即可
return file
}
}

func main() {
//测试调用
r := CheckSuffix(".txt")
ret := r("张三")
fmt.Println(ret)
}

同样的,对于函数 CheckSuffix 中的匿名函数,其也调用了外层的变量,那么这就是一个闭包的使用。

需要注意,内部的匿名函数是可以修改到外层变量的,如果我要这样使用:

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

func CheckSuffix(suffix string) func(file, suf string) string {
//定义一个函数CheckSuffix用于检测文件名后缀,该函数需要一个suffix(后缀)的string参数
//其返回值为一个函数,返回值函数需要一个file(文件)的string参数,该函数返回值为string类型
return func(file, suf string) string {
//改变代码,变量suf,如果suf是以.为开头的string,那么将suf的值赋给suffix
if strings.HasPrefix(suf, ".") {
suffix = suf
}
//使用strings提供的HasSuffix检测file的后缀是否是suffix,如果是,则返回true,不是则返回false
if !strings.HasSuffix(file, suffix) {
//如果后缀不对,则将其添加后缀再返回
return file + suffix
}
//如果后缀正确,则直接返回file即可
return file
}
}

func main() {
//测试调用
r := CheckSuffix(".txt")
ret := r("张三", ".docx")
ret2 := r("李四", "")
fmt.Println(ret, ret2)
}

可以发现,输出结果为

张三.docx 李四.docx

也就是说,对于 suffix 的修改是持续生效的,并非只作用于匿名函数内部

如果用 lambda 表达式来说明,那么它应该相当于 [&](我个人推测)

# panic 与 recover

Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try…catch…finally 这种异常,因为 Go 语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱。因为开发者很容易滥用异常,甚至一个小小的错误都抛出一个异常。在 Go 语言中,使用多值返回来返回错误。不要用异常代替错误,更不要用来控制流程。

panic 与 recover 是 Go 的两个内置函数,这两个内置函数用于处理 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
27
28
29
package main

import "fmt"

func a() {
fmt.Println("Hello in a")
}

func b() {
//defer recover需要写在pannic的前面,否则会报错
defer func() {
err := recover()//使用recover捕获错误
if err != nil {
fmt.Println(err)
}
}()
panic("panic in b")
}

func c() {
fmt.Println("Hello in c")
}

func main() {
//测试调用
a()
b()
c()
}

# 指针与 C 语言基本无异,不再赘述

# type

作用差不多相当于 typedef,使用如下

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

// 给int取一个别名叫Int
type Int = int

func main() {
var a Int
a = 10

var b MyInt
b = 20
fmt.Println(a, b)
}

# 结构体

# 定义结构体

使用方法和 C 语言类似,使用 type 和 struct 关键字来定义一个结构体,如下:

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

import "fmt"

// 定义人类结构体
type people struct {
name string
age int8
city string
//这里也可以写成 name, city string 类似于函数的参数类型简写
}

func main() {
//实例化
var p people
p.name = "张三"
p.age = 18
p.city = "北京"
fmt.Printf("p=%v\n", p)

//匿名结构体:
var user struct {
name string
married bool
}
user.name = "李四"
user.married = false

//结构体指针
var p2 = new(people)
//正常的结构体指针成员调用方式:
(*p2).name = "王五"
//对于结构体指针,Go语言可以直接使用星号调用其成员,类似于->
p2.age = 18
p2.city = "上海"
fmt.Printf("%v\n", p2)

//取结构体的地址进行实例化:
p3 := &people{}
fmt.Printf("%v\n", p3)
}

# 结构体初始化

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 people struct {
name, city string
age int8
}

func main() {
//1.键值对初始化:
p := people{
name: "张三",
age: 18, //需要注意,每一个值后面都需要加逗号,最后一行也是,否则会报错,city会被初始化为空串
}
fmt.Printf("%#v\n", p)

//2.值的列表进行初始化
p2 := &people{ //列表初始化时,需要值和结构体成员一一对应
"李四",
"北京",
18,
}
fmt.Printf("%#v\n", p2)
}

# 构造函数

Go 语言的结构体没有构造函数,但是可以自己实现。例如,实现 people 的构造函数:

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"

// 构造函数
type people struct {
name, city string
age int8
}

func newPeople(name, city string, age int8) *people {
//结构体是值类型,所以返回指针更节省空间
return &people{
name: name,
city: city,
age: age,
}
}

func main() {
p1 := newPeople("张三哥", "北京", int8(18))
fmt.Printf("type:%T value:%#v\n", p1, p1)
}

# 匿名结构体与结构体嵌套

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

import "fmt"

type Person struct {
//Go语言结构体中的字段可以匿名
string
int8
}

func main() {
p1 := Person{
"小王子",
18,
}
fmt.Println(p1)
//访问时通过其类型进行访问,但需要注意,使用匿名字段时需要避免类型重复
fmt.Println(p1.string, p1.int8)
}

使用匿名结构体,对结构体进行嵌套

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"

// Address struct
type Address struct {
province, city string
}

// Person stuct
type Person struct {
name, gender string
age int8
//嵌套另外一个结构体
//address Address
//匿名嵌套一个结构体
Address
}

func main() {
p1 := Person{
name: "张三",
gender: "男",
age: 18,
Address: Address{
province: "河北",
city: "石家庄",
},
}

fmt.Println(p1)
//使用匿名结构体嵌套,调用匿名结构体中的字段时,会首先在结构体中检索
//若未检索到该字段,则会去匿名结构体中进行查找
fmt.Println(p1.name, p1.province)
}

# 方法和接收者

Go 语言中方法 (Method) 是一种作用域特定类型变量的函数

这种特定类型变量叫做接收者 (Receiver)

接收者的概念就类似于其他语言中的 this 或者 self

方法的定义格式如下:

1
2
3
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
38
39
40
41
package main

import "fmt"

// 一个结构体,以大写字母开头,那么它通常是对外部可见的,最好补充注释
// Person is a struct
type People struct {
name string
age int8
}

// 构造函数
// NewPeople is a constructor of type People
func NewPeople(name string, age int8) *People {
return &People{
name: name,
age: age,
}
}

// 定义方法
// Dream is a method of type People
func (p People) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

// SetAge is used to modify the age of People types
func (p *People) SetAge(newAge int8) {
p.age = newAge
}

func main() {
p1 := NewPeople("张三", int8(18))
//正常写法:(*p1).Dream()
//也可以写为:
p1.Dream()

fmt.Println(p1.age)
p1.SetAge(int8(24))
fmt.Println(p1.age)
}

Go 语言中,接收者类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

但是只能给自己的包中的类型定义方法。

可以通过 type 关键字,基于 int 定义一个新的 MyInt 类型,然后为其定义方法。

# 结构体继承

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

import "fmt"

// 使用结构体实现类似继承的效果
// 基类Animal
type Animal struct {
name string
}

// Animal的方法move
func (a *Animal) move() {
fmt.Printf("%s会动\n", a.name)
}

// 派生类Dog
type Dog struct {
Feet int8
//继承基类Animal
*Animal
}

// Dog的方法bark
func (d *Dog) bark() {
fmt.Printf("%s会汪汪汪\n", d.name)
}

func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{
name: "旺财",
},
}
d1.bark()
//通过子类调用其父类的方法
d1.move()
}

# 结构体字段的可见性

Go 语言的结构体字段,如果开头字母是大写的,那么就是公开的,可以被外部访问的 (类似于 public)

如果开头字母是小写的,那么就是私有的,只能被定义该结构体的包中使用 (类似于 protected)

# 结构体与 JSON 序列化

JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。

JSON 键值对是用来保存 JS 对象的一种方式:

  1. 键 / 值对组合中的键名写在前面,并用双引号包裹
  2. 使用冒号分隔
  3. 然后紧接着值
  4. 多个键值之间用逗号分隔
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
package main

import (
"encoding/json"
"fmt"
)

type student struct {
Id int
Name string
}

func newStudent(id int, name string) student {
return student{
Id: id,
Name: name,
}
}

type class struct {
Title string
Student []student
}

func main() {
//创建一个班级变量c1
c1 := class{
Title: "火箭班",
Student: make([]student, 0, 20),
}
//往班级c1中添加学生
for i := 0; i < 10; i++ {
tmpStu := newStudent(i, fmt.Sprintf("stu%02d", i))
c1.Student = append(c1.Student, tmpStu)
}
//fmt.Printf("%#v\n", c1)

//JSON序列化:Go语言中的数据->JSON格式的字符串
//使用json包中提供的marshal方法进行json序列化
//marshal返回两个值,第一个是data,第二个是err信息
//marshal需要一个参数,即需要序列化的数据
data, err := json.Marshal(c1)
//对返回的err进行判断,如果err不为空,那么打印err信息,并退出程序
if err != nil {
fmt.Println("json marshal failed, err:", err)
return
}
//打印序列化后的数据进行确认
fmt.Printf("%T\n", data)
//fmt.Printf("%s\n", data)

//JSON反序列化:JSON格式的字符串->Go语言中的数据
jsonStr := `{"Title":"火箭班","Student":[{"Id":0,"Name":"stu00"},{"Id":1,"Name":"stu01"},{"Id":2,"Name":"stu02"}]}`
//使用c2对转换后的数据进行接受
var c2 class
//使用json包中的Unmarshal方法,对其进行反序列化
//所需参数为原始json字符串的字节流,与存储容器
//将jsonStr强转为字节流,取c2地址进行指针传递以使其能够存储
//返回一个值为err信息,需要接收
err = json.Unmarshal([]byte(jsonStr), &c2)
if err != nil {
//对err进行判断
fmt.Println("json unmarshal failed, err:", err)
return
}
fmt.Printf("%#v\n", c2)
}

需要注意的是,如果结构体中的字段首字母变为小写,那么其将对外不可见。

则使用 json 包中的方法时,json 的方法并不能够调用其字段,会导致错误。

# 结构体标签 (Tag)

Tag 用于解决其他语言字段首字母小写与 Go 语言字段首字母大写不兼容的问题

Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag 在结构体字段的后方定义,由一对反引号 (ESC 下面那个键) 包裹起来,具体格式如下:

1
2
//每对键值对中间用空格分隔
`key1:"value1" key2:"value2"`

注意:为结构体编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正常取值。

例如:不要在 key 和 value 之间添加空格

具体使用如下:

1
2
3
4
5
type class struct {
Title string `json:"title"`
Student []student
}
//这样就可以使class在json序列化时,字段名被转换为title

# 小练习:

实现学员信息管理系统,包含以下功能:

  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
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
//main.go
package main

import (
"fmt"
"os"
)

//学员信息管理系统

//1.添加学员信息
//2.编辑学员信息
//3.展示所有学员信息

func showMenu() {
fmt.Println("欢迎来到学员信息管理系统")
fmt.Println("1.添加学员")
fmt.Println("2.编辑学员信息")
fmt.Println("3.展示所有学员信息")
fmt.Println("4.退出系统")
}

//get user input infomation of student
func getNewStu() *student {
var id, name, class string
fmt.Println("请按要求输入学员信息")
fmt.Print("请输入学员的学号:")
fmt.Scanln(&id)

fmt.Print("请输入学员的姓名:")
fmt.Scanln(&name)

fmt.Print("请输入学员的班级:")
fmt.Scanln(&class)

return newStudent(id, name, class)
}

func main() {
sms := newStuManSys()

for {
//1.打印系统菜单
showMenu()
fmt.Println("请输入你要操作的序号")

//2.等待用户选择执行的选项
var choice int
fmt.Scanf("%d\n", &choice)

//3.执行用户选择的选项
switch choice {
case 1:
sms.addStu(getNewStu())
case 2:
sms.editStu(getNewStu())
case 3:
sms.showStu()
case 4:
//退出
os.Exit(0)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//student.go
package main

import "fmt"

//student struct
type student struct {
id, class, name string
}

//constructor of student
func newStudent(id, name, class string) *student {
return &student{
id: id,
name: name,
class: class,
}
}

//studentManageSystem struct
type stuManSys struct {
allStudents []*student
}

//constructor of stuManSys
func newStuManSys() *stuManSys {
return &stuManSys{
allStudents: make([]*student, 0, 100),
}
}

//Method of adding stu belonging to stuManSys
func (s *stuManSys) addStu(newStu *student) {
s.allStudents = append(s.allStudents, newStu)
}

//Method of editing stu belonging to stuManSys
func (s *stuManSys) editStu(newStu *student) {
for i, v := range s.allStudents {
if newStu.id == v.id {
s.allStudents[i] = newStu
return
}
}
fmt.Println("输入学号有误")
}

//Method of showing stu belonging to stuManSys
func (s *stuManSys) showStu() {
for _, i := range s.allStudents {
fmt.Printf("学号:%s\t姓名:%s\t班级:%s\n", i.id, i.name, i.class)
}
}

# Go 语言 - 包 (package)

# 介绍

包 (package) 是多个 Go 源码的集合,是一种高级的代码复用方案,Go 语言提供了很多内置包,如 fmt、os、io 等

# 定义包

可以根据自己的需要创建自己的包。

一个包可以简单的理解为一个存放.go 文件的文件夹。

该文件夹下面的所有 go 文件都要在代码的第一行添加如下代码,声明该文件归属的包。

1
package 包名

注意事项:

  • 一个文件夹下面只能有一个包,同样一个包的文件不能在多个文件夹下
  • 包名可以和文件夹名不同,包名不能包含 - 符号
  • 包名为 main 的包为应用程序的入口包,编译不包含 main 包的源代码时不会得到可执行文件

# 可见性

如果想在一个包中引用另外一个包里的标识符 (如变量、常量、类型、函数等) 时,该标识符必须是对外可见的 (public)

在 Go 语言中只需要将标识符的首字母大写,就可以让标识符对外可见了

示例:

文件路径:/GoLearn/package_demo/calc/add.go

1
2
3
4
5
6
package calc

// add funciton
func Add(x, y int) int {
return x + y
}

文件路径:/GoLearn/package_demo/main/main.go

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

import (
"GoLearn/package_demo/calc"
"fmt"
)

func main() {
fmt.Println(calc.Add(10, 20))
}

也可以给导入的包起别名:

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

//可以给导入的包起别名,来规避包名相同的冲突
import (
Another_name "GoLearn/package_demo/calc"
"fmt"
)

func main() {
fmt.Println(Another_name.Add(10, 20))
}

# init 函数与匿名包

init 函数是一种特殊的函数,它没有参数也没有返回值

在包被导入的时候会自动调用

示例:

1
2
3
func init() {
fmt.Println("Hello!")
}

当仅需要执行包的 init 函数而不需要其内部的数据时,可以使用匿名包的形式,格式如下:

1
import _ "包的路径"

导入包与 init 函数的调用符合栈结构

即先导入者后 init,而 main 包是最先被导入的,所以它的 init 函数会被最后调用

# Go 语言 - 接口 (interface)

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

import "fmt"

//interface

type usbFlashDisk struct{}

func (u usbFlashDisk) usbLink() {
fmt.Println("U盘已连接")
}

type phone struct{}

func (p phone) usbLink() {
fmt.Println("手机已连接")
}

// 定义一个抽象的类型,只要实现了usbLink()这个方法的类型都可以称为usb类型
type usb interface {
usbLink()//有参数和返回值的方法,在这里也写上参数和返回值
}

// 接口不管你是什么类型,只管你要实现什么方法
func link(arg usb) {
arg.usbLink()
}

func main() {
u1 := usbFlashDisk{}
link(u1)
p1 := phone{}
link(p1)
}

当一个接口不要求实现任何方法时,该接口是一个空接口

任意结构都满足空接口

# 值接收者与指针接收者关于接口的差异

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

import "fmt"

type mover interface {
move()
}

type person struct {
name string
age int8
}

// 使用值接收者实现接口:类型的值和类型的指针都能保存到接口变量中
//func (p person) move() {
// fmt.Printf("%s在跑\n", p.name)
//}

// 使用指针接收者实现接口:只有类型指针能够保存到接口变量中
func (p *person) move() {
s := "在跑"
fmt.Println(p.name, s)
}

func main() {
var m mover
p1 := &person{
name: "张三",
age: 18,
}
m = p1
m.move()
fmt.Println(m)
}

# 接口内部存储

接口内部存储分为两部分:

  • 一部分保存其动态类型,用于记录存储变量的类型。
  • 另一部分保存其动态值,用于记录存储变量的值。

类型断言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//可以给一个接口存储变量的类型进行断言:
package main
//定义空接口
type xxx interface{}

func main(){
var x xxx
//任意类型都满足空接口,所以可以传入bool类型
x = false

//使用x.(type)来断言x中保存的变量类型
//会返回两个值,ret是x的变量值,ok是一个布尔类型,true时表示断言正确,false时表示断言错误
//ok=false时,ret=string的零值
ret, ok := x.(string)
if !ok {
fmt.Println("不是string类型")
}else {
fmt.Println("是字符串类型", ret)
}
}

使用 switch 进行类型断言:

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"

// 定义空接口
type xxx interface{}

func main() {
var x xxx
x = false

switch v := x.(type) {
case string:
fmt.Println("是字符串类型,value:", v)
case bool:
fmt.Println("是布尔类型,value:", v)
case int:
fmt.Println("是int类型,value:", v)
default:
fmt.Println("猜不到了,value:", v)
}
}

# Go 语言标准库

# time 包

time 包提供的部分方法:

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
now := time.Now() //获取当前时间,返回值是一个对象
now.Unix() //获取时间戳
now.UnixNano() //纳秒时间戳
time.Unix() //时间戳转换为时间

//可以由结构Time的对象调用(比如now),增加d(Duration是一种时间间隔的枚举类型,规定了时、分、秒等时间间隔),返回一个Time类型
func (t Time)Add(d Duration) Time
//可以由结构Time的对象调用(比如now),减去另一个Time对象,求出其时间间隔(Duration类型),并返回
func (t Time)Sub(u Time) Duration
//判断两个时间是否相等
func (t Time)Equal(u Time) bool
//判断t是否在u之前,是返回真,不是返回假
func (t Time)Before(u Time) bool
//判断t是否在u之后,是返回真,不是返回假
func (t Time)After(u Time) bool

//定时器
//使用time.Tick(d Duration)可以设置定时器,定时器本质上是一个通道(channel)
tick := time.Tick(time.Second)

//格式化
//Go语言的格式化需要按照Y m d H M S为2006 01 02 15 04 05的顺序填入,不能填错
//maybe这个时间是go语言被创造出来的时间,可以记为2006一二三四五,但注意3是下午的15点。
//中间使用的分隔符号可以自行选择,可以用点或者-或者:,如下:
ret1 := now.Format("2006-01-02 15:04:05")

//解析字符串类型的时间
timeStr := "2023/08/12 09:29:00"
//载入时区,使用LoadLocation函数
//其中loc为时区,err为错误信息,需要进行判断
loc, err := time.LoadLocation("Asia/Shanghai")//这里以上海为例
//根据时区解析字符串格式的时间
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", timeStr, loc)
//也可以使用Parse方法直接转换,会转换为UTC时间
timeObj, err := time.Parse("2006/01/02 15:04:05", timeStr)

具体使用可以看 Go 语言 - 时间对象

# os 包

os 包提供的部分方法:

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
//以只读的形式打开路径为path的文件,返回打开的文件和错误信息,错误信息需要判断
func Open(path string)(*File, error)
//以flag的模式,打开路径为path的文件,其权限为perm(Linux中的权限)
func OpenFile(path string, flag int, perm FileMode)(*File, error)

//文件指针的方法,读取文件指针的内容,并以字节流的形式存储到b中
//返回值包括读取的字节数n,和错误信息err
//当读取到文件末尾时,错误信息err==io.EOF
func(f *File)Read(b []byte)(n int, err error)

//bufio会先将读取或写入内容写入到缓存区,然后再执行操作,NewReader返回一个对象
reader := bufio.NewReader(f *File)
//调用reader对象的方法ReadString,后面的('\n')表示读取内容以换行符结尾,注意使用字符而非字符串
//返回值line为读取到的内容,是string类型,err为错误信息
line, err := reader.ReadString('\n')

//读取文件的所有内容,返回值为文件内容(字节形式),和错误信息
content, err := ioutil.ReadFile(path)//该函数于Go 1.16已被废弃,相关内容转移至io包或os包

//写入字节内容b,返回写入字节数n,和错误信息err
func (f *File)Write(b []byte)(n int, err error)
func (f *File)WriteString(s string)(n int, err error)

//使用bufio写入
func NewWriter(w io.writer) *Writer
//将s写入到缓冲区
func (w *Writer)WriteString(s string)(n int, err error)
//将缓冲区内容写入到磁盘
func (w *Writer)Flush() error

打开模式 flag 包括:

模式 含义
os.O_WRONLY 只写
os.O_CREATE 创建文件
os.O_RDONLY 只读
os.O_PDWR 读写
os.O_TRUNC 重写
os.O_APPEND 追加

perm:按照 linux 权限规定:r=4,w=2,x=1

具体使用可以看 Go 语言 - 文件读写

# sync 包

1
2
3
4
5
sync.WaitGroup	//goroutine等待队列
sync.Mutex //互斥锁,详情可见并发编程->并发同步和锁->互斥锁
sync.RWMutex //读写互斥锁,详情可见并发编程->并发同步和锁->读写互斥锁
sync.Once //保证某段代码在并发情况下必定,且仅执行一次
sync.Map //Go语言中内置的Map并不是并发安全的Map,这里提供一个并发安全的Map

# net 包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Listen("protocol", "Port") (Listener, error)//参数输入样例: "tcp", "localhost:8080"
func (l Listener) Accept() (Conn, error)//开启连接
func (c Conn) Close()//关闭连接
func (c Conn) Write(b []byte) (n int, err error)//以字节流的形式发送数据
func Dial("protocol", "address") (Conn, error)//主动向address发起连接请求
func (c Conn) Read(b []byte) (n int, err error)//接收对方发送的以字节流形式的数据

//参考输入样例: "udp", &UDPAddr{IP:net.IPv4(127,0,0,1),Port:8080,}
func ListenUDP("protocol", *UDPAddr) (*UDPConn, error)
//读取传来的数据到b中,返回读取字节数n,发送地址addr和错误信息
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
//发送字节流b到addr
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
//主动向addr发送数据
func DialUDP(network string, laddr *UDPAddr, raddr *UDPAddr) (*UDPConn, error)
func (u *UDPConn) Write(b []byte) (n int, err error)//以字节流的形式发送数据
func (u *UDPConn) Close()//关闭连接
func (u *UDPConn) ReadFromUDP(b []byte) (n int, err error)//接收对方发送的以字节流形式的数据

# 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"fmt"
"time"
)

func main() {
//获取当前时间
now := time.Now() //时间对象
fmt.Println(now)

//从该时间对象中获取具体的年、月、日、时、分、秒
year := now.Year()
month := now.Month()
day := now.Day()
hour := now.Hour()
minute := now.Minute()
second := now.Second()
fmt.Println(year, month, day, hour, minute, second)

//获取时间戳:从1970.1.1到现在的秒数
timeStamp1 := now.Unix() //该时间戳单位时秒
timeStamp2 := now.UnixNano() //该时间戳单位是纳秒
fmt.Println(timeStamp1, timeStamp2)

//将时间戳转换为具体的时间
//第一个参数sec是秒为单位,第二个参数nsec是纳秒为单位
t := time.Unix(timeStamp1, 0)
fmt.Println(t)

//时间间隔Duration
//Duration是time包中定义的一种类型,其中枚举了各种时间单位,比如Hour,Second等
sleepTime := 5
//使用time.Duration将int64转换为Duration类型,使得time.Sleep能够识别
//time.Sleep作用是使程序睡眠
time.Sleep(time.Duration(sleepTime) * time.Second)
go
//时间差
//使用now中的Add、Sub方法增加时间或减少时间
t2 := now.Add(time.Hour)
fmt.Println(t2)
fmt.Println(t2.Sub(now))

//使用定时器:
ticker := time.Tick(time.Second)//定义一个1秒间隔的定时器
for i := range ticker {
fmt.Println(i)//每秒都会执行的任务
}
}

# Go 语言 - 文件读写

# os 读取具体使用

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 (
"bufio"
"fmt"
"io"
"os"
)

func ReadAll(path string) {
file, err := os.Open(path)
if err != nil {
fmt.Println("open file failed, err:", err)
}
//使用defer关闭文件
defer file.Close()

for {
var temp = make([]byte, 128)
n, err := file.Read(temp)
if err == io.EOF {
fmt.Println(string(temp[:n]))
return
}
if err != nil {
fmt.Println("read from file failed, err:", err)
}
//fmt.Println("read", n, "bytes from file.")
fmt.Printf(string(temp[:n]))
}
}

func ReadByBufio(path string) {
file, err := os.Open(path)
if err != nil {
fmt.Println("open file failed, err:", err)
}
defer file.Close()

reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err == io.EOF {
fmt.Println(line)
fmt.Println("file read over")
break
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Print(line)
}
}

func main() {
ReadAll("./example.txt")
ReadByBufio("./example.txt")
}

# os 写入具体使用

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 (
"bufio"
"fmt"
"os"
)

func write(path string) {
file, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND, 0644)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()

str := "Hello World!"
_, err = file.Write([]byte(str))
if err != nil {
fmt.Println("Writer err:", err)
}
_, err = file.WriteString(str)
if err != nil {
fmt.Println("WriteString err:", err)
}
}

func writeByBufio(path string) {
file, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND, 0644)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
writer := bufio.NewWriter(file)

_, err = writer.WriteString("Hello World!") //将内容写入缓冲区
if err != nil {
fmt.Println("WriteString err:", err)
}
_ = writer.Flush() //将缓冲区内容写入磁盘
}

func main() {
write("./example.txt")
writeByBufio("./example.txt")
}

# Go 语言 - 反射

# Go 语言 - 并发编程

# 基本概念

串行:像串一样,顺序执行。比如先读小学,小学结束读初中,初中结束读高中

并发:同一时间段内执行多个任务。比如上午我要学习和刷视频,可能是学一会儿习,然后刷一会儿视频

并行:同一时刻执行多个任务。比如我在看小说的同时听音乐

进程:程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位

线程:操作系统基于进程开发的轻量级进程,是操作系统调度执行的最小单位

协程:非操作系统提供而是由用户自行创建和控制的用户态‘线程’,比线程更轻量级

并发模型:

业界将如何实现并发编程总结归纳为各式各样的并发模型,常见的有以下几种:

  1. 线程 & 锁模型
  2. Actor 模型
  3. CSP 模型
  4. Fork&Join 模型

Go 语言中的并发程序主要是通过基于 CSP (communicating sequential processes) 的 goroutine 和 channel 来实现,当然也支持使用传统的多线程共享内存的并发模式

# goroutine 使用

使用关键字 go 来开启一个 goroutine,例如:

1
go Hello()//开启一个goroutine来执行Hello函数

具体使用:

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

// 当主goroutine结束时,由它开启的其他goroutine也会立即结束(不一定执行完了)
// 使用sync包中提供的WaitGroup来记录开启的线程是否执行完成,决定是否等待
var wg sync.WaitGroup

func hello() {
fmt.Println("Hello World!")
//使用wg.Done()通知WaitGroup该线程已执行完毕,将计数牌-1
wg.Done()
}

func main() { //开启一个主goroutine去执行main函数
//在wg中加入一个计数牌
wg.Add(1)

go hello() //开启了一个goroutine去执行hello这个函数
fmt.Println("Hello Main!")

//阻塞,等待所有goroutine都执行完后再结束程序
wg.Wait()
}

# GOMAXPROCS

通过 runtime 包中的 GOMAXPROCS 可以指定占用的 CPU 数量,Go1.5 版本之后默认使用所有逻辑核心。示例:

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

import (
"fmt"
"runtime"
"sync"
)

var wg sync.WaitGroup

func a() {
for i := 0; i < 10; i++ {
fmt.Println("a:", i)
}
wg.Done()
}

func b() {
for i := 0; i < 10; i++ {
fmt.Println("b:", i)
}
wg.Done()
}

func main() {
runtime.GOMAXPROCS(4) //占用4个cpu
wg.Add(2)
go a()
go b()
for i := 0; i < 10; i++ {
fmt.Println("main:", i)
}
wg.Wait()
}

# channel

channel 是一种类型,一种引用类型。声明方式如下:

1
2
3
4
5
6
7
8
9
var 变量 chan 元素类型

//例如:
var ch1 chan int //声明一个传递整型的通道
var ch2 chan bool //声明一个传递布尔型的通道
var ch3 chan []int //声明一个传递int切片的通道

//channel声明后需要使用make函数进行初始化
ch1 = make(chan int, [缓冲大小])//缓冲大小是可选的,可以不填

发送:

1
ch <- 10//把10发送到通道ch中

接受:

1
2
x := <-ch 	//从ch中接受值并赋值给变量x
<-ch //从ch中接受值,忽略结果

关闭:

1
close(ch)//关闭通道ch

需要注意的是,在 go 语言中,channel 的关闭并不一定是必须的。通道是可以被垃圾回收机制回收的。

只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭通道。

关闭后的通道有以下特点:

  1. 对一个已经关闭的通道发送值会导致 panic
  2. 对一个已经关闭的通道进行接收会一直获取值,直到通道为空
  3. 对一个已经关闭的并且没有值的通道执行接收操作会得到对应类型的零值
  4. 关闭一个已经关闭的通道会导致 panic

# 缓冲

无缓冲通道又被称为阻塞的通道,创建如下通道:

1
2
3
4
5
func main(){
ch := make(chan int)
ch <- 10
fmt.Println("send success")
}

执行上面这段代码会导致程序死锁。

这是因为这段代码创建了一个无缓冲区的通道,则该通道无法缓存发送过来的值 10,所以它会一直等待有一个 goroutine 来取走这个值,从而阻塞程序。

而对于有缓冲区的通道:

1
2
3
4
5
func main(){
ch := make(chan int, 1)
ch <- 10
fmt.Println("send success")
}

make 函数里的 1 表示有一个位置的缓冲区,有缓冲区的通道会将发送过来的值暂存值缓冲区,有 goroutine 来取值时,则从缓冲区发送给它。

当缓冲区被存满后,则会变成无缓冲区的阻塞情况。

# 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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import "fmt"

//将1-100的值存入到ch1中
//将ch1中的值取出,求平方,然后存入ch2中

// 这里的chan<-表示ch是一个单向通道,它只能够向通道发送值
func f1(ch chan<- int) {
for i := 0; i < 100; i++ {
ch <- i
}
close(ch)
}

// 这里的<-chan表示ch1是一个单向通道,它只能够从通道中取出值
// chan<-表示ch2是一个单向通道,它只能够向通道发送值
func f2(ch1 <-chan int, ch2 chan<- int) {
//从通道中取值的方法1
for {
temp, ok := <-ch1
if !ok {
break
}
ch2 <- temp * temp
}
close(ch2)
}

func main() {
ch1 := make(chan int, 100)
ch2 := make(chan int, 200)

go f1(ch1)
go f2(ch1, ch2)

//从通道中取值的方法2
for ret := range ch2 {
fmt.Println(ret)
}
}

# worker pool (giriytube 池)

工作中通常会使用 workerpool 模式,控制 goroutine 的数量,防止 goroutine 泄露和暴涨,简单示例:

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

import (
"fmt"
"time"
)

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

func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)

//开启3个goroutine
for j := 0; j < 3; j++ {
go worker(j, jobs, results)
}

//发送5个任务
for i := 0; i < 5; i++ {
jobs <- i
}

close(jobs)

for i := 0; i < 5; i++ {
ret := <-results
fmt.Println(ret)
}
}

# select

select 语句的使用类似于 switch,当匹配到其可以执行的操作时,则进行该操作。

需要注意的是,当有多个 case 都满足时,select 并不会顺序执行,而是从中随机抽一个进行执行,使用示例如下:

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

import (
"fmt"
)

func main() {
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
default:
fmt.Println("default")
}
}
}

# 并发同步和锁

有时候在 Go 代码中可能会存在多个 goroutine 同时操作一个资源 (临界区),这种情况会发生竞态问题 (数据竞态)

# 互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访问共享资源

Go 语言中使用 sync 包的 Mutex 类型来实现互斥锁,使用方法如下:

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

import (
"fmt"
"sync"
)

// 创建全局变量x
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
)

// 使用两个goroutine,并行给x+50000,形成竞态问题
func add() {
for i := 0; i < 50000; i++ {
lock.Lock() //加锁
x += 1
lock.Unlock() //释放锁
}
wg.Done()
}

func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}

# 读写互斥锁

互斥锁是完全互斥的,但有些时候有些资源仅被少量修改,大量读取时,可以尝试仅添加写锁,而不限制其访问。

使用 sync 包中的 RWMutex 类型实现读写锁,具体如下:

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

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

// 创建全局变量x
var (
x int64
wg sync.WaitGroup
rwLock sync.RWMutex
)

// 模拟读取操作
func read() {
//添加读锁
rwLock.RLock()
time.Sleep(time.Millisecond)
//释放读锁
rwLock.RUnlock()
wg.Done()
}

// 模拟写入操作
func write() {
//添加读写锁
rwLock.Lock()
x += 1
time.Sleep(time.Millisecond * 10)
//释放读写锁
rwLock.Unlock()
wg.Done()
}

func main() {
start := time.Now()

//进行一千次读模拟
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}

//进行十次写模拟
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}

wg.Wait()

fmt.Println(time.Now().Sub(start))
}

# Go 语言 - socket 编程

使用 Go 语言内置的 net 包来进行 tcp 或者 udp 通讯

该包所提供的方法,可以查看 Go 语言标准库 ->net 包

# TCP 样例

# server

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

import (
"bufio"
"fmt"
"net"
)

func process(conn net.Conn) {
//处理结束后关闭连接
defer conn.Close()

//针对当前的连接做数据的发送和接收操作
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:])

if err != nil {
fmt.Println("read from conn failed, err:", err)
break
}

recv := string(buf[:n])
fmt.Println("receive message:", recv)
//把收到的数据返回给客户端
conn.Write([]byte("ok"))
}
}

func main() {
//1.开启服务,监听端口
address := "localhost:20000"
listen, err := net.Listen("tcp", address)
if err != nil {
fmt.Println("listen failed, err:", err)
return
} else {
fmt.Println("listen success.")
fmt.Println("Be listening ", address)
}

for {
//2.等待客户端来建立连接
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
} else {
fmt.Println("connect success")
}

//3.启动一个单独的goroutine去处理连接
go process(conn)
}
}

# client

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 (
"bufio"
"fmt"
"net"
"os"
"strings"
)

func main() {
//1.与服务端建立连接
conn, err := net.Dial("tcp", "localhost:20000")
if err != nil {
fmt.Println("dial failed, err:", err)
return
} else {
fmt.Println("dial success")
}

//2.利用该连接进行数据的发送和接收
input := bufio.NewReader(os.Stdin)
for {
s, _ := input.ReadString('\n')
s = strings.TrimSpace(s)
if strings.ToUpper(s) == "Q" {
return
}

//给服务端发消息
_, err := conn.Write([]byte(s))
if err != nil {
fmt.Println("send failed, err:", err)
return
}

//从服务端接收回复的消息
var buf [1024]byte
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("read failed, err:", err)
return
}

fmt.Println("receive server respond:", string(buf[:n]))
}
}

# UDP 样例

# server

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

func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 30000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
} else {
fmt.Println("Be listening 127.0.0.1:30000")
}
defer listen.Close()

for {
var buf [1024]byte
n, addr, err := listen.ReadFromUDP(buf[:])
if err != nil {
fmt.Println("read from udp failed, err:", err)
return
}
fmt.Println("receive message:", string(buf[:n]))
_, err = listen.WriteToUDP(buf[:n], addr)
if err != nil {
fmt.Println("write to", addr, "failed, err:", err)
return
} else {
fmt.Println("send success")
}
}
}

# client

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

import (
"bufio"
"fmt"
"net"
"os"
)

func main() {
conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 30000,
})
if err != nil {
fmt.Println("dial failed, err:", err)
return
}
defer conn.Close()

input := bufio.NewReader(os.Stdin)
for {
s, _ := input.ReadString('\n')
_, err = conn.Write([]byte(s))
if err != nil {
fmt.Println("send to server failed, err:", err)
return
}

var buf [1024]byte
n, addr, err := conn.ReadFromUDP(buf[:])
if err != nil {
fmt.Println("recv from udp failed, err:", err)
return
}
fmt.Println("read from", addr, "message:", string(buf[:n]))
}
}