这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记」
go语言进阶
一、并发编程:
1.go语言相较于其他语言更适合并发('快'标签)
首先我们需要了解并发和并行的区别
并发 :多线程程序在一个核上的cup上运行(由于速度较快,不同的线程拿到时间片,运行完后又再次切换,cpu运行速度快,给人的感觉就是同时运行)
并行 :过线程程序在多个核的cpu上运行(多个核工作不相互影响,真同时运行)
go语言则可以充分发挥多核的优势:由于他相较其他高级语言来说,有着自己的获取到线程之后的分配机制(简单来讲就是在go拿到系统分配的线程后,有着自己的独立分配机制,再将系统线程划分到自己的每个进程中--协程)这就是go语言适合高并发的原因
2.go中创建线程
下面是一个展示使用go语言协程的案例
package concurrence
import (
"fmt"
"time"
)
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second) // 这里由于可能子线程在主线程销毁之后才执行完,因此让主线程等待
}
从上面的案例能看出,go语言有着成熟简洁的多线程方式即使用
go func(){}()
这样一个回调函数加 go关键字,即可开启一个Goroutine协程,且由于其占用内存很小,因此在go中开辟百万数据量的协程是一件轻松的事
3.go中的Channel管道
根据实际业务来创建有无缓冲的通道
- 当对于数据量不需要太多时间产生或者消耗,没有必要进行等待时直接使用无缓冲,将数据从源到目的
- 当数据量较大或者生产耗费较多时间时,则需要选取合适大小的缓冲区来预防数据丢失,阻塞过久等问题
案例
package concurrence
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)
}
}
既然有高并发,因此就有相应的问题
3.高并发的安全问题(Lock)
由于高并发可能存在操作同一数据的操作,因此在操作时就存在脏读,同时写错问题,解决方法就需要设置互斥形信号量来对不同线程限制访问(同时回降低效率,但是换取正确数据这是有必要的)。
以下案例则展现了不互斥访问,和互斥访问的结果现象
package concurrence
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 ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
运行结果:
WithoutLock: 8621; WithLock: 10000;
二、依赖管理
在之前java的学习中,使用各个框架,以及数据库,以及各个插件,学习过使用maven来管理项目的依赖,同样在go的开发中也有他自己的依赖管理 Go Module
(经历过三次迭代 GOPATH -> GO Vendor -> Go Module(go.mod文件)
go中更是进一步改进了依赖的托管(增加一层Proxy代理)
三、测试
类似于java中junit插件,go中也有着自己的测试插件 从上到下,覆盖率逐层变大,成本却逐层降低
-
回归测试:
类似于开发程序,让测试用户体验总体的整个软件,类似各大游戏的内测环节
-
集成测试:
系统功能为主,测试主要功能
-
单元测试:
开发阶段,每个不同的阶段进行测试
单元测试的规则类似junit
package test
import (
"github.com/stretchr/testify/assert" // 进行对比判断
"testing" // 导入测试包
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output) // expectOutput是预期结果
}
通过导入两个包来对单元进行测试,如果达成结果则完成测试
四、小项目demo(实现帖子以及回帖/评论):
再次巩固后端的分层结构,由于带我们浅尝一下go开发项目,因此没有使用数据库,使用文件来存储数据,并且去掉Client,通过返回的json直接来检验成果
由于涉及到web的开发,因此这里涉及到框架的使用gin
通过go mod init 对项目实现初始化
通过go get gopkg.in/gin-gonic/gin.v1@v1.3.9
go get指令获取gin插件
代码的实现 通过map作为索引,因为其时间复杂度为 o(1),因此在服务启动时初始化两个索引容器:~ 页面 ~ 页面附属评论
// 数据层的实现
package repository
import (
"bufio"
"encoding/json"
"os"
)
var (
topicIndexMap map[int64]*Topic
postIndexMap map[int64][]*Post
)
func Init(filePath string) error {
if err := initTopicIndexMap(filePath); err != nil {
return err
}
if err := initPostIndexMap(filePath); err != nil {
return err
}
return nil
}
func initTopicIndexMap(filePath string) error {
open, err := os.Open(filePath + "topic")
if err != nil {
return err
}
scanner := bufio.NewScanner(open)
topicTmpMap := make(map[int64]*Topic)
for scanner.Scan() {
text := scanner.Text()
var topic Topic
if err := json.Unmarshal([]byte(text), &topic); err != nil {
return err
}
topicTmpMap[topic.Id] = &topic
}
topicIndexMap = topicTmpMap
return nil
}
func initPostIndexMap(filePath string) error {
open, err := os.Open(filePath + "post")
if err != nil {
return err
}
scanner := bufio.NewScanner(open)
postTmpMap := make(map[int64][]*Post)
for scanner.Scan() {
text := scanner.Text()
var post Post
if err := json.Unmarshal([]byte(text), &post); err != nil {
return err
}
posts, ok := postTmpMap[post.ParentId]
if !ok {
postTmpMap[post.ParentId] = []*Post{&post}
continue
}
posts = append(posts, &post)
postTmpMap[post.ParentId] = posts
}
postIndexMap = postTmpMap
return nil
}