0%

Go|Go语言修炼手册

img

Go的吉祥物Gopher也太可爱了叭!!!

参考文章:

Go语言标准库:https://studygolang.com/pkgdoc

Go案例代码:https://gobyexample-cn.github.io/

Go语言学习手册:https://www.topgoer.com/

1.Golang的主要特征

  • 自动立即回收

  • 更丰富的内置类型

  • 函数多返回值

  • 错误处理

  • 匿名函数和闭包

  • 类型和接口

  • 并发编程

  • 反射

  • 语言交互性

2.第一个Go程序

1
2
3
4
5
6
7
8
9
10
// 要导入main包才能运行main函数
// 而且一个项目中只能有一个文件导入main包使用main函数(类似于C语言)

package main

import "fmt"
func Hello() {
fmt.Println("HelloGo")
}

3.Go的值运算

1
2
3
4
5
6
7
8
9
10
11
12
13
// Value 数据值的一些运算
func Value() {

fmt.Println("go"+"lang")
fmt.Println("go","lang")
fmt.Println("1+1=", 1+1)
fmt.Println("7.0/3.0")

fmt.Println(true && false)
fmt.Println(true || false)
fmt.Println(!true)

}

运行结果:

image-20210907085705239

变量与常量

1.变量

(1)标准声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Go语言用var声明变量

// go推断变量类型
var a = "TestString"
fmt.Println(a)

// 变量名后声明变量类型
var b, c int = 1, 2
fmt.Println(b, c)

var d = true
fmt.Println(d)

// 声明变量赋给默认值
var e int
fmt.Println(e)
(2)批量声明
1
2
3
4
5
6
var (
a string
b int
c bool
d float32
)
(3)短变量声明
1
2
3
// 化简写法声明字符串变量f(相当于 var f string = "apple")
f := "apple"
fmt.Println(f)

2.常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明恒定值
const s string = "constant"

func Constants() {
fmt.Println(s)

const n = 500000

const d = 3e20 / n

fmt.Println(d)
// 转换类型
fmt.Println(int64(d))
// 数学运算
fmt.Println(math.Sin(n))

}

常量的声明只是将变量声明中的var更改为const

基本数据结构

1.数组Array

(1)声明默认数组
1
2
3
4
5
6
7
8
// 声明默认数组
var a [5]int
fmt.Println(a)
// 数组元素赋值
a[4] = 100
fmt.Println(a)
fmt.Println(a[4])
fmt.Println(len(a))
(2)声明并初始化数组
1
2
3
4
5
// 声明并初始化数组
b:= [5]int{1, 2, 3, 4}
// ...即让程序自动读取数组大小
c:= [...]int{1, 2, 3, 4, 5, 6}
fmt.Println(b, c)
(3)声明二维数组
1
2
3
4
5
6
7
8
9
// 声明二维数组
var twoD [2][3]int
for i := 0; i < 2; i++{
for j:=0; j < 3; j++ {
twoD[i][j] = i + j
}
}

fmt.Println(twoD)

2.切片Slice

(1)切片简介
  • 切片是 Go 中的一种关键数据类型(引用类型),它为序列提供了比数组更强大的接口
  • 切片的长度可以改变,因此,切片是一个可变的数组
  • 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制
(2)切片一般声明
1
2
3
4
// 声明切片(让数组长度为空即形成切片)
var s1 []int
s2 := []int{2,4,5}
fmt.Println(s1, s2)
(3)make创建
1
2
3
4
5
6
// 声明非0长度的空片,需要用内置的make函数
// make([]type, len, cap):len(长度),cap(容量即为切片最长长度,可选)
// cap可以求出slice最大扩张容量,不能超出数组限制。len(slice) >= len(array),其中array是slice引用的数组
s3 := make([]string, 5, 6)
fmt.Println(s3)
fmt.Println(len(s3),cap(s3))
(4)切片赋值与追加
1
2
3
4
5
6
7
8
9
10
11
// 切片赋值(虽然切片可以自动扩容,但是取索引时不会扩容)
s3[0] = "a"
s3[4] = "b"
fmt.Println(s3)

// 向切片中追加元素(必须接受返回值,在追加时可能使切片的容量增大)
s3 = append(s3, "d")
s3 = append(s3, "e","f")
// 切片可以追加其他切片或数组使用...解压缩
s3 = append(s3, s3...)
fmt.Println(s3)
(5)切片复制
1
2
3
4
// 切片复制
c := make([]string, len(s3))
copy(c, s3)
fmt.Println(c)
(6)由数组或切片获得切片
1
2
3
4
5
6
7
// 由数组或切片获得切片
// 获得s3的5,6元素
l := s3[5:7]
fmt.Println(l)
// 去掉切片最后一个元素
l = s3[:len(s3)-1]
fmt.Println(l)

3.Map

(1)map的创建
1
2
3
// 创建空地图(键类型为string,值类型为int)
m := make(map[string]int)
m1 := make(map[int]string)
(2)map初始化
1
2
3
// map初始化
n := map[string]string{"foo":"str1", "bar":"str2"}
fmt.Println(n)
(3)map赋值与取值
1
2
3
4
5
6
7
8
9
10
11
12
// map键值对赋值
m["k1"] = 7
m["k2"] = 10
// map的键为整型时区别于数组
m1[1] = "test1"
m1[10] = "test2"
fmt.Println(m)
fmt.Println(m1)

// 取值
v1 := m["k1"]
fmt.Println(v1)

