红岩2019课件大概是刷完了,从接触golang到今天大概花了接近20天

由于只学习没地方应用,总是觉得学着后面的忘着前面的。

现如今进行一次复习,对其中遗漏,遗忘的知识点进行总结。

第一节课

var的一种声明方法:

1
2
3
4
var (
A int = 100
B string
)

const常量:

var可以不赋值(会自动赋值初始值),而const必须赋值

for代替while:

1
2
3
4
sum := 1
for sum < 1000 {
sum += sum
}

for range遍历循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for key, value := range oldMap {
newMap[key] = value
} //由于个人习惯问题,for range用的不多,手很生

func main() {
numbers := [6]int{1, 2, 3, 5}

for i, x := range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i, x)
}
}
0 位 x 的值 = 1
1 位 x 的值 = 2
2 位 x 的值 = 3
3 位 x 的值 = 5
4 位 x 的值 = 0
5 位 x 的值 = 0 //output 可见0也会被输出

if的短声明

1
2
3
4
5
6
7
8
9
10
11
if v := math.Pow(x, n); v < lim {
...
}

if x := 0; a > 0 {
...
} else if x := 1; a < 0 {
...
} else {
...
} //else if也可以带短声明,但else不能带短声明

switch

  • switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面不需要再加 break (和java c不一样
  • switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough 。
  • 最后有一个default XD

两个格式:

跟变量

1
2
3
4
5
6
7
8
switch varA {
case 0:
...
case 1:
...
default:
...
}

不跟变量

1
2
3
4
5
6
7
8
switch {
case varA > 2:
...
case varA > 0:
...
default:
...
}

其中一句case可以跟多个语句:

1
2
3
4
5
6
7
8
9
10
11
	a := 0
switch a {
case 0 :
fmt.Println("9")
fmt.Println("0")
default :
fmt.Println("x")
fmt.Println("x")
}
9
0 //output

Type Switch

switch 语句还可以被用于 type-switch 来判断某个 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
31
32
33
switch x.(type){
case type:
statement(s);
case type:
statement(s);
/* 你可以定义任意个数的case */
default: /* 可选 */
statement(s);
} //有点像断言

package main //实例

import "fmt"

func main() {
var x interface{}

switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T",i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型" )
default:
fmt.Printf("未知型")
}
}
x 的类型 :<nil> //output

fallthrough

使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。

注意,只是下一条,不包括下下一条

数组

Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:

1
var variable_name [SIZE] variable_type

数组是值类型

切片

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

或使用make()函数来创建切片

1
2
3
var slice1 []type = make([]type, len)

slice1 := make([]type, len)

切片里[a:b]都是左闭右开区间, 即从a 到 b - 1

切片的容量:

在slice中,len(sli)表示可见元素有几个(也即直接打印元素看到的元素个数),而cap(sli)表示所有元素有几个,比如

1
2
3
4
5
6
7
8
9
10
	arr := []int{2, 3, 5, 7, 11, 13}
sli := arr[1:4]
fmt.Println(sli)
fmt.Println(len(sli))
fmt.Println(cap(sli))
fmt.Println(sli[:5])
[3 5 7]
3
5
[3 5 7 11 13] //output 可见sli里是有11 13的,只是被隐藏了不显示。而且当切片自动扩容时,会把多出来的容量赋值为已经存在的内容

切片相关的函数

append添加元素
1
2
3
4
5
6
7
sli = append(sli, 元素)
//append的用法有两种:
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

//第一种用法中,第一个参数为slice,后面可以添加多个参数。
//如果是将两个slice拼接在一起,则需要使用第二种用法,在第二个slice的名称后面加三个点,而且这时候append只支持两个参数,不支持任意个数的参数。

在添加元素时候,若原本slice的容量不够了,则会自动扩大一倍cap,在扩大cap时候是将原来元素复制一份(而不是引用)

每一次扩容空间,都会重新申请一块区域,把旧空间里面的元素复制进来到新空间里,然后把新的元素追加进来。旧空间里面的元素等着垃圾回收。

这就容易理解为什么要使用 sli = append()的形式了,因为返回值是一个指针。

copy

没什么意思,就是直接把元素复制一份,(而不是引用)

函数的引用

传递值变量的时候,若想在函数中造成改变,记得使用传递地址

