Golang 工程进阶和简单 Gin 项目实战 | 青训营笔记

250 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

Go 工程进阶

语言进阶

从并发的编程的视角了解 Go 高性能的本质

并发 & 并行

并行:多线程程序在不同的 CPU 核心上同时执行

并发:多线程程序在一个 CPU 核心上通过不停的切换执行的线程达到当前类似并行执行的效果 Golang 能充分发挥多核的优势高效运行

协程 & 线程

协程:用户态,轻量级的并发实现方式,栈 KB 级别

线程:内核态,协程运行在线程中,栈 MB 级别

  • Hello Goroutine

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func hello(i int) {
    	fmt.Printf("Hello goroutine : %d\n", i)
    }
    
    func main() {
    	fmt.Println("Start")
    	for i := 0; i < 100; i++ {
    		go hello(i)
    	}
    
    	// 休眠 1 秒,等待协程执行完成
    	time.Sleep(time.Second)
    	fmt.Println("End")
    }
    

通道(chan)

通过 make(chan <type>[,size]) 创建通道

type: 元素类型

size: 缓冲大小

没有缓冲的通道,一个读请求会等待一个写请求

有缓冲的通道,当写满了时就会等待读请求将缓冲区中的元素去出去,等缓冲区能写入了才会继续执行

使用 close 函数能关闭通道,关闭后的通道读取数据会返回 默认值和 false,同时不能再读关闭后的通道再执行 close 和 写请求

  • 计算平方后输出

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	fmt.Println("Start")
    	src := 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 { // 使用 for range 方法,能自动感知通道是否关闭了,等到缓冲区读取完成,就会自动退出
    			dest <- i * i
    		}
    	}()
    	for i := range dest {
    		fmt.Println(i)
    	}
    	fmt.Println("End")
    }
    

并发不安全

10个协程对同一个变量进行 1 万次加 1 后,输出结果不一定是 10 万

package main

import (
	"fmt"
	"time"
)

func main() {
	id := 0

	for i := 0; i < 10; i++ {
		go func(i int) {
			for j := 0; j < 10000; j++ {
				id += 1
			}
			fmt.Println("协程退出:", i)
		}(i)
	}

	time.Sleep(time.Second)
	fmt.Println("id = ", id)
}

并发安全

  • Mutex

    使用互斥锁能使协程部分代码变一次原子操作,这个协程执行完成,再执行其它协程或者继续执行本协程

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    func main() {
    	var mutex sync.Mutex
    	id := 0
    
    	for i := 0; i < 10; i++ {
    		go func(i int) {
    			for j := 0; j < 10000; j++ {
    				mutex.Lock()
    				id += 1
    				mutex.Unlock()
    			}
    			fmt.Println("协程退出:", i)
    		}(i)
    	}
    
    	time.Sleep(time.Second)
    	fmt.Println("id = ", id)
    }
    
  • WaitGroup

    通过 time.Sleep(time.Second) 等待子协程结束运行显得格格不入,当子协程提前结束了也不能感知,同时如果子协程没有结束只等待1秒又不够,需要自行估算子协程全部结束所需要的时间,通过 sync.WaitGroup 能简化这一操作

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    func main() {
    	var mutex sync.Mutex
    	id := 0
    
    	// 使用 WaitGroup 实现主协程等待子协程
    	var wg sync.WaitGroup
    	for i := 0; i < 10; i++ {
    		wg.Add(1) // 让计数器加一
    		go func(i int) {
    			defer wg.Done() // 让计数器减一
    			for j := 0; j < 10000; j++ {
    				mutex.Lock()
    				id += 1
    				mutex.Unlock()
    			}
    			fmt.Println("协程退出:", i)
    		}(i)
    	}
    
    	wg.Wait() // 当计数器的值为 0 时,结束等待
    	fmt.Println("id = ", id)
    }
    

依赖管理

Golang 依赖管理的演进路线

背景

  • 工程项目不可能完全基于标准库从 0~1 实现
  • 管理依赖库

演进过程

  • GOPATH -> Go Vendor -> Go Module

作用

  • 控制不同项目依赖的版本
  • 引用其它已经实现好了的库

GOPATH

  • 文件夹

    • bin

      • 项目编译的二进制文件
    • pkg

      • 项目编译的中间产物
    • src

      • 项目源码
      • 通过 go get 下载的最新的包
  • 问题

    • 不能让不同的项目依赖不同的包版本

Go Vender

  • 类似 PHP 的 Composer 和 NPM 的包管理方式

  • 文件夹

    • vender

      • 在项目目录下
      • 依赖的版本优先选择 vender 文件夹下的副本
  • 问题

    • 不能很好的解决传递依赖的版本冲突的问题

Go Module

  • 类似Java 语言的依赖管理工具

    • Maven
    • Gradle
  • 文件

    • go.mod

      • 指定项目依赖包和版本
      • 指定传递依赖包的版本
    • go.sum

  • 依赖配置

    • module

      • 标识一个模块的名字
    • go

      • 标识使用的 Go 的版本
    • require

      • 项目依赖的包,可以重复

      • indirect

        • 加上 // indirect 的包,表示间接依赖
        • 没有则表示直接依赖
      • incompatiable

        • Go 推荐 2+ 的 Major 版本使用 /v* 的后缀 module命名
        • 对于之前已经创建的版本需要在版本后拼接 【+incompatiable】
  • 命令

    • init

      • 通过 go mod init 创建 go.mod 文件
    • tidy

      • 检查项目的依赖,并自动添加删除依赖

测试

编写良好的单元测试,能让项目更加健壮

单元测试

  • 流程

    • 输入

    • 测试单元

      • 函数
      • 模块
      • 接口
      • ......
    • 校对

      • 输出
      • 期望
  • 规则

    • 所有的测试文件以 _test.go 结尾
    • 函数定义为 func TestXxx(*testing.T)
    • 初始逻辑放在 TestMain 方法中
  • 运行

    • 使用 Fleet 或者 Golang 能直接使用测试方法前的运行按钮开始测试
    • 通过命令行执行 go test [flags] packages
  • 代码覆盖率

    • 用来检测一个单元测试是否足够完备的一种方法
    • 使用 --cover 标志

Mock 测试

  • 通过开源库:monkey 实现

    github.com/bouk/monkey

  • 提供的函数

    • Patch:将一个函数的实际执行代码进行替换,即为函数打桩
    • Unpatch:撤销一个函数的 Patch

基准测试

  • 测试程序的运行性能和 CPU 的损耗

  • 规则

    • 测试文件以 _test.go 结尾
    • 函数定义为 func BenchmarkXxx(*testing.B)
  • 运行

    • 通过命令行执行 go test -bench=.

项目实战

通过实际的项目能学习更多在工作中的操作

需求描述

  • 展示话题和回帖列表
  • 仅仅实现一个本地 Web 服务

分层结构

  • 数据层

    数据 Model ,对外部数据进行处理

  • 业务层

    处理核心的业务逻辑

  • 视图层

    处理和外部额交互逻辑

代码的编写流程

这里使用外链(自己的),因为能更好的分步写出代码的编写过程

话题回帖 | 项目实战 | Wen Flower 学习笔记 (twtool.icu)