不存在取值的返回:

1
2
3
4
// 不存在的键值默认返回为0,加上_,返回fasle(下划线用来忽略结果,也可以理解为那个位置本应赋给某个值,但是咱们不需要这个值)
prs := m["k2"]
_,prs2 := m["k2"]
fmt.Println(prs, prs2)

image-20210907094003976

(4)map删除操作
1
2
3
// 删除(map限定,不存在的键自动略过不报错)
delete(m, "k2")
fmt.Println(m)

基本流程控制

1.if语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func If(str string)  {

// if语句
if str=="IfA" {
fmt.Println("a条件")
}else{
fmt.Println("其他条件")
}

// 声明可以先于条件
if str = "IfA"; str=="IfA"{
fmt.Println("a条件满足")

}

}

2.switch语句

(1)使用变量作为判定条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// switch基本案例,使用变量作为判定条件
i := 2
fmt.Println("write", i, "as")
switch i {

case 1:
fmt.Println("one")

case 2:
fmt.Println("two")

case 3:
fmt.Println("three")

}

(2)使用函数返回的变量值作为expression
1
2
3
4
5
6
7
8
9
10
// 使用函数返回的变量值作为expression
// Weekday返回周数(weekday类型)
switch time.Now().Weekday(){
case time.Saturday, time.Sunday:
fmt.Println("weekend")

default:
fmt.Println("weekday")

}
(3)使用函数返回值的变量作为constant-expression
1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用函数返回值的变量作为constant-expression
t := time.Now()
switch {

case t.Hour() >= 6 && t.Hour() < 8:
fmt.Println("清晨")
case t.Hour() >= 8 && t.Hour() < 12:
fmt.Println("早上")

default:
fmt.Println("其他时间")

}

3.for语句

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
func For()  {

i := 1

// 只要i小于等于3就一直执行循环
for i <= 3{
fmt.Println(i)
i = i + 1

}

// 经典for语句初始/条件/后循环
for j := 7; j <= 9; j++{
fmt.Println(j)
}

// 无条件for循环等待break终止
for{
fmt.Println("loop")
break
}

for n:= 0; n <= 5; n++{

// 复数进入下个循环
if n%2 == 0 {
continue
}
fmt.Println(n)

}

}

4.Range语句

(1)Range简介
  • range类似迭代器操作,返回 (索引, 值) 或 (键, 值)
  • range 格式可以对 slice、map、数组、字符串等进行迭代循环
(2)数组遍历
1
2
3
4
5
6
7
8
nums := []int{2, 3, 4}
sum := 0
// 遍历数组
//range会提供索引和值,这里我们不需要索引所以用占位符代替
for _, num := range nums{
sum += num
}
fmt.Println(sum)
(3)map遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 遍历map
kvs := map[string]string{"a":"Ava", "b":"Bella", "c":"Carol", "d":"Diana", "e":"Eileen"}

for k, v := range kvs{
fmt.Printf("%s : %s\n", k, v)
}

// 遍历键
for k := range kvs{
fmt.Println(k)
}

// 遍历值
for _,v := range kvs{
fmt.Println(v)
}

函数

1.多参数

(1)两参数
1
2
3
4
5
// 函数接受两个int参数,返回值为int
func plus(a int, b int) int {
return a + b

}
(2)同类型多参数
1
2
3
4
5
6
7
8
9
// 接受多个同类型参数时
func plusPlus(a, b, c int) string{
// 使用strconv包将整型转换成字符串(强制类型转换会出现乱码)
a1 := strconv.Itoa(a)
b1 := strconv.Itoa(b)
c1 := strconv.Itoa(c)
return a1 + b1 + c1

}
(3)可变参数
1
2
3
4
5
6
7
8
9
10
11
12
13
// 可变参数(不定数量参数),只能作为最后的参数项
func sum(str string, nums ...int) {

fmt.Println(str, nums)
total := 0

for _,num := range nums{
total += num
}

fmt.Println(total)
}

2.多返回值

1
2
3
4
5
6
7
8
// 多个返回值的函数
func vals(a, b int) (int, int) {

add := a + b
sub := a - b

return add, sub
}

3.闭包函数

(1)声明
1
2
3
4
5
6
7
8
9
10
11
12
// 匿名函数形成闭包
// 匿名函数常用于赋给变量,像普通变量一样传递操作
// 闭包最终效果就是将闭包赋予变量后,通过调用变量调用闭包函数
func intSeq() func() int {

i := 0
return func() int {
i++
return i
}

}
(2)调用
1
2
3
4
5
6
7
8
// 变量赋予闭包,变量可以作为函数使用
nextInt := intSeq()
// intSeq()输出的是原对象指针
fmt.Println(intSeq())
fmt.Println(nextInt())
fmt.Println(nextInt())
nextInt2 := intSeq()
fmt.Println(nextInt2())
(3)结果

image-20210907100638778

4.递归函数

1
2
3
4
5
6
7
8
9
10
11
// 递归计算阶乘
// 递归函数特征
// 1.子问题须与原始问题为同样的事,且更为简单。
// 2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
func fact(n int) int {
if n == 0{
return 1
}
return n * fact(n-1)

}

指针

1.函数中的指针参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 普通参数是将值传递给函数,相当于复制了副本
func zeroval(ival int) {

ival = 0

}

// 用指针作为参数,函数直接操作参数的内存地址
func zeroptr(iptr *int) {

fmt.Println(iptr)
// 引用当前地址的值并修改
*iptr = 0

}

