Go语言工程实践 | 青训营笔记

171 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

前言

本笔记完成了第二次后端课程GO语言工程实践的课后作业,代码点击此处,主要完成了了下面几个方面 \

  1. 使用读写锁控制并发读写post文件
  2. 使用URL参数传递topicId,使用表单传递回帖数据content
  3. 在视图层controller完成topicId的范围检查,在业务层实现读写锁并发读写文件和索引文件,在数据层实现确立post回帖的id和时间createtime
  4. 最后给出实验过程和结果截图 本项目在使用Go&Gin实现后,还使用Python&Django,Java&Spring实现了一次

语言进阶

并发和并行的区别
并发是多线程在一个核上轮流执行,通过时间片实现 并行是多个程序在多个核上同时运行,需要多核CPU支持
Go语言多线程模型
用户态运行协程,实际上是用户级线程;内核态运行线程 协程处于用户态,是轻量级线程,栈很大MB级别 线程处于内核态,一个线程位多个协程服务,栈KB级别
Go协程和不使用协程区别 \

package main

import (
   "fmt"
   "time"
)

func print(i int) {
   fmt.Println(i)
}

func main() {
   t1 := time.Now()

   for i := 0; i < 10000; i++ {
      go print(i)
   }
   t2 := time.Now()

   fmt.Println(t2.Sub(t1))
}

使用协程上面的程序执行55ms,而不是用协程执行850ms
协程间通信机制
使用通道通信共享内存而不是使用共享内存实现通信。协程可以想通过通道向另外一个协程发送数据,而不是使用共享内存传递数据,使用共享内存就意味着进程竞争锁的问题,会影响性能。Go用牺牲内存的方法,为每两个协程创建一个通道进行通信,而不是让多个协程竞争共享内存

  • 使用make创建通道
    通道有两种,一种是无缓冲通道(make(chan int) ),一种是有缓冲通道( make(chan int, 3) ),无缓冲通道是一种同步机制,有缓冲通道是一种生产者消费者模型。
package main

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

defer用于延迟的资源关闭
使用带缓存的channel可以解决生产者和消费者能力不均衡的问题
协程并发安全问题
写成并发因为操作非原子性会导致计算机结果不能和顺序执行结果一样,那么我们就要并发安全,使用锁实现并发安全

package main

import (
   "fmt"
   "sync"
)

var (
   x    int64
   lock sync.Mutex
)

func addwithlock() {
   for j := 0; j < 10; j++ {
      lock.Lock()
      x++
      lock.Unlock()
   }
}

func addwihoutlock() {
   for j := 0; j < 10; j++ {
      x++
   }
}

func main() {
   var wait sync.WaitGroup
   wait.Add(10)
   x = 0
   for i := 0; i < 10; i++ {
      //go func() {
      // defer wait.Done()
      // addwithlock()
      //}()
      //addwihoutlock()
      go func() {
         defer wait.Done()
         addwithlock()
      }()
   }
   wait.Wait()
   fmt.Println(x)

   wait.Add(10)
   x = 0
   for i := 0; i < 10; i++ {
      go func() {
         defer wait.Done()
         addwihoutlock()
      }()
   }
   wait.Wait()
   fmt.Println(x)
}

协程中阻塞主线程

package main

import (
   "fmt"
   "sync"
   "time"
)

func print(i int) {
   fmt.Println(i)
}

func main() {

   var wg sync.WaitGroup

   t1 := time.Now()

   for i := 0; i < 10000; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         go print(i)
      }()
   }
   t2 := time.Now()

   wg.Wait()
   fmt.Println(t2.Sub(t1))
}

使用 waitgroup实现主协程和子协程的同步,类似Java程序的

for(Thread i: threadList){
    i.join()
}

Java实现主线程和子线程同步,是通过让线程进入等待队列
Waitgroup类似Java信号量实现,初始化协程个数,没执行完一个协程就让wg信号量减一,直到信号量0,不过信号量到0会阻塞增加的线程,而wg会返回主协程

依赖管理

框架、集合、日志、驱动等依赖的导入

