Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • JavaSE

  • JavaEE

  • Linux

  • MySQL

  • NoSQL

  • Python

  • Python模块

  • 机器学习

  • 设计模式

  • 传智健康

  • 畅购商城

  • 博客项目

  • JVM

  • JUC

  • Golang

    • Golang

      • Go 基础
      • Go 项目案例
      • Go 进阶
        • 并发编程
          • 并发介绍
          • 进程和线程
          • 并发和并行
          • 协程和线程
          • Goroutine
          • Goroutine
          • 使用Goroutine
          • 启动多个Goroutine
          • Channel
          • 无缓冲的通道
          • 有缓冲的通道
          • 案例
          • 并发安全和锁
          • Sync
          • WaitGroup
        • 依赖管理
          • GOPATH
          • Go Vendor
          • Go Moudle
          • 依赖配置- go.mod
          • 依赖配置- version
          • 依赖配置- indirect
          • 依赖配置- incompatible
          • 依赖分发
          • go get
          • go mod
        • 测试
          • 单元测试
          • 单元测试 - 规则
          • 单元测试 - 例子
          • 单元测试 - assert
          • 单元测试 - 覆盖率
          • 单元测试 - Tips
          • 单元测试 - 依赖
          • 基准测试
        • 项目实践
          • 需求描述
          • 需求用例
          • ER图
          • 分层结构
          • 组件工具
          • Repository
          • Repository-index
          • Repository - 查询
          • Service
          • Controller
          • Router
          • 运行测试
      • 性能调优
    • gRPC

  • Kubernetes

  • 硅谷课堂

  • C

  • 源码

  • 神领物流

  • RocketMQ

  • 短链平台

  • 后端
  • Golang
  • Golang
Iekr
2023-01-21
目录

Go 进阶

# Go 进阶

# 并发编程

# 并发介绍

# 进程和线程

  • 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
  • 线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
  • 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

# 并发和并行

  • 多线程程序在一个核的 cpu 上运行,就是并发。
  • 多线程程序在多个核的 cpu 上运行,就是并行。

并发

image-20230121154040733

并行

image-20230121154049481

# 协程和线程

  • 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
  • 线程:一个线程上可以跑多个协程,协程是轻量级的线程。

# Goroutine

goroutine 只是由官方实现的超级 "线程池"。

每个实例 4~5KB 的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是 go 高并发的根本原因。

并发主要由切换时间片来实现 "同时" 运行,并行则是直接利用多核实现多线程的运行,go 可以设置使用核数,以发挥多核计算机的能力。

goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

# Goroutine

在 java/c++ 中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换。

Go 语言中的 goroutine 就是这样一种机制,goroutine 的概念类似于线程,但 goroutine 是由 Go 的运行时(runtime)调度和管理的。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。Go 语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

image-20230121154102089

  • 协程:用户态,轻量级线程,栈 MB 级别。
  • 线程:内核态,线程跑多个协程,栈 KB 级别。

# 使用 Goroutine

Go 语言中使用 goroutine 非常简单,只需要在调用函数的时候在前面加上 go 关键字,就可以为一个函数创建一个 goroutine。

一个 goroutine 必定对应一个函数,可以创建多个 goroutine 去执行相同的函数。

# 启动多个 Goroutine

func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("Hello Goroutine!", i)
}
func main() {

    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}
1
2
3
4
5
6
7
8
9
10
11
12

# Channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go 语言的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

image-20230121154114213

如果说 goroutine 是 Go 程序并发的执行体,channel 就是它们之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

image-20230121154138761

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型。

# 无缓冲的通道

image-20230116114338640

无缓冲的通道又称为阻塞的通道,无缓冲的通道只有在有人接收值的时候才能发送值,如果没有则会出现 deadlock 错误。

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}
1
2
3
4
5
6
7
8
9
10

无缓冲通道上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时值才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方的 goroutine 将阻塞,直到另一个 goroutine 在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道。

# 有缓冲的通道

image-20230117121943822

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}
1
2
3
4
5

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。申请多少容量通道中就能缓冲多少个数据。

# 案例

我们申请一个无缓冲和一个有缓冲的通道

func main() {
    srs := make(chan int)
    dest := make(chan int, 3)
    go func() {
        defer close(src)
        //往通道的另一端发送数据
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
    go func() {
        defer close(dest)
        //接受无缓冲的通道的数据,做平方后放入有缓冲的通道中
        for i := range src {
            dest <- i * i
        }
    }()
    for i := range dest {
        println(i)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

输出结果

0
1
4
9
16
25
36
49
64
81
1
2
3
4
5
6
7
8
9
10

# 并发安全和锁

在 Go 代码中可能会存在多个 goroutine 同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)

下面例子我们通过对 x 进行 2000 次 + 1 操作,开启 5 个协程并发执行,分别使用锁和不使用锁的情况。

package main

import (
	"sync"
	"time"
)

var (
	x    int64
	lock sync.Mutex
)

func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock()
		x += 1
		lock.Unlock()
	}
}