2.调用与运行结果

1
2
3
4
5
6
7
8
9
i := 1

zeroval(i)
fmt.Println(i)

// 取变量地址作为参数
zeroptr(&i)
fmt.Println(i)
fmt.Println(&i)

运行结果:

image-20210907101514269

结构体

1.结构体简介

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性

2.结构体实例化

(1)声明结构体
1
2
3
4
5
6
7
// 结构体是字段的类型化集合,它们可以将数据组合在一起形成记录
type person struct {

name string
age int

}
(2)结构体实例化
1
2
3
4
// 结构体实例化方法
fmt.Println(person{"Bob", 20})
s := person{name: "Sean", age: 50}
fmt.Println(s.name)

输出结果

image-20210907102546338

(3)结构体地址
1
2
3
4
// 结构体地址
fmt.Println(&person{name: "Ava"})
sp := &s
fmt.Println(sp)

输出结果

image-20210907103421612

3.构造函数

(1)构造方法
1
2
3
4
5
6
7
8
9
10
// newPerson给定的名称一个新的person结构体
func newPerson(name string) *person{

// 声明新的结构体并赋值
p := person{name:name}
p.age = 42
// 返回新结构体的位置
return &p

}
(2)调用与结果
1
fmt.Println(newPerson("Bella"))

输出结果

image-20210907103904793

4.结构体方法

  • 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
  • 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法
  • 为了减少内存开销,一般用指针类型作为接收者,用指针变量调用方法
(1)定义结构体
1
2
3
4
// 定义结构体的方法
type rect struct {
width, height int
}
(2)指针接收类型方法
1
2
3
4
// 指针接收器类型
func (r *rect) area() int{
return r.width * r.height
}

调用:

1
2
3
4
5
// 值类型调用方法
fmt.Println("area", r.area())
// 指针类型调用方法
rp := &r
fmt.Println("area", rp.area())

运行结果:

image-20210907110512031

(3)值接收类型方法
1
2
3
4
5
6
// 值接收器类型
func (r rect) perim() int {

return 2 * r.width + 2 * r.height

}

调用:

1
2
3
4
// 值类型调用方法
fmt.Println("perim", r.perim())
// 指针类型调用方法
fmt.Println("perim", rp.perim())

运行结果:

image-20210907110638342

接口

1.接口简介

  • 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节
  • interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则)

2.接口的实现

(1)定义接口
1
2
3
4
5
6
// go语言提倡面向接口编程
// 接口是一种抽象类型,定义对象的行为规范
type Gemoetry interface {
area() float64
perim() float64
}
(2)构建结构体(用于生成对象)
1
2
3
4
5
6
7
8
9
// 构建结构体
type rect2 struct {
width, height float64
}

type circle struct {
radius float64
}

(3)结构体的方法实现接口

只要对象实现了接口中所有的方法,就是实现了这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 只要对象实现了接口中所有的方法,就是实现了这个接口(不需要显性声明)
func (r rect2) area() float64 {
return r.width * r.height
}
func (r rect2) perim() float64 {
return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
return 2 * math.Pi * c.radius
}
(4) 结构体实例化生成对象
1
2
3
4
5
6
7
8
9
10
// 实现接口后,接口类型变量可以存储所有实现该接口的实例
var x Gemoetry

x = rect2{width: 3, height: 4}
fmt.Println(x.area())
fmt.Println(x.perim())

x = circle{radius:5}
fmt.Println(x.area())
fmt.Println(x.perim())

异常处理

go语言通过显式的返回值传递错误

1.内置接口返回错误信息

1
2
3
4
5
6
7
8
9
10
11
// 参数arg,返回值类型int,error
// 使用内置接口返回错误信息
func f1(arg int)(int, error) {

if arg == 42{
return -1, errors.New("can't work")
}

return arg + 3, nil

}

2.自定义结构体

(1)定义结构体
1
2
3
4
5
// 自定义结构体实现 Error ()方法,可以显示表示错误
type argError struct{
arg int
prob string
}
(2)定义结构体方法
1
2
3
4
5
// 定义结构体的方法
func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.prob)

}
(3)异常测试方法
1
2
3
4
5
6
7
8
9
10
11
// 参数arg,返回值类型int,error
func f2(arg int)(int, error){

if arg == 42{
// 实例化argError返回指针error(执行函数Error)
return -1, &argError{arg, "cam't work with it"}
}

return arg + 3, nil

}
(4)测试用例
1
2
r,e = f2(42)
fmt.Println(r, e)

运行结果:

image-20210907213437028

并发编程

1.Goroutines(协程)

(1)协程简介
  • go func()这种形式即可实现创建一个新的协程执行函数
  • 一般函数调用是阻塞主线程的,即为同步;而使用协程调用函数,则会与主线程,其他协程一起运行,则是一个异步的过程
  • 在并发编程中,不能用顺序执行语句的同步思维。在多个程序同时运行时,要考虑到各个协程开始的时间和结束的时间决定的运行结果,各个协程对同一数据的同时操作,各个协程任务同步,以及主线程结束后会关闭其他协程(不管其任务是否执行完毕)的问题
