学习了Go语言进阶与依赖管理和Go语言工程实践之测试,这是我参与青训营项目写下的第二篇笔记。
一、
-
首先我们确定已经安装了Go语言以及所需的第三方包(Gin和go-sqlite3)
-
创建一个新的Go模块并初始化:
go mod init topicforum -
创建一个
main.go文件,并导入所需的包:"github.com/gin-gonic/gin" "github.com/jinzhu/gorm" "github.com/jinzhu/gorm/dialects/sqlite" "io/ioutil" "log" -
在
main函数中,我们初始化了数据库并进行了迁移,注册了对应的路由处理函数。func main() { router := gin.Default() db, err := gorm.Open("sqlite3", "forum.db") if err != nil { log.Fatal(err) } defer db.Close() // 迁移数据库结构 db.AutoMigrate(&Topic{}, &Post{}) // 注册路由 router.GET("/topics", getTopicsHandler) router.POST("/topics", createTopicHandler) router.GET("/topics/:id/posts", getPostsHandler) router.POST("/topics/:id/posts", createPostHandler) router.Run(":8080") } -
Topic和Post分别代表话题和回帖的数据结构。type Topic struct { gorm.Model Title string Description string } type Post struct { gorm.Model TopicID uint Content string } -
getTopicsHandler:获取所有话题的处理函数。func getTopicsHandler(c *gin.Context) { db := getDB() var topics []Topic db.Find(&topics) c.JSON(200, topics) } -
createTopicHandler:创建新话题的处理函数。func createTopicHandler(c *gin.Context) { db := getDB() var topic Topic c.BindJSON(&topic) db.Create(&topic) c.JSON(200, topic) } -
getPostsHandler:获取特定话题下的回帖列表的处理函数。func getPostsHandler(c *gin.Context) { db := getDB() topicID := c.Param("id") var posts []Post db.Where("topic_id = ?", topicID).Find(&posts) c.JSON(200, posts) } -
createPostHandler:创建特定话题下的回帖的处理函数。func createPostHandler(c *gin.Context) { db := getDB() topicID := c.Param("id") var post Post c.BindJSON(&post) post.TopicID = convertID(topicID) db.Create(&post) c.JSON(200, post) } -
getDB():函数是用来获取数据库连接的实例。通过这个函数获取数据库连接,以便进行数据操作。func getDB() *gorm.DB { db, err := gorm.Open("sqlite3", "forum.db") if err != nil { log.Fatal(err) } return db } -
用于初始化操作的
init()函数func init() { // 创建数据库文件 err := ioutil.WriteFile("forum.db", []byte(""), 0644) if err != nil { log.Fatal(err) } -
convertID()函数func convertID(id string) uint { // 这里可以使用你自己的ID转换逻辑 // 示例中简单地将字符串转换为uint // 请确保转换的结果唯一性和不重复性 return uint(len(id)) }
当我们将一个字符串转换为uint类型时,我们希望转换后的结果是唯一且不重复的。也就是说,不同的字符串转换后不能得到相同的uint值,而同一个字符串每次转换的结果应该是一致的。
这样做的目的是为了确保每个字符串都能生成一个唯一的标识符,例如用于在数据库中标识话题或回帖的ID。
示例:
func main() {
id := "example"
convertedID := convertID(id)
fmt.Println(convertedID)
}
func convertID(id string) uint {
// 在实际应用中,可以使用更复杂的转换逻辑来保证结果的唯一性和不重复性。
// 这里只是一个简单的示例,将字符串转换为其ASCII码之和作为ID。
var converted uint
for _, char := range id {
converted += uint(char)
}
return converted
}
-
运行服务
go run main.go -
使用curl或Postman等工具来测试API接口(课程里面推荐Postman我用的这个)
-
获取所有话题
GET http://localhost:8080/topics -
创建话题
POST http://localhost:8080/topics Request Body: { "title": "话题标题", "description": "话题描述" } -
获取特定话题下的回帖列表
GET http://localhost:8080/topics/:id/posts -
创建特定话题下的回帖:
POST http://localhost:8080/topics/:id/posts Request Body: { "content": "回帖内容" }注意:将URL中的
:id替换为实际的话题ID。
二、
并发与并行
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,所以无论从微观还是从宏观来看,二者都是一起执行的。
Golang是实现高并发的调度模型,可以最大限度的利用计算资源,充分发挥多核优势,高效运行,可以说go语言是为并发而生。
goroutine
协程:用户态,轻量级线程,栈KB级别
线程:内核态,线程跑多个协程,栈MB级别
协程的创建和调度由Go语言本身去完成,Go语言可以一次性创建上万个级别的协程。
协程: 同一时间只能执行某个协程。开辟多个协程开销不大。协程适合对某任务进行分时处理。
线程: 同一时间可以同时执行多个线程。开辟多条线程开销很大。线程适合多任务同时处理。
协程与线程的区别主要在于它们的执行方式和资源管理:
- 执行方式:
线程是由操作系统进行调度的,并且多个线程可以同时运行在多个CPU核心上,从而实现并发。 协程是由程序员自行控制的,它在单个线程内进行切换,通过yield、await等关键字实现协程之间的切换,从而实现并发或并行。
- 资源管理:
线程由操作系统管理,因此线程之间的切换会涉及上下文切换,这可能导致一些开销,尤其在多线程高并发的情况下。 协程切换由程序员控制,因此在切换时避免了操作系统的开销,使得协程的切换更加轻量级。
- 通信方式:
线程之间的通信需要使用同步原语,如锁、信号量等,以避免竞态条件和资源冲突。 协程之间的通信可以通过共享数据,但更常见的是通过消息传递方式进行通信,例如使用生成器或异步函数。
- 并发性和并行性:
线程通常用于实现并发性,多个线程在同一时刻执行不同的任务。 协程通常用于实现并发或者并行性,通过在单线程内切换协程,可以让多个任务在同一时间段内执行。 总的来说,线程适用于CPU密集型任务,而协程适用于IO密集型任务,因为协程的切换更为轻量级,适合处理大量的IO操作。
启动goroutine
启动的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。
代码第3行:创建一个goroutine ,在新的goroutine中执行Hello函数
func main() {
// 启动 goroutine
go Hello()
fmt.Println("This main func")
time.Sleep(time.Second)
}
当main()函数返回的时候goroutine便结束了,所有在main()函数中启动的goroutine会随之结束。
time.Sleep
另附加一个点,使用time.sleep可允许main函数等待hello函数
var wg sync.WaitGroup
func Hello() {
fmt.Println("hello word")
wg.Done() // 计数器-1
}
func main() {
wg.Add(1) // 计数 +1
go Hello()
fmt.Println("This main func")
wg.Wait() // 阻塞,一直等待所有的goroutine结束
}
csp(Communicating Sequential Processes)
协程之间的通信,go提倡通过通信共享内存,而不是通过共享内存而实现通信
goroutine 是go程序并发的执行体,channel相当于把协程做了一个连接,就像是传输队列遵循一个先入先出,能保证说话数据的顺序。
通过对内存进行一个加速,需要获取临界区的权限,在这种机制下,不同的goroutine之间容易发生数据静态的问题,在一定程度上影响程序性能。
对比两种通信方式,go更提倡通信共享内存。
channel(通道)
channel是一种引用类型。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
创建channel
通道类型的空值是nil
var ch chan int
fmt.Println(ch)
通道的初始化
声明的通道需要使用make函数初始化,可以选择缓冲大小。
make(chan 元素类型, [缓冲大小])
例:
var ch chan int // 声明一个传递int类型的channel
ch := make(chan int) // 使用make()函数定义一个channel
channel的三种操作
channel分为:发送(send)、接收(receive)和关闭(close),三种操作。
//定义一个通道
ch := make(chan int)
//把一个值发送到通道中
ch <- 5
// 从一个通道中接收值并赋值给变量
x := <- ch
<-ch
//调用内置的close函数来关闭通道
close(ch)
例子:
A子协程发送0~9数字,B子协程计算输入数字的平方,主协程输出最后的平方数
func CalSquare(){
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{
dest<-i*i
}
}()
for i:=range dest{
println(i)
}
}
打印:
0
1
4
9
16
25
36
49
64
81
并发安全lock
Lock是一个接口。lock()方法是主要的使用方法,与之配合的是unLock()。
例图是加锁不加锁的区别,预期是10000。
加法函数的实现,addWithLock()通过临界区控制的实现,用sync.mutex一个关键字实现的加锁。
addWithoutLock()函数没有对临界区进行保护,直接进行了操作。
每个都开启一个协程并发地执行,分别对加锁和未加锁进行测试。
可知不加锁会输出未知的结果,并发安全问题。
实际项目开放中,并发安全问题会有一定几率引起错误结果,且比较难定位,所以应该避免对共享内存做一些非并发安全的读写操作。
WaitGroup
WaitGroup 类实现的功能是:等待一系列协程并发地执行完毕。如果不等待所有协程执行完毕,可能会导致一些线程安全问题。
WaitGroup的三个方法
WaitGroup是Golang并发的两种方式之一,一个是Channel,另一个是WaitGroup。
WaitGroup有3个API:
1)Add(delta int):增加/减少若干计数 2)Done:减少 1 个计数,等价于 Add(-1) 3)Wait:等待,直到计数等于 0
开启协程+1; 执行结束-1 ;主协程阻塞直到计数器为0。
例子:
package main
import (
"fmt"
"sync"
)
func hello(i int) {
fmt.Printf("hello goroutine: %v\n", i)
}
func main() {
wg := sync.WaitGroup{}
//调用Add(),声明协程的数量delta=5
wg.Add(5)
//协程执行
for i := 0; i < 5; i++ {
go func(j int) {
//每个协程执行完后,调用Done(),delta-1
defer wg.Done()
hello(j)
}(i)
}
//调用Wait(),当等待全部协程执行完毕才执行后续代码
wg.Wait()
fmt.Println("所有协程执行完毕,可执行下一项操作...")
}
打印结果:
hello goroutine: 4
hello goroutine: 1
hello goroutine: 0
hello goroutine: 3
hello goroutine: 2
所有协程执行完毕,可执行下一项操作...
三、
go的依赖管理主要经历了三个阶段:
分别是GOPATH,GO Vendor和 Go Module,到现在为止广泛运用的是Go Module。
主要是围绕以下两个目的:
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
GOPATH
GOPATH是go支持的一个环境变量。
GOPATH文件结构需要有三个子目录:
./go
|—— bin
|—— 存储所编译生成的二进制文件。
|—— pkg
|—— 存储预编译的目标文件,以加快程序的后续编译速度。
|—— src
|—— 存储所有.go文件或源代码。
|—— ......
当我们在src目录中写的模块是main时, 它会对应到一个可执行文件, 并且编译后的文件会被复制到bin目录。如果是其他模块, 它会被编译成一个库文件, 并且被复制到pkg目录。即一个放源代码, 一个放编译后的可执行文件, 另外一个放编译后的库文件。
项目代码直接依赖src下的代码;go get下载最新版本的包到src目录下。
go get命令的使用
默认情况下,go get 可以直接使用。
为了 go get 命令能正常工作,你得确保安装了合适的源码管理工具,同时把这些命令加入到你的 PATH 中。
若想获取 go 的源码并编译,可使用以下命令:
$ go get github.com/davyxu/cellnet
获取前,请确保 GOPATH 已经设置。Go 1.8 版本之后,GOPATH 默认在用户目录的 go 文件夹下。
cellnet 是一个网络库,没有可执行文件,因此在 go get 操作成功后 GOPATH 下的 bin 目录下不会有任何编译好的二进制文件。
需要测试获取并编译二进制的,可以尝试下面的这个命令。当获取完成后,会自动在 GOPATH 的 bin 目录下生成编译好的二进制文件。
$ go get github.com/davyxu/tabtoy
GOPATH的弊端
右边的v2没做到前后的一个兼容
A和B依赖于某一package的不同版本;无法实现package的多版本控制
Go Vendor
首先在项目目录下添加一个vendor文件,将所有依赖包以副本形式放在$ProjectRoot/vendor下
依赖寻址方式是vendor=>GOPATH
优点:
通过每个项目引入一个依赖副本,解决了多个项目需要同一个package依赖的冲突问题。
Go Vendor的弊端
通过vendor的管理模式不能很好地控制v1和v2的版本选择,一旦更新项目有可能出现依赖冲突导致编译出错。
Go Module
go语言官方推出的依赖管理系统,解决了以前依赖管理系统存在的无法依赖同一个库,多个版本等问题。
在Go v1.11/1.12版本引入,在Go 1.13版本稳定并默认打开。
- 通过go.mod 文件管理依赖包版本
- 通过go get/go mad 指令工具管理依赖包
如何开启Go Modules
使用go env命令可以查看你的Go Modules是否开启。
初始化Go Modules
go mod init [MODULE_PATH]
//初始化Go Modules,它将会生成go.mod文件。
//[MODULE_PATH]是指模块引入路径,可修改
基础命令使用
| 命令 | 作用 |
|---|---|
| go mod init | 生成 go.mod 文件 |
| go mod tidy | 整理现有依赖 |
| go mod download | 下载 go.mod 文件中指明的所有依赖 |
| go mod edit | 编辑 go.mod 文件 |
| go mod graph | 查看现有的依赖结构 |
| go mod vendor | 导出项目所有的依赖到vendor目录 |
| go mod verify | 校验模块是否被篡改过 |
| go mod why | 查看为什么需要依赖某模块 |
| go list -m all | 列出当前模块及其所有依赖项 |
| go get -u | 更新现有的依赖 |
分析关键字
module: 用于定义当前项目的模块路径
go:标识当前Go版本.即初始化版本
还有一个require(我图上未用到)
require: 当前项目依赖的一个特定的必须版本
依赖配置 -version
分为两个版本:语义化版本和基于commit 伪版本。
语义化版本
${MAJOR}.${MINOR}.\${PATCH}
MAJOR:不同的major版本不兼容(代码隔离)
MINOR:新增函数或者功能需要保持在这个major下做到兼容
PATCH:bug代码的修复
例:
V1.3.0
V2.3.0
基于commit 伪版本
vX.0.0-yyymmddhhmmss(时间戳)-abcdefgh1234(12位的hash码的前缀)
例:
v0.0.0-20220401081311-c38fb5932b7
GOPROXY
Proxy指的是代理软件或代理服务器,也可以认为是一种网络访问方式。
Go的1.11版本以后可以设置环境变量 GOPROXY,来设置代理,以加速下载。
它有下载速度快,没有限制,功能齐全,以及数据可视化等的特点。
GOPROXY="https://proxy1.cn,https://proxy2.cn,direct"
服务站点URL列表,"direct"表示源站
以下是项目查找依赖的路径:
覆盖率
示例代码
package main
func Size(a int) string {
switch {
case a < 0:
return "negative"
case a == 0:
return "zero"
case a < 10:
return "small"
case a < 100:
return "big"
case a < 1000:
return "huge"
}
return "enormous"
}
测试代码
package main
import "testing"
type Test struct {
in int
out string
}
var tests = []Test{
{-1, "negative"},
{5, "small"},
}
func TestSize(t *testing.T) {
for i, test := range tests {
size := Size(test.in)
if size != test.out {
t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
}
}
}
覆盖率测试如下:
我测试的时候出现了一个问题,但也找到了解决方法:
no Go files in D:\Trysomething
这个错误提示表明Go命令在指定路径下找不到任何Go文件。请确保在正确的目录下执行测试命令,以我的文件举例,该目录中包含size.go和size_test.go文件。可以使用cd命令切换到正确的目录,然后再次运行go test命令。
顺便说说另一个问题:
no required module provides package size.go; to add it: go get size.go
这个错误提示表明Go命令无法找到size.go文件所在的模块。请注意,go get命令是用来获取/安装包或模块的,而不是用来获取单个源文件的。如果你的项目使用模块管理,请确保在正确的模块路径下执行测试命令。
查看测试结果
- 使用
-coverprofile标志来指定输出的文件:
- 运行
test coverage tool
- 将注释源代码用html展示,由
-html标志调用:
已覆盖 (绿色), 未覆盖(红色) 和 未埋点(灰色)
参考地址:
Go 语言进阶与依赖管理(主要课程)