func addWithoutLock() {
	for i := 0; i < 2000; i++ {
		x += 1
	}
}

func Add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("WithoutLock:", x)
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("WithLock:", x)
}

func main() {
	Add()
}
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

输出

WithoutLock: 8094
WithLock: 10000
1
2

可以看到不加锁的情况下并发不安全,这五个 goroutine 在访问和修改 x 变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

# Sync

# WaitGroup

在代码中生硬的使用 time.Sleep 肯定是不合适的,Go 语言中可以使用 sync.WaitGroup 来实现并发任务的同步。 sync.WaitGroup 有以下几个方法:

方法名 功能
(wg * WaitGroup) Add(delta int) 计数器 + delta
(wg *WaitGroup) Done() 计数器 - 1
(wg *WaitGroup) Wait() 阻塞直到计数器变为 0

sync.WaitGroup 内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了 N 个并发任务时,就将计数器值增加 N。每个任务完成时通过调用 Done () 方法将计数器减 1。通过调用 Wait () 来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。

var wg sync.WaitGroup

func hello() {
    defer wg.Done()
    fmt.Println("Hello Goroutine!")
}
func main() {
    wg.Add(1)
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    wg.Wait()
}
1
2
3
4
5
6
7
8
9
10
11
12

需要注意 sync.WaitGroup 是一个结构体,传递的时候要传递指针。

# 依赖管理

image-20230121154202417

  • 工程项目不可能基于标准库 0~1 编码搭建
  • 管理依赖库

Go 依赖管理演进

image-20230121154211566

  • 不同环境 (项目) 依赖的版本不同
  • 控制依赖库的版本

# GOPATH

image-20230117124824493

我们使用 go get 会下载最新版本的包到 src 目录下

但是 GOPATH 也有弊端,无法实现 package 的多版本控制

如 A 和 B 依赖于某一 package 的不同版本

image-20230121154230213

# Go Vendor

  • 项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vendor
  • 依赖寻址方式:vendor=>GOPATH

image-20230121154239828

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题。

但是 Go Vendor 也有弊端,无法控制依赖版本和可能会出现依赖冲突,导致编译错误。

image-20230121154248023

# Go Moudle

  • 通过 go.mod 文件管理依赖包版本
  • 通过 go get/go mod 指令工具管理依赖包
  • 定义版本规则和管理项目依赖关系

# 依赖配置 - go.mod

image-20230121154259982

依赖标识:[Module Path][Version/Pseudo-version]

# 依赖配置 - version

image-20230121154310232

# 依赖配置 - indirect

require (
	github.com/Rican7/retry v0.1.0 // indirect
	github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7 // indirect
	github.com/boltdb/bolt v1.3.1 // indirect
	github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b // indirect
	github.com/codegangsta/negroni v1.0.0 // indirect
	...
)
1
2
3
4
5
6
7
8

image-20230121154325927

出现 //indirect 的标识则表明改依赖是间接依赖,没有该标识则为直接依赖

# 依赖配置 - incompatible

image-20230121154341722

go mod 要求每个 module 从大版本 2 开始,模块路径必须有类似 /vN 版本号的后缀,假如 module example.com/mod 从 v1.0.0 发展到 v2.0.0,这时它的 go.mod 中的模块路径应该修改为 example.com/mod/v2 。go mod 认为如果一个 module 的两个不同版本之间引入路径相同,则它们必须是相互兼容的,而不同的大版本通常意味着是不兼容的,所以引入路径也不该相同,通过在模块路径上加上大版本后缀,这样就可以同时使用同一个模块的多个不同大版本。

对于一些比较老的项目可能当时 go mod 还没出现,但版本早已经迭代到 v2 以上,或者有些项目没有遵循以上的原则,go mod 为了能够正常使用它们,会在引入 v2 以上的版本后加上 +incompatible 以示提醒。

# 依赖分发

image-20230121154409195

之前我们通过 GOPROXY 设置代理,GOPROXY 环境变量可以设置多个代理站,使用逗号分开,其中 direct 为源站

$env:GOPROXY = "https://goproxy.cn,direct"
1

# go get

image-20230121154426037

# go mod

image-20230121154436584

# 测试

测试是避免事故的最后一道屏障

image-20230121154725568

从上到下,覆盖率逐层变大,成本却逐层降低

image-20230121154737202

# 单元测试

image-20230121154804772

# 单元测试 - 规则

  • 所有测试文件以_test.go 结尾 image-20230121155053646
  • 测试方法以 func TestXxx(*testing.T) 命名 image-20230121154943159
  • 初始化逻辑放到 TestMain 中 image-20230121154935076

# 单元测试 - 例子