(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
28
func f(from string)  {

for i := 0; i < 5; i++{
fmt.Println(from, ":", i)
}


}

func Goroutines() {

// 一般调用同步运行
f("direct")

// 将函数放到goroutine运行(Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU)
go f("goroutine")

// 为匿名函数启动一个goroutine(最后完成调用)
go func(msg string) {
fmt.Println(msg)
}("going")

// 必须设置等待,使主线程阻塞,否则main()执行完后goroutine也一同结束了
time.Sleep(time.Millisecond)
fmt.Println("done")


}

运行结果:

image-20210908074235644

2.Channels(通道)

(1)通道简介
  • 通过goroutines可以实现函数并发运行,而这些并发执行的函数可以通过channels交换数据
  • 默认情况下,通道是无缓冲的(阻塞通道),无缓冲的通道必须有接收才能发送
  • 使用make函数初始化通道的时候为其指定通道的容量即可生成有缓冲通道
(2)通道的发送与接收
1
2
3
4
5
6
7
8
9
10
11
// channel是一种类型,一种引用类型
// 声明并初始化channel(声明的通道后需要使用make函数初始化之后才能使用)
messages := make(chan string)
fmt.Println(reflect.TypeOf(messages), messages)

// goroutines中函数将信息发送到messages通道
go func() {messages <- "ping"}()

// 接收信息
msg := <-messages
fmt.Println(reflect.TypeOf(msg), msg)

运行结果:

image-20210908081215613

(3)无缓冲通道
1
2
3
4
// 默认情况下,通道是无缓冲的(阻塞通道),无缓冲的通道必须有接收才能发送
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")

运行结果:

image-20210908081412049

无缓冲通道原理图:

无缓冲相当于小区没有快递点,快递员必须亲自把这个物品送到你的手上

img

(4)有缓冲通道
1
2
3
4
5
6
// 使用make函数初始化通道的时候为其指定通道的容量即可生成有缓冲通道
// 有缓冲通道(相当于设置了快递点)
ch := make(chan int, 2)
ch <- 10
ch <- 20
fmt.Println("发送成功")

有缓冲通道原理图:

有缓冲通道相当于小区有个菜鸟驿站(非广告)或代收点帮你存快递,需要时再去取快递

img

(5)信道同步

在协程中存在着主线程结束后不管子协程有没有完成任务仍然让其强制下班的问题

虽然可以通过时间延迟让主线程嗯等,但是这种硬性处理方式浪费资源

通过信道,我们可以让主线程等待子协程发送任务完成的信号再结束自己的生命

子协程任务:

1
2
3
4
5
6
7
8
9
// 信道同步
func worker(done chan bool){
fmt.Println("working")
time.Sleep(time.Second)
fmt.Println("done")
// 发送任务完成的信号到通道
done <- true

}

主线程的运行:

1
2
3
4
5
// 信道同步
done := make(chan bool, 1)
go worker(done)
// 阻塞接收,等待goroutine完成
<- done
(6)单向通道

将通道作为参数在多个任务函数间传递时,限制通道在函数中只能发送或只能接收

单向通道通信方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 发送通道 chan <- int 接收通道 <- chan int
// 发送信息到pings通道
func ping(pings chan<-string, msg string) {
pings <- msg
}

// 接收pings通道的信息, 发送到pongs通道
func pong(pings <-chan string, pongs chan<-string) {

msg := <-pings
pongs <- msg

}

单向通道的使用:

1
2
3
4
5
6
7
8
9
10
// 单向信道
pings := make(chan string, 1)
pongs := make(chan string, 1)
// 发送信息到pings通道
ping(pings, "passed message")
// 接收pings通道的信息, 发送到pongs通道
pong(pings, pongs)

// 接收pongs信道信息并发送
fmt.Println(<-pongs)

3.通道操作

(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
func Close()  {

jobs := make(chan int, 5)
done := make(chan bool)

// 工作协程接收来自Close协程的信息,当接受完毕后关闭jobs通道
go func() {
for {
// 接受到more为fasle说明通道被关闭了,发送信息的任务已经结束
j, more := <-jobs
if more {
fmt.Println("received job", j)
} else {
fmt.Println("received all jobs")
// 完成全部任务后通知close协程
done <- true
return
}
}

}()

// 向jobs通道发送信息
for j := 1; j <= 3; j++{
jobs <- j
fmt.Println("sent job", j)
}

// 关闭通道
close(jobs)
fmt.Println("sent all job")
// close协程接收到完成的信号关闭close协程
// 使得主协程能随工作协程同步结束,可以防止主协程运行完成了,工作协程未完成接收就一起随主协程关闭的情况
<-done

}

运行结果:

image-20210908152110310

(2)通道数据遍历

通过range语句也可以遍历通道的数据,但是要先关闭掉通道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func ChannelsRange()  {

// 通道数据遍历
queue := make(chan string, 3)
queue <- "one"
queue <- "two"
// 遍历前要先关闭通道(让循环的迭代在2后结束)
close(queue)

// 通道关闭后仍然可以接收到信息(无法发送信息)
for elem := range queue{
fmt.Println(elem)
}

}

4.select

(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
c1 := make(chan string)
c2 := make(chan string)

// 开启任务1
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()

// 开启任务2
go func(){
time.Sleep(2 * time.Second)
c2 <- "two"
}()

// 使用select可以同时响应多个通道的操作,从而实现从多个通道接收数据(本质也是对各个通道扫描)
for i := 0; i < 2; i++{
select {
case msg1 := <- c1:
fmt.Println(msg1)

case msg2 := <- c2:
fmt.Println(msg2)
}
}

运行结果:

image-20210908085306965

(2)非阻塞发送接收

这里所谓的非阻塞是指通过select的default让主线程进入默认分支

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
// 使用select实现通道非阻塞发送接收
messages := make(chan string)
signals := make(chan bool)

// 非阻塞接收
select{
// 成功读到数据到msg
case msg := <-messages:
fmt.Println("received message", msg)
default:
fmt.Println("no message received")
}

msg := "hi"
// 非阻塞发送
select{
// 成功发送数据到messages
case messages <- msg:
fmt.Println("sent message", msg)
default:
fmt.Println("no message sent")
}

select {

case msg := <- messages:
fmt.Println("received message", msg)

case sig := <- signals:
fmt.Println("received signal", sig)

default:
fmt.Println("no activity")
}

运行结果:

image-20210908090500412

(3)超时操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 超时对于一个需要连接外部资源,或者有耗时较长的操作的程序而言是很重要的
func Timeouts() {

c1 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c1 <- "res 1"

}()

select {
// 等待结果
case res := <- c1:
fmt.Println(res)
// 等待超时后发送的值
case <-time.After(1 * time.Second):
fmt.Println("timeout 1")
}

}

5.定时器

(1)定时器Timer

定时器的作用与time.Sleep类似,不同的是time.Sleep硬性规定等待一定时间才能继续进程,而定时器可以在触发前取消掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func Timer()  {
// 定时器表示在未来某一时刻的独立事件
time1 := time.NewTimer(2 * time.Second)

// 阻塞直到定时器的通道C发送定时器失效的值
<- time1.C
fmt.Println("Timer 1 fired")

// 使用定时器与time.Sleep不同之处在于:
// time.Sleep硬性规定等待一定时间才能继续进程,而定时器可以在触发前取消掉
time2 := time.NewTimer(time.Second)
go func() {
<- time2.C
fmt.Println("Time 2 fired")
}()

// 停止定时器
stop2 := time2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}

time.Sleep(2 * time.Second)
}

运行结果:

image-20210908152938454

(2)打点器Ticker

定时器用于执行一次时使用,而打点器用于在固定时间间隔重复执行而准备

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
func Ticker()  {

// 定义打点器
ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)


go func() {
// 不断运行,直到从主协程接收到done
for {
select {
case <- done:
return
case t := <- ticker.C:
fmt.Println("Tick at", t)
}
}

}()

// 打点器一共运行2000ms,每一次500ms,可以粗略算出运行4次
time.Sleep(2000 * time.Millisecond)
// 停止打点器并结束子协程
ticker.Stop()
done <- true
fmt.Println("Ticker stopped")


}