Gopath 缺点是没有实现依赖版本控制,这样如果对于项目依赖升级而导致的升级的依赖包对旧的函数不支持,那么升级的依赖包是不可用的,应该使用旧旧的依赖包
GoVender使用Vendoer目录,保存旧的package包,而如果项目在使用旧版本无法构建成功,那么就会回到GoPath使用新版本构建.GoVender的问题在于一个项目如果依赖两个包,而两个包又依赖同一个包的不同版本,那么就不能构建成功,主要问题在于两个包依赖于源码,而不能区分版本,这需要GoModule解决
GoModule,通过go.mod文件管理依赖包版本,通过go get和go mod指令工具管理依赖包。终极目标定义版本规则和项目依赖关系 依赖管理的三要素
类似于Java的包管理工具Maven,通过一个依赖文件,定义了编译项目的依赖关系,在编译的时候会获取对应的包并且完成编译。依赖管理实际上是要完成编译时使用的依赖,类似于C/C++编译器使用的Makefile文件,定义了编译使用的依赖

  • 描述依赖的配置文件go.mod, Maven又对应的配置文件
  • 中心仓库管理依赖库Proxy,Maven从中心仓库获取依赖包
  • 本地工具 go get/mod实现依赖包的管理

依赖配置

GoMod版本规则

  • 语义化版本
    有三个部分,类似Linux的版本,Major.Minor.Patch,Major是可以不兼容的版本,Minor是大版本下面的兼容版本,一般是小的函数增加或修改,Patch是处理异常错误的版本
  • 基于Commit的位版本
    语义版本(v1.0.3)/commit时间/commit生成哈希前缀

依赖图 两个包依赖相同包的两个兼容版本,构建的时候会选择最低兼容的版本

依赖分发

从版本管理仓库获得需要的依赖
有两中依赖分发的方式,一种是回源,从作者放到的第一个代码管理仓库比如Github获取,但是这有几个问题,一是作者可以删除依赖,二是作者可以更改依赖的版本造成不可靠依赖,三是大规模访问源会造成仓库负载问题;第二种是使用代理服务站点,可以缓存旧的版本
配置Proxy使用参数Goproxy

GOPOXY="https://proxy1.cn, https://proxy2.cn,direct"

访问的时候先访问proxy1,如果不存在再proxy2,否则访问源
设置proxy实际就相当于数据库缓存Redis一样,如果Redis访问不到,旧访问源MySQL

工具

go get pkg参数\

@update 默认
@none 删除依赖
@v1.1.2
@2...... 特定的commit
@master 最新的commit
**go mod ** go mod init 创建go.mod
go mod download 下载需要的包 go tidy 删除不需要的包

测试

回归测试使用整个软件,集成测试测试某个功能,单元测试是开发阶段测试函数。测试时防止上线损失的一道防线

单元测试

输入到测试单元,校对输出和期望的比对

image.png 图中,2我们看到被测试的模块HelloTom,1是测试文件,测试文件必须_test.go结尾,测试函数命名规则是Test测试模块(t *testing.t),编译运行使用go test -v,会自动运行所有Test测试模块
衡量测试好坏标准是覆盖率 测试实践

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

测试依赖

  • 稳定性,单元测试隔离
  • 幂等性,用相同参数重复运行函数结果不变

Mock机制 Mock解决使用文件等用于测试的而文件被别人修改的问题,使得测试不依赖本地文件

image.png

基准测试

测试单元CPU性能,用于优化热点代码

image.png

项目实践

需求设计

需求描述

  1. 展示话题(标题,文字描述)和回帖列表
  2. 话题和回帖数据用文件存储

需求用例

使用PlatUml画用例图 PlantUML

用例图
@startuml
left to right direction
actor User             #yellow;line:red;line.bold;
rectangle TopicPage{
  usecase Topic        #yellow;line:red;line.bold
  usecase PostList     #yellow;line:red;line.bold
}
User --> Topic         #line:red
User --> PostList      #line:red
@enduml

image.png

ER图
@startuml
left to right direction
entity Topic #yellow;line:red;line.bold
{  
  * id
  * title
  * content
  * create_time
}               
entity Post #yellow;line:red;line.bold
{
  * id
  * topic_id
  * content
  * create_time
}               