func HelloTom() string {
    return "Jerry"
}
1
2
3

测试样例

func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    if output != expectOutput {
        t.Errorf("Expected % do not match actual %s", expectOutput, output)
    }
}
1
2
3
4
5
6
7

运行单元测试

image-20230121155422769

# 单元测试 - assert

使用第三方库 assert 可以更好的编写测试用例

func HelloTom() string {
    return "Jerry"
}
1
2
3

测试用例

func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    assert.Equal(t, expectOutput, output)
}
1
2
3
4
5

运行单元测试

image-20230121155620171

# 单元测试 - 覆盖率

在实际项目中

  • 如何衡量代码是否经过了足够的测试?
  • 如何评价项目的测试水准?
  • 如何评估项目是否达到了高水准测试等级?

答案就是代码覆盖率

方法

func JudegePassLine(score int16) bool {
    if score >= 60 {
        return true
    }
    return false
}
1
2
3
4
5
6

测试用例

func TestJudegePassLineTrue(t *testing.T) {
    isPass := JudegePassLine(70)
    assert.Equal(t, true, isPass)
}
1
2
3
4

执行 test 并加上 --cover 属性

go test judgment_test.go judgment.go --cover
1

image-20230121160017004

可以看到代码覆盖率为 66.7% 我们测试用例中并没有命中 return false 这行代码,我们修改一个测试用例使代码覆盖率到 100%

测试用例

func TestJudegePassLineTrue(t *testing.T) {
    isPass := JudegePassLine(70)
    assert.Equal(t, true, isPass)
}

func TestJudegePassLineFail(t *testing.T) {
    isPass := JudegePassLine(50)
    assert.Equal(t, false, isPass)
}
1
2
3
4
5
6
7
8
9

image-20230121160223048

# 单元测试 - Tips

  • 一般覆盖率:50%~60%,较高覆盖率 80%+。
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责

# 单元测试 - 依赖

工程中复杂的项目,一般会有多个依赖,而我们的单测需要保证稳定性和幂等性,稳定是指相互隔离,能在任何时间,任何环境,运行则试。幂等是指每一次测试运行都应该产生与之前一样的结果。而要实现这一目的就要用到 Mock 机制。

image-20230121160431033

这里我们以 monkey 作为例子,https://github.com/bouk/monkey

快速 Mock 函数

  • 为一个函数打桩
  • 为一个方法打桩

image-20230121160859359

简单使用,使用 Mock

原始函数

image-20230121161629392

打桩

image-20230121161728618

对 ReadFirstLine 打桩测试,不在依赖本地文件

# 基准测试

GO 语言还提供了基准测试框架,基准测试是指测试一殷程序的运行性能及耗费 CPU 的程度。而我们在实际项目开发中,经常会遥到代码性能瓶颈,为了定位问题经常要对代码做性能分祈,这就用到了基准测试。使用方法类以于单元测试。

原始函数

image-20230121162128879

基准测试

image-20230121162146048

运行测试

image-20230121162415319

  • Resttimer 重置计时器,我们再 reset 之前做了 init 或其他的准备操作,这些操作不应该作为基准测试的范围;
  • runparallel 是多协程并发测试;执行 2 个基准测试,发现代码在并发情况下存在劣化,主要原因是 rand 为了保证全局的随机性和并发安全,持有了一把全局锁。

而公司为了解决这一随机性能问题,开源了一个高性能随机数方法 fastrand,下面有开源地址;我们这边再做一下基准测试,性能提升了百倍。主要的思路是牺牲了一定的数列一致性,在大多数场景是适用的

https://github.com/bytedance/gopkg

image-20230121162932721

image-20230121162940292

# 项目实践

# 需求描述

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

# 需求用例

浏览消费用户

image-20230121183831085

# ER 图

image-20230121183859219

# 分层结构

image-20230121183915306

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

# 组件工具

gin (opens new window)

go mod init
go get -u github.com/gin-gonic/gin.v1@v1.3.0
1
2

# Repository

image-20230121184311488

# Repository-index

image-20230121184342955

image-20230121184352675

初始化话题数据索引

image-20230121184419109

# Repository - 查询

image-20230121184452494

# Service

实体

image-20230121184520057

流程

image-20230121184534548

编码

image-20230121184600493

话题和回帖并行处理

image-20230121184640781

image-20230121184649615

# Controller

  • 构建 View 对象
  • 业务错误码

image-20230121184724574

# Router

  • 初始化数据索引
  • 初始化引擎配置
  • 构建路由
  • 启动服务

image-20230121184802447

# 运行测试

go run server.go
1

发送请求

curl --location --request GET 'http://0.0.0.0:8080/community/page/get/2' | json
1
编辑 (opens new window)
上次更新: 2023/12/06, 01:31:48
Go 项目案例
性能调优

← Go 项目案例 性能调优→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式