运行结果:

image-20210908153237860

(3)速率限制

速率限制是控制服务资源利用和质量的重要机制。 基于协程、通道和打点器,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
func RateLimiting()  {

requests := make(chan int, 5)
for i:= 1; i <= 5; i++{
requests <- i
}
close(requests)

// 速率限制调度器设置
// tick是NewTicker的封装,只提供对Ticker的通道的访问
limiter := time.Tick(200 * time.Millisecond)

for req := range requests{
// 让请求每200ms执行一次
<-limiter
fmt.Println("request", req, time.Now())
}

// 速率限制方案允许短暂的并发请求,并同时保留总体速率限制
// 填充通道表示允许的爆发
burstyLimiter := make(chan time.Time, 3)

// 每200ms新的值到burstyLimiter中,直到达到 3 个的限制。
for i := 0; i < 3; i++{
burstyLimiter <- time.Now()
}

go func() {
for t := range time.Tick(200 * time.Millisecond){
burstyLimiter <- t
}

}()


// 模拟5个传入请求
busstyRequests := make(chan int, 5)
for i := 1; i <= 5; i++{
busstyRequests <- i
}
close(busstyRequests)

// 受益于burstyLimiter 的爆发(bursts)能力,前 3 个请求可以快速完成
for req := range busstyRequests{
<- burstyLimiter
fmt.Println("request", req, time.Now())
}

}

6.Goroutine池

工作池实质上时生产者消费者模型,其可以有效控制gorouine的数量

工作池中有工人(处理工作的协程),有两条流水线(传递工作的通道与传递结果的通道)

(1)创建工作池
1
2
3
4
5
6
7
8
9
10
11
12
13
// 参数:任务编号id,从通道接收工作jobs,发送结果到res通道
func workers(id int, jobs <-chan int, res chan<- int ) {

for j := range jobs{

fmt.Println("worker", id, "started job", j)
time.Sleep(time.Second)
fmt.Println("worker", id, "finnished job", j)
res <- j * 2

}

}
(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
func WorkerPools(){
const numJob = 5
jobs := make(chan int, numJob)
results := make(chan int, numJob)

// 创建三个worker任务(初始时阻塞的,因为还没有传递任务)
// 三个workers任务并行执行(即工作池有三位工具人,两条流水线)
for w := 1; w <= 3; w++{
go workers(w, jobs, results)
}

// 发送信息到jobs
for j := 1; j <= numJob; j++{
jobs <- j
}
// 关闭通道,表示发送完毕
close(jobs)

// 收集worker的返回值,确保所有worker协程都已经完成
for a := 1; a <= numJob; a++{
<-results
}


}

运行结果:

image-20210908154642399

7.Sync

(1)WaitGroup

在前面的例子中

  • 为了同步子协程与主线程我们通过传递done来让主线程等等子协程
  • 为了让主线程等待工作池内的子协程完成任务我们通过收集返回结果来实现同步
  • 这次我们用 sync.WaitGroup来实现并发任务的同步,其内部维护着一个计数器(每个计数器对应一个并发任务)
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
// sync.WaitGroup来实现并发任务的同步,其内部维护着一个计数器(每个计数器对应一个并发任务)
// WaitGroup 必须通过指针传递给函数
func workerA(id int, wg *sync.WaitGroup) {

// 延迟调用wg的Done方法(相当于直到return后才执行),计数器减1(当计数器为0,表明所有的并发任务完成)
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}

func WaitGroup() {
// WaitGroup 用于等待该函数启动的所有协程
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
// 递增wg计数器
wg.Add(1)
// 启动多个协程任务,并传递其wg计数器
go workerA(i, &wg)

}

// 阻塞(主协程)至计数器变为0(所有并发协程已完成)结束
wg.Wait()

}