1
2
3
4
5
6
7
8
9
10
func swap(a, b *int) {
*a, *b = *b, *a
}

func main() {
a := 1
b := 2
swap(&a, &b)
fmt.Println(a, b)
}

第二节课

结构体

结构体初始化

1
2
3
4
5
6
7
8
9
10
11
d := dove{
name: "lcm",
age: 21,
gender: "male",
doveNum: 999,
}

d1 := dove{"lcm", 21, "male", 999}

d2 := dove{}
d2.name = "lcm"

结构体函数

1
2
3
func (name type) funcName() result {
// ignore some code
}

这种函数叫做方法

结构体的内嵌

1
2
3
4
5
6
7
8
9
10
11
12
13
type dove struct {
person
doveNum int // 鸽的次数
}

d := dove{
person: person{
name: "lcm",
age: 18,
gender: "male",
},
doveNum: 999,
}

和继承蛮像的

接口

接口断言

1
value, ok := in.(type)

注意和第一课的type switch结合记忆

第三节课

进程和线程

  • 线程和进程最根本的区别在于:进程是资源分配的基本单位,线程是CPU调度和执行的基本单位。(程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行(程序的一次执行被称为进程),所以这里我们称进程是资源分配的单位)
  • 线程是进程的一部分,所以线程有的时候被称为轻量级进程
  • 多线程: 在同一应用程序中有多个顺序流同时执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var (
myRes = make(map[int]int, 20)
// 声明全局互斥锁
lock sync.Mutex
)

func factorial(n int) {
var res = 1
for i := 1; i <= n; i++ {
res *= i
}

lock.Lock() // 在进行存储操作之前,先加锁
myRes[n] = res
lock.Unlock() // 当存储完毕后,进行解锁
}

简单暴力

Channel

不带缓存的channel

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。

只存不取或者只取不存会产生堵塞

for range 遍历channel

记得要close(chan) 否则会deadlock

对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据

记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic

单方向的Channel

前面我们提到,channel具有接受和发送两种操作,但如果我们只想要一个具有发送功能的channel应该怎么办呢?

Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。

类型chan<- int表示一个只发送int的channel,只能发送不能接收(只读)。相反,类型<-chan int表示一个只接收int的channel,只能接收不能发送(只写)。

Select

select 是一种与 switch 非常相似的控制结构,与 switch 不同的是,select 中虽然也有多个 case,但是这些 case 中的表达式都必须与 Channel的操作有关,也就是 Channel 的读写操作,下面的函数就展示了一个包含从 Channel 中读取数据和向 Channel 发送数据的 select 结构:

如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机会

如果没有case需要处理,则会选择default去处理,若没有default语句时则select语句会被阻塞(select默认为阻塞),直到某个case需要处理

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
func fibonacci(ch, quit chan int) {
x, y := 0, 1
for {
select {
case ch <- x:
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++ {
// 接受通道c传来的值,并打印到控制台
fmt.Println(<-c)
}
// 当协程执行完上述操作后,向quit发送数据
quit <- 0
}()
fibonacci(c, quit)
}

think.1 这段代码写的很巧妙。 协程这一块内容我总感觉缺了一些相应的思想,程序都能看懂,但是自己写不出。

这里quit的处理方法和那个channel算1000000素数的exit很相似。

假如没有这个quit,斐波那契函数中的for循环就会一直执行下去,可以自然想到用quit。

think.2 突然又想到前面那个生产者,消费者的比喻,觉得真是贴切。

斐波那契函数是生产者,产出x放到chan中,而匿名自执行函数是消费者,负责取走x。

此外,这二者是并行的,如同两种生物

IO流

读入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
file, err := os.Open("C:/Users/wuhaoda/Desktop/golang/src/ioText/pack/t1.txt")
defer file.Close()

if err != nil {
fmt.Println(err)
return
}

s := make([]byte, 128)
var str []byte //注意此处不能用make,因为make之后会给str赋值为空格,append后文段开头会有空格
var n int

for {
n, err = file.Read(s)

if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
}

str = append(str, s[:n]...)
}

