这是我参与「第五届青训营」伴学笔记的第2天
协程与通道
协程
程序、进程、线程和协程
- 程序:为了完成特定任务,用某种语言编写的一组指令的集合,是一段静态的代码
- 进程:是程序的一次执行过程
- 线程:操作系统调度(CPU调度)执行的最小单位;进程可进一步细化为线程,是一个程序内部的一条执行路径;一个进程至少有一个线程
- 协程:是一种用户态的轻量级线程,调度完全由用户控制;一个线程可以跑多个协程
有关线程和协程的区别: 线程是内核态,启动线程需要调用内核空间,于是导致线程之间的切换会比较重量级,消耗资源多; 而协程是用户态,启动线程不需要调用内核空间,消耗资源比较低,效率比较高
使用WaitGroup控制协程
主要方法:
- Add(delta int) 计数器+delta
- Done() 计数器-1
- Wait() 阻塞直到计数器为0
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()
fmt.Println(n)
}(i)
}
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
通道
通道的特质:
- 本质是一个数据结构--->队列(先进先出)
- 自身线程安全,多协程访问时,不需要加锁,channel本身就是线程安全的
- 通道有类型,只能存放同类型数据
通道的使用:
**var 变量名 chan 数据类型**
// 管道是引用类型,必须初始化才能写入数据,即make后才能使用
// 无缓冲通道 make(chan int) ----> 同步通道
// 有缓冲通道 make(chan int, 2) ----> 可以存放多少个数据,满了会堵塞
package main
import(
"fmt"
)
func main(){
//定义通道 、 声明通道 ---> 定义一个int类型的通道
var intChan chan int
//通过make初始化:通道可以存放3个int类型的数据
intChan = make(chan int,3)
//证明通道是引用类型:
fmt.Printf("intChan的值:%v",intChan) // 0xc000112080
//向通道存放数据:
intChan<- 10
num := 20
intChan<- num
intChan<- 40
//注意:不能存放大于容量的数据:
//intChan<- 80
//在通道中读取数据:
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
fmt.Println(num1)
fmt.Println(num2)
fmt.Println(num3)
//注意:在没有使用协程的情况下,如果通道的数据已经全部取出,那么再取就会报错:
num4 := <-intChan
fmt.Println(num4)
//输出通道的长度:
fmt.Printf("通道的实际长度:%v,通道的容量是:%v",len(intChan),cap(intChan))
}
**通道关闭使用close,此后再写入数据会报错,但是可以读取数据**
package main
import(
"fmt"
)
func main(){
//定义通道 、 声明通道
var intChan chan int
//通过make初始化:通道可以存放3个int类型的数据
intChan = make(chan int,3)
//在通道中存放数据:
intChan<- 10
intChan<- 20
//关闭通道:
close(intChan)
//在遍历前,如果没有关闭通道,就会出现deadlock的错误
for v:= range intChan{
fmt.Println(v)
}
//再次写入数据:--->报错
//intChan<- 30
//当通道关闭后,读取数据是可以的:
num := <- intChan
fmt.Println(num)
}
**通道可以声明为只读或者只写性质**
package main
import(
"fmt"
)
func main(){
//默认情况下,通道是双向的--》可读可写:
//var intChan1 chan int
//声明为只写:
var intChan2 chan<- int // 通道具备<- 只写性质
intChan2 = make(chan int,3)
intChan2<- 20
//num := <-intChan2 报错
fmt.Println("intChan2:",intChan2)
//声明为只读:
var intChan3 <-chan int// 通道具备<- 只读性质
if intChan3 != nil {
num1 := <-intChan3
fmt.Println("num1:",num1)
}
//intChan3<- 30 报错
}
select功能:解决多个通道的选择问题,也可以叫做多路复用,可以从多个通道中随机公平地选择一个来执行 (case后面必须进行的是io操作,不能是等值,随机去选择一个io操作;default防止select被阻塞住)
package main
import (
"fmt"
"time"
)
func main() {
//定义一个int管道:
intChan := make(chan int, 1)
go func() {
//time.Sleep(time.Second * 15)
intChan <- 10
}()
//定义一个string管道:
stringChan := make(chan string, 1)
go func() {
//time.Sleep(time.Second * 12)
stringChan <- "msbgolang"
}()
//fmt.Println(<-intChan)//本身取数据就是阻塞的
time.Sleep(time.Second * 5)
select {
case v := <-intChan:
fmt.Println("intChan:", v)
case v := <-stringChan:
fmt.Println("stringChan:", v)
default:
fmt.Println("防止select被阻塞")
}
}
协程与通道协同使用
package main
import(
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup //只定义无需赋值
//写:
func writeData(intChan chan int){
defer wg.Done()
for i := 1;i <= 50;i++{
intChan<- i
fmt.Println("写入的数据为:",i)
time.Sleep(time.Second)
}
//管道关闭:
close(intChan)
}
//读:
func readData(intChan chan int){
defer wg.Done()
//遍历:
for v := range intChan{
fmt.Println("读取的数据为:",v)
time.Sleep(time.Second)
}
}
func main(){//主线程
//写协程和读协程共同操作同一个管道-》定义管道:
intChan := make(chan int,50)
wg.Add(2)
//开启读和写的协程:
go writeData(intChan)
go readData(intChan)
//主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
}
依赖管理
依赖管理演进
GOPATH模式
- $GOPATH :项目根路径
- src :项⽬源代码
- bin :可执行程序,项目编译的二进制文件
- pkg :项目编译的中间产物,加速编译 运行方式:所有工程代码要求放在GOPATH/src目录下,项目代码直接依赖src下的代码
缺点:没有版本控制的概念;并且项目不在$GOPATH/src 下就不能编译
GO Vender模式
解决 GOPATH模式 所有项目都在$GOPATH/src目录的问题,可以随处可以创建项目,不用扎堆 src 目录下
- 方案原理:本地化构建
在每个项目下都创建一个 vendor 目录,每个项目所需的依赖都只会下载到自己vendor目录下.
在使用包时,会先从当前项目下的 vendor 目录查找,然后GOPATH 中查找,都没找到最后在 GOROOT中查找。 - 缺点
放弃了依赖重用,使得冗余度上升
Go Module模式
通过go.mod文件管理依赖包版本
通过go get/go mod指令工具管理依赖包
- 三要素:
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
- 主要改动:
- GOMODULE模式下所有依赖的包存放在$GOPATH/pkg/mod目录下
- 项目中需要有go.mod文件,来应用$GOPATH/pkg/mod
mod相关环境变量
# Modules 开关
GO111MODULE="auto"
# Go 模块代理(脱离VCS版本控制方式,直接通过镜像站点来拉取)
GOPROXY="https://proxy.golang.org,direct" # 国内无法访问
# 保证拉取到的模块版本数据未经过篡改
GOSUMDB="sum.golang.org" # 国内无法访问
# 私有模块配置(用于Go 模块代理无法访问到的地方,如私有库)
GONOPROXY=""
GONOSUMDB=""
GOPRIVATE=""
12345678910
go.mod 文件
启用了 Go modules 的项目,初始化项目时,会生成一个 go.mod 文件。描述了当前项目(也就是当前模块)的元信息
# module:用于定义当前项目的模块路径。
module example/project
# go:用于标识当前模块的 Go 语言版本,值为初始化模块时的版本,目前来看还只是个标识作用。
go 1.16
# require:用于设置一个特定的模块版本。
require (
example.com/apple v0.1.2
example.com/banana v1.2.3
example.com/banana/v2 v2.3.4
example.com/pear // indirect # indirect 标识表示该模块为间接依赖
example.com/strawberry // incompatible
)
# exclude:排除一个特定的模块版本。
exclude example.com/banana v1.2.4
# replace:用于将一个模块版本替换为另外一个模块版本。
replace example.com/banana => example.com/fish
1234567891011121314151617181920
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 download 拉取模块,其拉取的结果缓存在 $GOPATH/pkg/mod和 $GOPATH/pkg/sumdb 目录下
两个命令
go get xxx(下载xxx第三方依赖包并安装)
go get会做两件事:
- 从远程下载需要用到的包
- 执行
go install安装
拉取的过程总共分为了三大步,分别是 finding(发现)、downloading(下载)以及 extracting(提取), 并且在拉取信息上一共分为了三段内容:
go install xxx(安装xxx二进制可执行文件 )
go install 是建立在 GOPATH 环境变量上的,无法在独立的目录里使用 go install。
go install 会生成可执行文件直接放到bin目录下
项目实践
需求设计
需求描述
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅是先一个本地web服务
- 话题和回帖数据用文件存储
需求用例
ER图
分层结构
- 数据层(Respository):数据Model,外部数据的增删改查
- 逻辑层(Service):业务Entity,处理核心业务逻辑输出
- 视图层(Controller):视图view,处理和外部的交互逻辑
组件工具
gin高性能go web框架
一个流行的golang Web框架