运行结果:

image-20210908163034514

8.并发安全

在工作池中我们使用通道之间的通信管理状态,下面我们使用atomic原子技术和互斥锁技术管理状态保证并发安全

这里以多协程并发访问同一变量作为并发安全问题的案例

(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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func Mutexes()  {
// 访问对象
var state = make(map[int]int)

// mutex同步对state的访问
var mutex = &sync.Mutex{}

// 追踪读写操作的数量
var readOps uint64
var writeOps uint64

for r := 0; r < 100; r++{
// 启动100个协程做读取操作,每个协程以每 1ms 一次的频率来重复读取 state
go func() {
total := 0
for{
// 生成键对map进行访问(生成在5之间的随机数)
key := rand.Intn(5)
// 互斥锁保证该协程对map独占访问
mutex.Lock()
// 读取选定键的值
total += state[key]
// 解开互斥锁
mutex.Unlock()
// 读操作增加1
atomic.AddUint64(&readOps, 1)
time.Sleep(time.Millisecond)

}

}()

}

for w := 0; w < 10; w++{
// 启动10个协程模拟写入操作
go func() {
// 与读取的协程类似
key := rand.Intn(5)
val := rand.Intn(100)
mutex.Lock()
state[key] = val
mutex.Unlock()
// 写操作增加1
atomic.AddUint64(&writeOps, 1)
time.Sleep(time.Millisecond)

}()


}
// 让这 10 个协程对 state 和 mutex 的操作持续 1 s(主进程阻塞)
time.Sleep(time.Second)

// 展示最终操作数
readOpsFinal := atomic.LoadUint64(&readOps)
fmt.Println("readOps:", readOpsFinal)
writeOpsFianl := atomic.LoadUint64(&writeOps)
fmt.Println("writeOps:", writeOpsFianl)

// 关闭对state的操作,展示最后结束时的state
mutex.Lock()
fmt.Println("state:", state)
mutex.Unlock()

}

运行结果:

image-20210908171059061

(2)atomic(原子操作)

加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全

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
func Atomic()  {

// 设置原子计数器
var ops uint64
// 设置非原子计数器
var opn uint64

// waitgroup等待协程完成工作
var wg sync.WaitGroup

for i := 0; i < 50; i++{
// 增加协程计数器
wg.Add(1)

// 开启协程进行原子操作
go func() {
for c := 0; c < 1000; c++{
// 原子操作增加计数器数值,各协程操作同一数据时之间不会互相干扰
atomic.AddUint64(&ops, 1)
// 非原子计数,各协程相互干扰
opn++
}
// 增加操作结束后,协程计数器-1
wg.Done()
}()


}

// 等待直到所有协程完成
wg.Wait()

// 安全访问ops(此时无协程写入内容)
// 此外atomic.LoadUint64 之类的函数,在原子更新的同时安全地读取它们
fmt.Println("ops", ops)
fmt.Println("opn", opn)

}

运行结果:

image-20210908165217871

(3)状态协程

互斥锁中通过锁定让state跨多个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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 结构体封装写入与读取的请求
type readOp struct {
key int
resp chan int
}

type writeOp struct {
key int
value int
resp chan bool

}

func SatefulGoroutines() {

var readOps uint64
var writeOps uint64

// 创建readOp,writeOp类型的通道
// 其他协程将通过 reads 和 writes 通道来发布 读 和 写 请求
reads := make(chan readOp)
writes := make(chan writeOp)

// 拥有state的协程(私有),其可以不断接收读写通道的信息
go func() {
// state为该协程私有,其他协程(包括主协程都不能访问)
var state = make(map[int]int)
for {
select{
// 其他协程进行读操作,返回map键对应的值
case read := <-reads:
read.resp <- state[read.key]
// 其他协程进行写操作,返回成功
case write := <-writes:
state[write.key] = write.value
write.resp <- true
}


}


}()


// 启动100个协程通过reads通道发起读请求
for r := 0; r < 100; r++{
go func() {
for{
// 构造read请求
read := readOp{
key: rand.Intn(5),
resp: make(chan int),
}
// read请求发送reads通道中进行读操作
reads <- read
// 通过给定的resp通道接收结果
<- read.resp
atomic.AddUint64(&readOps, 1)
time.Sleep(time.Millisecond)

}


}()

}

// 以相同方法启动10个写操作
for w := 0; w < 10; w++{
go func() {
for{
write := writeOp{
key: rand.Intn(5),
value: rand.Intn(100),
resp: make(chan bool),

}

writes <- write
<- write.resp
atomic.AddUint64(&writeOps, 1)
time.Sleep(time.Millisecond)

}


}()

}

// 让协程们跑1s(阻塞主协程)
time.Sleep(time.Second)
readOpsFinal := atomic.LoadUint64(&readOps)
fmt.Println("readOps", readOpsFinal)
writeOpsFinal := atomic.LoadUint64(&writeOps)
fmt.Println("writeOps", writeOpsFinal)


}

运行结果:

image-20210908171921079

排序

1.自定义排序

这里自定义按字符串长度排序

同过实现go提供的sort.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
// 自定义函数实现自定义排序
type byLength []string

// 实现byLength结构体的对象是实现了sort.Interface接口的所有方法
// 这样我们就可以使用 sort 包的通用 Sort 方法了
func (s byLength) Len() int {
return len(s)

}

func (s byLength) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

// less方法控制实际的自定义排序逻辑
func (s byLength) Less(i, j int) bool {
// 按长度排序字符串
return len(s[i]) < len(s[j])

}

func Sorting() {
asoul := []string{"Ava", "Bella", "Eileen","Carol", "Diana"}
// 将asoul强转换为byLength类型
// Sort调用实现结构的对象
sort.Sort(byLength(asoul))
fmt.Println(asoul)
}

运行结果:

image-20210908172335154

JSON

1.基本概念

JSON的编码解码即序列化与反序列化

我们常用byte和string作为数据和json表示形式的中介

编码过程:go的其他数据类型 -> byte(字节数组) -> json

解码过程:json -> byte(字节数组) -> go的其他数据类型(map[string]interface{} / 自定义类型)

2.构造结构体

1
2
3
4
5
6
7
// 构造json类型的结构体
// 必须以大写字母开头的字段才是可导出的。
type res struct {
Page int
Fruits []string

}

3.编码

将Go的数据转换为JSON,go的数据在编码后是byte[]类型,所以要转换成string类型才能打印出字符串

(1)map类型编码
1
2
3
4
// map类型json编码
mapD := map[string]int{"apple":5, "lettuce":7}
mapB, _ := json.Marshal(mapD)
fmt.Println(string(mapB))

运行结果:

image-20210908173628386

(2)自定义类型编码
1
2
3
4
5
6
7
8
9
10
11
// 结构体实例化
// 结构体实例化赋值时,可以赋予地址也可以赋予值(得到一样的结果)
// 但是赋予地址可以减少系统开销
res1D := &res{
Page: 1,
Fruits: []string{"apple", "peach", "pear"},
}

// json可自动编码自定义类型,编码的输出只有可导出的字段
res1B, _ := json.Marshal(res1D)
fmt.Println(string(res1B))

运行结果:

image-20210908173448648

4.解码

将JSON转换为Go语言可以读取的数据类型,

(1)map类型接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 解码过程
// 构造json
byt := []byte(`{"num":6.13, "strs":["a", "b"]}`)

// 提供Json包存放解码数据的变量(键为string,值为任意的map)
var dat map[string]interface{}

// 对数据进行解码
// if语句进行错误检查
if err := json.Unmarshal(byt, &dat); err != nil{
panic(err)
}
// 输出解码后的dat
fmt.Println(dat)

// 为了使用map中的值,我们需要进行恰当的类型转换
num := dat["num"].(float64)
fmt.Println(num)
// 访问嵌套的值
strs := dat["strs"].([]interface{})
str1 := strs[1].(string)
fmt.Println(str1)

运行结果:

image-20210908174316707

(2)自定义数据类型接收
1
2
3
4
5
6
7
8
9
10
// 将json解码为自定义数据类型(增加类型安全性),推荐的解码方式
// ``可以定义字符串,且其内可以有双引号""
str := `{"page": 1, "fruits": ["apple", "peach"]}`
fmt.Println(reflect.TypeOf(str))

res := res{}
// 将str转换为byte数组类型,后将其解码放在res类型的结构体中
json.Unmarshal([]byte(str), &res)
fmt.Println(res)
fmt.Println(res.Fruits[0])

运行结果:

image-20210908174428706

(3)Encode接收
1
2
3
4
5
6
// 我们还可以将os.Stdout 一样直接将 JSON 编码流传输到 os.Writer 甚至 HTTP 响应体
// 创建一个将数据写入*Encoder(可以接收信息)
enc := json.NewEncoder(os.Stdout)
d := map[string]int{"apple": 5, "lettuce": 7}
// 将json编码写入输出流输出
enc.Encode(d)

运行结果:

image-20210908174505894

单元测试

1.单元测试命名规则

  • 文件名必须以xx_test.go命名
  • 方法必须是Test开头
  • 方法参数必须 t *testing.T
  • 测试例与被测试对象要放在一个包中
  • 使用go test执行单元测试(idea可以直接运行测试例)

2.被测对象

1
2
3
4
5
6
7
8
9
// IntMin 主程序
func IntMin(a, b int) int {
if a < b{
return a
} else {
return b
}

}

3.一般测试例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TestIntMinBasic 测试程序
func TestIntMinBasic(t *testing.T) {

ans := IntMin(2, -2)
if ans != -2{
// t.Error*会报告测试失败的信息,然后立即终止测试
// t.Fail*会报告测试失败的信息,然后立即终止测试
t.Errorf("IntMin(2, -2) = %d; want -2", ans)
} else{

fmt.Println(ans)

}

}

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
// TestIntMinTableDriven 单元测试可以重复,所以会经常使用表驱动风格编写单元测试
// 表中列出了输入数据,预期输出,使用循环,遍历并执行测试逻辑
func TestIntMinTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{0, 1, 0},
{1, 0, 0},
{2, -2, -2},
{0, -1, -1},
{-1, 0, -1},
}