fmt.Println(string(str))
}
1
2
3
4
5
6
7
8
func main() {
str, err := ioutil.ReadFile("C:/Users/wuhaoda/Desktop/golang/src/ioText/pack/t1.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(str))
}

这是另一种比较方便的写法,但不建议用来处理长本文

写入

1
2
3
4
5
6
7
8
9
10
11
func main() {
file, err := os.OpenFile("C:/Users/wuhaoda/Desktop/golang/src/ioText/pack/t2.txt", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
defer file.Close()

if err != nil {
fmt.Println(err)
return
}

file.WriteString("666")
}

另外问了一下学长,使用bufio的方法更优。但是暂时不想学,用不到的话学了也要忘记。其实是懒

第四节课

map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var m1 map[string]int = make(map[string]int)
m1["haha"] = 1

// map声明初始化并赋值
m2 := map[string]int {
"haha" : 1,
"lala" : 2,
"A" : 1,
"B" : 3,
"C" : 4,
"wuhaoda" : 7,
}

if v, ok := m2["haha"]; ok { //判断是否有值 (有
fmt.Println(v, " Find It")

} else {
fmt.Println("No this")
}

delete(m2, "haha") //删除一个键对

for key, v := range m2 { //for range遍历map
fmt.Println(key, v)
}

if v, ok := m2["haha"]; ok { //判断是否有值 (无
fmt.Println(v)
} else {
fmt.Println("No this")
}

time包,string函数,strconv

放弃了,太多了背不下来。

不过看了一遍知道了有哪些功能,需要时再查就OK

还是因为懒

panic和recover

概念

panic和recover是两个内置函数,这两个内置函数用来处理Go的运行错误

panic用来主动抛出错误,recover用来捕获panic抛出的错误

一般发生panic之后流程是这样的

程序会从调用panic的函数位置或发生panic的地方立即返回,然后逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,直到被recover捕获或运行到最外层函数而退出

这也就解释了为什么defer+recover可以捕获panic

recover()用来捕获panic,阻止panic继续向上传递,recover()和defer一起使用,但是recover()只有在defer后面的函数体内被直接调用才能捕获panic终止异常,否则返回nil,异常继续向外传递

延迟调用: 只有最后的可以被捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func Demo_2_2(){
defer func() {
if err:=recover();err!=nil{
fmt.Println(err)
}
}()

//只有最后一次能被捕获
defer func() {
panic("first defer panic")
}()

defer func() {
panic("second defer panic")
}()

panic("main body panic")
}

//调用结果
first defer panic

函数并不能捕获内部新启动的goroutine所抛出的panic

使用场景

之前一直很疑惑为什么要使用panic

在什么情况下主动调用panic函数抛出panic呢?

  • 程序遇到了无法执行下去的错误,主动调用panic函数结束程序运行
  • 在调试程序的时候主动调用panic实现快速推出,panic打印出的堆栈能够更快地定位错误

但为了保证程序的健壮性,需要主动在程序的分支流程上使用recover()拦截运行时的错误

之前写的一个计算器就用到了panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func solve() int {
var x, y int
var o string
fmt.Scanf("%d %s %d", &x, &o, &y)
switch o {
case "+":
return add(x, y)
case "-":
return sub(x, y)
case "*":
return mul(x, y)
case "/":
return dev(x, y)
}
// fmt.Println(o)
defer func() {
err := recover()
if err != nil {
fmt.Println(err)
}
}()
panic("只能进行四则运算")
}

error

Go语言的内置错误接口类型error

1
2
3
type error interface{
Error() string
}

Go中典型的错误处理方式就是将error作为函数最后一个返回值,在调用函数时,通过检测其返回的error值是否为nil来进行错误处理

  • 在多个返回值的函数中,error常常为函数的最后一个返回值

  • 如果一个函数返回error类型变量,则先用if处理error!=nil的异常场景,正常逻辑放到if语句块的后面,保持代码平坦

  • defer语句应该放到error判断的后面,不然有可能产生panic

在实际的编程中,error和panic的使用应该遵循如下三个原则

  • 程序发生的错误导致程序不能容错继续执行,此时程序应该主动调用panic或由运行时抛出panic
  • 程序虽然发生错误,但是程序能够容错执行,此时应该使用错误返回值的方式处理错误,或者在可能发生运行时错误的非关键分支上使用recover捕获error

第五节课

简单的web服务

1
2
3
4
5
6
7
8
9
10
11
12
13
r := gin.Default()   //初始化一个路由header
r.GET("/hello", func(c *gin.Context) { //绑定路由规则和路由函数
fmt.Println(c.FullPath()) //打印完整路径
c.JSON(200, gin.H{
"hello": "guest",
})
})
/*
output:
{
"hello": "guest"
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
	r.GET("/hello/:name", func(c *gin.Context) {
username := c.Param("name") //该函数读取name字段,要牢记格式
c.String(http.StatusOK, "Hello %s", username) //注意第一个参数为状态码int OK是200
})
``` go
r.GET("/query/hello", func(c *gin.Context) {
nname := c.DefaultQuery("name", "guest") //带默认值的
message := c.DefaultQuery("message", "success")
c.JSON(200, gin.H{
"message" : message,
"name" : nname,
})
})

POST参数/报文体

1
2
3
4
5
6
7
8
9
10
r.POST("/form", func(c *gin.Context) {
name := c.DefaultPostForm("username", "guest")
sex := c.DefaultPostForm("sex", "unknow")
usrphone := c.PostForm("phone")
c.JSON(200, gin.H{
"name" : name,
"sex" : sex,
"phone" : usrphone,
})
})

记得这个是表单,再postman中,提交要再body中选择form-data

表单实体绑定

当表单数据过多时,使用PostForm和GetPostFrom一次获取一个表单数据,效率较慢。

使用结构体和表单提交数据绑定功能,可以提高效率

ShouldBindQuery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type User struct {
Username string `form:"thisUserName" binding:"required"` //注意:字段是form后面跟的
Phone string `form:"thisPhone" binding:"required"`
Password string `form:"thisPassword" binding:"required"`
}

r.GET("/bind", func(c *gin.Context) {
var user User
err := c.ShouldBindQuery(&user) //取地址
if err != nil {
log.Fatal(err.Error())
return
}
c.JSON(200, user) //可以用JSON直接输出
})

ShouldBind

1
2
3
4
5
6
7
8
9
r.POST("/postbind", func(c *gin.Context) {  //用于POST
var user User
err := c.ShouldBind(&user)
if err != nil {
log.Fatal(err.Error())
return
}
c.JSON(200, user)
})

BindJSON

1
2
3
4
5
6
7
8
9
r.POST("/postjson", func(c *gin.Context) {
var user User
err := c.BindJSON(&user) //换汤不换药
if err != nil {
log.Fatal(err.Error())
return
}
c.JSON(200, user)
})

加载HTML页面

1
2
3
4
5
6
7
8
9
10
r.LoadHTMLGlob("./html/*")    //必须加这个指定地址
r.Static("/img", "./img") //加载静态资源 把请求和本地静态文件进行映射

r.GET("/hellohtml", func(c *gin.Context) {
fullPath = c.FullPath()
c.HTML(200, "index.html", gin.H{
"fullPath" : fullpath,
"title" : "我的第一段html",
} //中间那个填写html名称 后面是要传到前端的数据
})
1
2
3
4
5
6
7
8
9
<html>
<head>
<title>{{.title}}</title>
</head>
<hl align="center">啦啦啦</hl>
{{.fullPath}}
<br/>
<div align="center"><img src="../img/haha.jpg"></div>
</html>

这是👴写的第一段html

路由组

1
2
3
4
5
6
7
8
9
10
r := gin.Default()
rt := r.Group("/user")

rt.POST("/register", func(c *gin.Context) {
var user User
c.ShouldBind(&user)
c.JSON(200, user)
})

r.Run()

中间件

1
2
3
4
5
6
7
rt.Use(getInf()) //全局使用

rt.POST("/register", getInf(), func(c *gin.Context) { //对单个请求使用
var user User
c.ShouldBind(&user)
c.JSON(200, user)
})
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
func getInf() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("1")

c.Next() //使用next分割为请求前执行和请求后执行两部分
fmt.Println("4")
}
}

func main() {
r := gin.Default()
rt := r.Group("/user")

//rt.Use(getInf()) //全局使用

rt.POST("/register", getInf(), func(c *gin.Context) { //对单个请求使用
var user User
fmt.Println("2")
c.ShouldBind(&user)
c.JSON(200, user)
fmt.Println("3")
})

r.Run()
}