Go语言进阶 - 工程进阶 | 青训营笔记

90 阅读4分钟

这是我参与「第五届青训营」伴学笔记的第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指令工具管理依赖包

  • 三要素:
    1. 配置文件,描述依赖 go.mod
    2. 中心仓库管理依赖库 Proxy
    3. 本地工具 go get/mod
  • 主要改动:
    1. GOMODULE模式下所有依赖的包存放在$GOPATH/pkg/mod目录下
    2. 项目中需要有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会做两件事:

  1. 从远程下载需要用到的包
  2. 执行go install安装

拉取的过程总共分为了三大步,分别是 finding(发现)、downloading(下载)以及 extracting(提取), 并且在拉取信息上一共分为了三段内容:
在这里插入图片描述

image.png

go install xxx(安装xxx二进制可执行文件 )

go install 是建立在 GOPATH 环境变量上的,无法在独立的目录里使用 go install。

go install 会生成可执行文件直接放到bin目录下

项目实践

需求设计

需求描述

社区话题页面

  • 展示话题(标题,文字描述)和回帖列表
  • 暂不考虑前端页面实现,仅仅是先一个本地web服务
  • 话题和回帖数据用文件存储

需求用例

image.png

ER图

E6EC0889B36EEF0D3983CB3D9D0F6EB7.png

分层结构

  • 数据层(Respository):数据Model,外部数据的增删改查
  • 逻辑层(Service):业务Entity,处理核心业务逻辑输出
  • 视图层(Controller):视图view,处理和外部的交互逻辑

组件工具

gin高性能go web框架

一个流行的golang Web框架

点击阅读快速入门文档

代码开发

点击查看相关demo

测试运行

image.png