for _, tt := range tests {

testname := fmt.Sprintf("%d, %d", tt.a, tt.b)
// t.Run运行一个子测试,每一个子测试对应表中一行数据
t.Run(testname, func(t *testing.T){
ans := IntMin(tt.a, tt.b)
if ans != tt.want{
t.Errorf("got %d, want %d", ans, tt.want)
}

})

}

HTTP客户端

1.GET请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// go向服务端发送GET请求

resp1, err := http.Get("https://search.bilibili.com/")
// 捕捉错误
if err != nil{
panic(err)

}

// 延迟关闭请求服务
defer resp1.Body.Close()

fmt.Println(resp1)
// 打印返回的请求状态
fmt.Println("Response status", resp1.Status)

// 打印响应的内容
// 使用ioutil读取数据(读取返回的是字节数组)
body, err := ioutil.ReadAll(resp1.Body)
if err != nil {
fmt.Println(err)

}
fmt.Println(string(body))

2.带参数GET请求

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
// go向服务端发送带参数GET请求
apiurl := "https://search.bilibili.com/all"
// 构造get请求表单
data := url.Values{}
data.Set("keyword", "Autovy")
data.Set("from_source", "webtop_search")

// 结合参数构造请求结构体
u, err := url.ParseRequestURI(apiurl)
if err != nil {
fmt.Println(err)
}