Topic ||--|{ Post #line:red;line.bold
@enduml

image.png

设计项目框架

使用分层结构

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

使用Go Web框架Gin,项目组件图

@startuml
[server]
note right of [server]
服务器程序,处理路由和请求参数的提取,
并且调用相应的方法处理请求
end note

folder Controller{
    [query_page_info.go] as q1
    [write_post.go] as w1
}

note left of Controller
视图层,接受server.go主程序调用参数,
对这些参数进行检查,之后调用
service逻辑层的方法获取数据,
视图层根据把获得的数据添加一些其他
返回参数组装一起返回给客户端
end note

note left of w1
检查server传递参数,将参数传递给Service层
的write_post.go
end note

folder Service{
    [query_page_info.go] as q2
    [write_post.go] as w2
}
note left of Service
逻辑层,对视图层请求的每一项数据,都利用
Repository数据层去请求,然后把所有的请求
得到的数据整合到一起返回给视图层
end note

note left of w2
接收逻辑层write_post.go给出的参数,根据要求
获取写入数据库的参数,调用下一层写入数据库和
postIndexMap
end note

folder Repository{
    [post.go] as post
    [topic.go] as topic
    component db_init.go as dbinit{
        [initTopicIndexMap] as topicmap
        [initPostIndexMap]  as postmap
    }

    component write_post.go as dbwp{
        [writePostIndexMap] as wpim
        [writePostdb] as wpdb
    }
}
note right of Repository
数据层,DAO是Data Access Object的意思,
也就是数据访问对象,这个类用来单独从
数据库中提取一个数据项
end note

database "data"{
    [post] as postdb
    [topic] as topicdb
}



[server] ..> [q1] : 调用
[q1] ..> [q2] : 调用
[q2] ..> [post] : 调用
[q2] ..> [topic] : 调用

[postmap] ..> [postdb]: 使用
[topicmap] ..> [topicdb]: 使用

[server] ..> dbinit: 创建索引数据结构并保存在内存
[topicmap] ..> [topicIndexMap]: 创建
[topic] ..> [topicIndexMap] :使用

[postmap] ..> [postIndexMap]: 创建
[post] ..> [postIndexMap] :使用

[server] ..> [w1] : 调用
[w1] ..> [w2] : 调用

[w2] ..> dbwp: 并发写入postIndexMap和post数据库 #red
[wpim] ..> postdb : 写入
[wpdb] ..> postIndexMap : 写入
@enduml

image.png

一些前置基础知识
PlumtUML组件图
plantuml.com/zh/componen…
使用curl提交表单测试后端链接
blog.csdn.net/freedomwjx/… \

本次实验使用 curl -d 通过 application/x-www-url-encoded方式发送post请求
curl -d "content=abc" http://127.0.0.1:8080/community/page/post/2 Post请求传递数据(消息主体)的四种方式
www.jianshu.com/p/f1fa0b275…
www.cnblogs.com/softidea/p/…
blog.tcs-y.com/2019/06/02/…
segmentfault.com/a/119000001…
blog.csdn.net/bigtree_372…
主要包括表单数据的三种数据格式application/x-www-form-urlencoded text/plain multipart/form-data和Json数据格式application/json。curl使用-d发送application/x-www-form-urlencoded格式的表单请求,使用-F发送multipart/form-data数据格式
golang gin获取请求参数的方法
www.icodebang.com/article/185…
www.modb.pro/db/220747
www.codebaoku.com/it-go/it-go…
www.tizi365.com/archives/25…
laravelacademy.org/post/21647
本次实验使用
topicId := c.Param("id") 获取url参数
content := c.PostForm("content") 获取post请求表单参数 Windows查看端口占用程序并且杀死占用程序的方法
cloud.tencent.com/developer/a…
blog.csdn.net/peng86788/a…
使用 netstat -aon|findstr "8080" 查看占用8080程序的并且获取PID
使用 taskkill /pid 9196 -f 关闭对应PID程序 Go语言协程错误传递和处理规范
www.jianshu.com/p/927cfb6cb…

Go 的相对路径是go run执行时的路径
segmentfault.com/a/119000001… \

这样就造成了相对路径失效的问题,解决办法是设置全局变量,然后os.Getenv("GO_PROJECT")获取一个绝对路径,使用这个绝对路径拼接我们需要的数据的路径 GO读写锁
www.flysnow.org/2017/05/03/…
curl并发请求
fedingo.com/how-to-run-…
本实验使用命令
并发写命令: xargs -I % -P 5 curl -d "content=abcdefg" "http://127.0.0.1:8080/community/page/post/2" < <(printf '%s\n' {1..10})
并发读命令: xargs -I % -P 5 curl "http://127.0.0.1:8080/community/page/get/1" < <(printf '%s\n' {1..10}) image.png

image.png Linux执行.sh文件
blog.csdn.net/ljp81218424…
先写一个.sh文件,再给他可执行权限,通过 chmod a+x test.sh 命令,之后./test.sh就可执行

本实验GO语言代码地址

实验结果

  1. 实验过程

image.png 2. 实验结果

  • 实现使用读写锁并发访问

image.png

image.png