Golang开发
Golang
文章目录
- Golang
- 预备技术
- 一、算法与数据结构
- 第1章:基础算法
- 第2章:数据结构
- 第3章:搜索与图论
- 第4章:数论
- 第5章:动态规划
- 第6章:贪心
- 第7章:算法竞赛入门
- 二、Linux操作系统与Shell编程
- 三、计算机操作系统
- 四、计算机组成原理
- 五、计算机网络
- 六、Docker:云原生核心技术之一
- Go 开发编程实战
- 一、基本语法
- 第1章:走进Golang
- 第2章:变量与数据类型
- 第3章:运算符
- 第4章:流程控制
- 第5章:函数
- 第6章:错误处理
- 第7章:数组
- 第8章:切片
- 第9章:映射
- 第10章:面向对象
- 第11章:文件的操作
- 第12章:协程和管道
- 1. 程序、进程、线程、协程
- (1)程序 (program)
- (2)进程 (process)
- (3)线程 (thread)
- (4)协程 (goroutine)
- 2. 协程入门
- (1)案例
- 3. 主死从随
- (1)主死从随
- 4. 启动多个协程
- (1)案例
- 5. 使用WaitGroup控制协程退出
- (1)WaitGroup的作用
- (2)主要方法
- (3)案例代码
- 6. 多个协程操纵统一数据案例(互斥锁)
- (1)案例
- 7. 读写锁的使用
- (1)互斥锁
- (2)读写锁
- (3)案例
- 8. 管道介绍
- (1)管道(channel)特质介绍
- 9. 管道入门案例
- (1)管道的定义
- (2)案例
- 10. 管道的关闭
- (1)管道的关闭
- (2)案例
- 11. 管道的遍历
- (1)管道的遍历
- 12. 协程和管道协同工作的案例
- 13. 声明只读只写管道
- 14. 管道的阻塞
- 15. select功能
- 16. defer + recover机制处理错误
- 第13章:网络编程
- 第14章:反射
- 二、Golang进阶 - 网络通信
- 三、Golang进阶 - 并发编程
- Go Web开发之前端技术实战
- 一、Go Web前置 - Go Module
- 二、Go Web前置 - HTML
- 三、Go Web前置-CSS
- 四、Go Web前置-JS
- 五、Go Web前置-JQuery
- 六、前端技术之Vue框架
- GORM及数据库
- 一、GORM前置:MySQL
- 二、MySQL性能调优与架构设计
- 三、GORM
- Go开发应用中间件
- 一、Redis_高效的NoSQL数据库
- 二、Redis缓存数据库进阶
- 三、Redis之go编程实战
- 四、消息中间件-Kafka实战
- 五、Kafka之go编程实战
- 六、RocketMQ基础实战版
- 七、NoSQL-MongoDB实战
- 八、分布式文件存储系统Minio
- 全文检索 ES
- 一、Elasticsearch核心知识篇
- Go Web开发之企业级框架
- 一、Web框架
- 二、Gin深入实战
- 三、Beego框架开发实战
- 四、微服务架构
- 五、Go框架开发
- 六、Kong入门与实战
- 七、Logrus日志
- 企业级项目实战
预备技术
一、算法与数据结构
第1章:基础算法
https://blog.csdn.net/m0_52806260/article/details/125519736
第2章:数据结构
https://blog.csdn.net/m0_52806260/article/details/125592926
第3章:搜索与图论
https://blog.csdn.net/m0_52806260/article/details/125771347
第4章:数论
https://blog.csdn.net/m0_52806260/article/details/125843269
第5章:动态规划
https://blog.csdn.net/m0_52806260/article/details/125459689
第6章:贪心
https://blog.csdn.net/m0_52806260/article/details/125987376
第7章:算法竞赛入门
https://blog.csdn.net/m0_52806260/article/details/146226657
二、Linux操作系统与Shell编程
https://blog.csdn.net/m0_52806260/article/details/126068791
三、计算机操作系统
四、计算机组成原理
五、计算机网络
六、Docker:云原生核心技术之一
Go 开发编程实战
一、基本语法
第1章:走进Golang
第2章:变量与数据类型
第3章:运算符
第4章:流程控制
第5章:函数
第6章:错误处理
第7章:数组
第8章:切片
第9章:映射
第10章:面向对象
第11章:文件的操作
第12章:协程和管道
1. 程序、进程、线程、协程
(1)程序 (program)
- 程序是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。 (程序是静态的)
(2)进程 (process)
- 是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程
(3)线程 (thread)
- 进程可进一步细化为线程, 是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
(4)协程 (goroutine)
- 又称为微线程,纤程,协程是一种用户态的轻量级线程
- 作用:在执行A函数的时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),注意这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行(协程的本质是个单线程)
- 对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算。在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而会更多的将cpu的执行权限分配给我们的线程(注意:线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级)
2. 协程入门
(1)案例
- 请编写一个程序,完成如下功能:
(1)在主线程中,开启一个goroutine,该goroutine每隔1秒输出"hello golang"
(2)在主线程中也每隔一秒输出"good bye golang",输出10次后,退出程序
(3)要求主线程和goroutine同时执行 - 实现代码
package main
import (
"fmt"
"time"
)
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("hello golang")
time.Sleep(1 * time.Second) // 阻塞一秒
}
}
func main() {
go test() // 开启一个协程:go + 函数名
// 主线程
for i := 1; i <= 10; i++ {
fmt.Println("good bye golang")
time.Sleep(1 * time.Second) // 阻塞一秒
}
return
}
- 运行结果
- 执行流程示意图
3. 主死从随
(1)主死从随
- 如果主线程退出了,则协程即使还没有执行完毕,也会退出
- 当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务
- 验证代码
package main
import (
"fmt"
"strconv"
"time"
)
func test() {
for i := 1; i <= 1000; i++ {
fmt.Println("hello golang" + strconv.Itoa(i))
time.Sleep(1 * time.Second) // 阻塞一秒
}
}
func main() {
go test() // 开启一个协程:go + 函数名
// 主线程
for i := 1; i <= 10; i++ {
fmt.Println("good bye golang" + strconv.Itoa(i))
time.Sleep(1 * time.Second) // 阻塞一秒
}
return
}
- 执行结果
4. 启动多个协程
(1)案例
package main
import (
"fmt"
"time"
)
func main() {
// 匿名函数 + 外部变量 = 闭包
for i := 1; i <= 5; i++ {
go func(n int) {
fmt.Printf("这是%d号协程\n", n)
}(i)
}
time.Sleep(2 * time.Second) // 避免主死从随
}
5. 使用WaitGroup控制协程退出
(1)WaitGroup的作用
WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。—> 解决主线程在子协程结束后自动结束
(2)主要方法
(3)案例代码
- Add/Done/Wait
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
// 启动五个协程
for i := 1; i <= 5; i++ {
wg.Add(1) // 协程开始
go func(n int) {
fmt.Printf("这是%d号协程\n", n)
wg.Done() // 协程结束
}(i)
}
wg.Wait() // 阻塞等待协程全部完成
}
- 如果忘记计数器减1,可以提前defer:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
// 启动五个协程
for i := 1; i <= 5; i++ {
wg.Add(1) // 协程开始
go func(n int) {
defer wg.Done() // 提前写defer,防止忘记计数器-1
fmt.Printf("这是%d号协程\n", n)
/*
其余代码
其余代码
其余代码
其余代码
其余代码
*/
}(i)
}
wg.Wait() // 阻塞等待协程全部完成
}
- 可以最开始在知道协程次数的情况下先Add操作:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(5) // 预先设置协程数
// 启动五个协程
for i := 1; i <= 5; i++ {
go func(n int) {
defer wg.Done() // 提前写defer,防止忘记计数器-1
fmt.Printf("这是%d号协程\n", n)
/*
其余代码
其余代码
其余代码
其余代码
其余代码
*/
}(i)
}
wg.Wait() // 阻塞等待协程全部完成
}
注意:Add中加入的数字和协程的次数一定要保持一致
6. 多个协程操纵统一数据案例(互斥锁)
(1)案例
package main
import (
"fmt"
"sync"
)
var totalNum int
var wg sync.WaitGroup
func add() {
defer wg.Done()
for i := 0; i < 100000; i++ {
totalNum++
}
}
func sub() {
defer wg.Done()
for i := 0; i < 100000; i++ {
totalNum--
}
}
func main() {
wg.Add(2) // 预先设置协程数
// 启动五个协程
go add()
go sub()
wg.Wait() // 阻塞等待协程全部完成s
fmt.Println(totalNum)
}
- 结果:在理论上,这个totalNum结果应该是0 ,无论协程怎么交替执行,最终想象的结果就是0,但是事实上:不是
-
问题出现的原因:(图解为其中一种可能性)
-
解决问题:确保一个机制:一个协程在执行逻辑的时候另外的协程不执行 ----> 锁的机制—> 加入互斥锁
- 代码实现
package main
import (
"fmt"
"sync"
)
var totalNum int
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
defer wg.Done()
for i := 0; i < 100000; i++ {
lock.Lock() // 上锁
totalNum++
lock.Unlock() // 解锁
}
}
func sub() {
defer wg.Done()
for i := 0; i < 100000; i++ {
lock.Lock() // 上锁
totalNum--
lock.Unlock() // 解锁
}
}
func main() {
wg.Add(2) // 预先设置协程数
// 启动五个协程
go add()
go sub()
wg.Wait() // 阻塞等待协程全部完成s
fmt.Println(totalNum)
}
7. 读写锁的使用
golang中sync包实现了两种锁 Mutex (互斥锁)和 RWMutex(读写锁)
(1)互斥锁
- 其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别
- 性能、效率相对来说比较低,因为大部分场景都是读多写少
(2)读写锁
- RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景.
- 在读的时候,数据之间不产生影响,写和读之间才会产生影响
(3)案例
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup // 只定义无需赋值
// 加入读写锁:
var lock sync.RWMutex
func read() {
defer wg.Done()
lock.RLock() //如果只是读数据,那么这个锁不产生影响,但是读写同时发生的时候,就会有影响
fmt.Println("开始读取数据")
time.Sleep(1 * time.Second)
fmt.Println("读取数据成功")
lock.RUnlock()
}
func write() {
defer wg.Done()
lock.Lock()
fmt.Println("开始修改数据")
time.Sleep(time.Second * 5)
fmt.Println("修改数据成功")
lock.Unlock()
}
func main() {
wg.Add(6)
// 启动协程 ---> 场合:读多写少
for i := 0; i <= 5; i++ {
go read()
}
go write()
wg.Wait()
}
- 运行结果
8. 管道介绍
(1)管道(channel)特质介绍
- 管道本质就是一个数据结构——队列
- 数据是先进先出
- 自身线程安全,多协程访问时,不需要加锁,channel本身就是线程安全的
- 管道有类型的,一个string的管道只能存放string类型数据
9. 管道入门案例
(1)管道的定义
var 变量名 chan 数据类型
- chan管道关键字
- 数据类型指的是管道的类型,里面放入数据的类型,管道是有类型的,int类型的管道只能写入整数int
- 管道是引用类型,必须初始化才能写入数据,即make后才能使用
(2)案例
package main
import "fmt"
func main() {
// 定义管道,声明管道 ---> 定义一个int类型的管道
var intChan chan int
// 通过make进行初始化:管道可以存放3个int类型的数据
intChan = make(chan int, 3)
// 证明管道是引用类型
fmt.Printf("intChan:%v\n", intChan)
// 向管道存放数据
intChan <- 10
num := 20
intChan <- num
intChan <- 40
// 注意:不能存放大于容量的数据
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
// 输出管道的长度
fmt.Printf("管道的实际长度:%v,管道的容量是:%v\n", len(intChan), cap(intChan))
fmt.Println(num1)
fmt.Println(num2)
fmt.Println(num3)
// 注意:在没有使用协程的情况下,如果管道的数据已经全部取出,那么再取就会报错:
//num4 := <-intChan
//fmt.Println(num4)
}
10. 管道的关闭
(1)管道的关闭
- 使用内置函数
close
可以关闭管道,当管道关闭后,就不能再向管道写数据了,但是仍然可以从该管道读取数据。
(2)案例
package main
import "fmt"
func main() {
// 定义管道,声明管道 ---> 定义一个int类型的管道
var intChan chan int
// 通过make进行初始化:管道可以存放3个int类型的数据
intChan = make(chan int, 3)
// 在管道中存放数据:
intChan <- 10
intChan <- 20
// 关闭管道:
close(intChan)
// 再次写入数据:--->报错
// intChan<- 30
// 当管道关闭后,读取数据是可以的:
num := <-intChan
fmt.Println(num)
}
11. 管道的遍历
(1)管道的遍历
- 管道支持
for-range
的方式进行遍历,请注意两个细节
(1)在遍历时,如果管道没有关闭,则会出现deadlock
的错误
(2)在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package main
import (
"fmt"
)
func main() {
// 定义管道 、 声明管道
var intChan chan int
// 通过make初始化:管道可以存放3个int类型的数据
intChan = make(chan int, 100)
for i := 0; i < 100; i++ {
intChan <- i
}
// 在遍历前,如果没有关闭管道,就会出现deadlock的错误
// 所以我们在遍历前要进行管道的关闭
close(intChan)
// 遍历:for-range
for v := range intChan {
fmt.Println("value = ", v)
}
}