// url编码
u.RawQuery = data.Encode()
// 生成带参数url
fmt.Println(u.String())

// 进行http请求(下面的步骤与一般的get请求一致)
// 其实也可以把带参数的url直接进行请求😅
resp2, err := http.Get(u.String())

if err != nil {
fmt.Println("post failed, err:%v\n", err)
return
}
defer resp2.Body.Close()

b, err := ioutil.ReadAll(resp2.Body)
if err != nil {
fmt.Println("get resp failed,err:%v\n", err)
return
}
fmt.Println(string(b))

3.POST请求

(1)发送from-data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 构造表单
data2 := url.Values{
"name": {"Autovy"},
"occupation": {"programmar"},
}

// 发送表单
resp, err := http.PostForm("https://httpbin.org/post", data2)

// 捕捉异常
if err != nil {
panic(err)
}

// 解码返回的信息
var res map[string]interface{}
json.NewDecoder(resp.Body).Decode(&res)
fmt.Println(res["form"])

运行结果:

image-20210908201313448

(2)发送json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 构造json数据
values := map[string]string{"name": "Ava", "occupation": "Gamer"}
json_data, err := json.Marshal(values)

if err != nil {
panic(err)
}

// post发送json数据
resp3, err := http.Post("https://httpbin.org/post", "application/json", bytes.NewBuffer(json_data))

if err != nil {
panic(err)
}

// 解码返回信息
var res2 map[string]interface{}
json.NewDecoder(resp3.Body).Decode(&res2)
fmt.Println(res2["json"])

运行结果:

image-20210908201329065

HTTP服务端

服务端构造处理请求的函数handler

handler 函数有两个参数,http.ResponseWriter 和 http.Request

1.普通服务

(1)处理http请求
1
2
3
4
5
6
7
8
9
//  我们需要读取的 HTTP 请求 header 中的所有内容,并将他们输出至 response body
func headers(w http.ResponseWriter, req *http.Request) {

for name, headers := range req.Header {
for _, h := range headers {
fmt.Fprintf(w, "%v: %v\n", name, h)
}
}
}
(2)函数注册到路由
1
2
3
4
http.HandleFunc("/headers", headers)

//我们调用 ListenAndServe 并带上端口和 handler。nil表示使用我们刚刚设置的默认路由器
http.ListenAndServe(":8090", nil)

运行结果:

image-20210908193457887

2.接受GET请求参数

(1)处理get请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 处理带参数的get请求
func getHandler(w http.ResponseWriter, r *http.Request) {
// 延迟关闭响应
defer r.Body.Close()
// 接受url的参数
data := r.URL.Query()

// 输出内容到页面
answer := `{"status" : "ok"}`
w.Write([]byte(answer))

// 获取请求链接中的参数
fmt.Fprintln( w ,data.Get("name"))
fmt.Fprintln( w ,data.Get("age"))

}
(2)函数注册到路由
1
2
3
4
http.HandleFunc("/get", getHandler)

//我们调用 ListenAndServe 并带上端口和 handler。nil表示使用我们刚刚设置的默认路由器
http.ListenAndServe(":8090", nil)

运行结果:

image-20210908194148189

3.接受POST请求参数

(1)处理post请求函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// post请求
func postHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
// 请求类型为application/x-www-form-urlencoded时解析form数据
r.ParseForm()
// 打印form数据
fmt.Println(r.PostForm)
fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))

// 2. 请求类型是application/json时从r.Body读取数据
b, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
return

}

fmt.Fprintln(w, string(b))

}
(2)函数注册到路由
1
2
3
4
http.HandleFunc("/post", postHandler)

//我们调用 ListenAndServe 并带上端口和 handler。nil表示使用我们刚刚设置的默认路由器
http.ListenAndServe(":8090", nil)
(3)请求类型为application/x-www-form-urlencoded

image-20210907073940923

运行结果:

image-20210907074031817

(3)请求类型为application/json

服务端可以进行反序列化操作得到json内的数据

image-20210907074448963

结束

cute

快乐生活,快乐工作,快乐学习

os.Exit(1)