go工程实践|青训营笔记

74 阅读2分钟

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

go工程实践

并发与并行

goroutine

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

// 为调用的函数创建一个协程来执行
//使用go关键字

func HelloGoRoutine() {
    for i:=0;i < 5>; i++ {
        go func(j int) {  // 创建协程
            hello(j)  // 被调用的函数
        }(i)
    }
}

go中协程间的通信

go提倡通过通信共享内存,而不是通过共享内存来完成通信 通信的方式:通道(channel)

go中的通道channel

make(chan 元素类型,【缓冲大小】)
- 无缓冲通道  make(chan int)
- 有缓冲通道  make(chan int, 2)
// A子协程发送0~9数字
// B子协程计算输入数字的平方
// 主协程输出最后的平方数
func CalSquare() {
    src := make(chan int)
    dest := make(chan int, 3)
    go func() {  // A子协程
        defer close(src)
        for i := 0; i < 10; i++ {
            sec <- i
        }
    }()
    go func() {
        defer close(dest)
        for i := range(src) {
            dest <- i * i
        }
    }()
    for i := range(dest) {
        fmt.Println(i)
    }
}

并发安全 Lock

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()
    }
    fmt.Println("withoutlock", x)
    x = 0
    for i := 0; i < 5>; i++ {
        go addWithLock()
    }
    fmt.Println("withlock", x)
}

使用计数器来搞定不知道子线程何时结束

WaitGroup包

func ManyGoWait() {
    var wg sync.WaitGroup
    wg.Add(5)  // 计数器初始化为5,因为 
    for i := 0; i < 5; i++ {
        go func(j int) {
            defer wg.Done()
            hello(j)
        }(i)

    }
    wg.Wait()
}

工程项目

gopath

gopath是有弊端的,有兼容问题,所有的源码都处于src目录下(bin目录为编译后的二进制文件,pkg目录为项目中编译的中间产物,加速编译 )。如果多个项目依赖不同版本的package,那么gopath无法做到

govendor

依旧依靠的是源码,无法控制依赖的版本

gomodule

通过go.mod文件管理依赖包版本 通过go get/go mod指令工具管理依赖包

go get

go get exanple.org/plg @maseter 获取分支的最新commit

go mod

go mod init 初始化,创建go.mod文件
go download 下载模块到本地缓存
go mod tidy 增加需要的依赖,删除不需要的依赖

测试

测试的种类

  1. 回归测试 QA测试人员
  2. 集成测试 自动化测试
  3. 单元测试 开发人员

单元测试的规则

  1. 所有测试文件以_test.go结尾
  2. 测试函数为func TestXxxx(*testing.T)
  3. 初始化逻辑放到TestMain中
import (
    "github.com/stretchr/testify/assert"
    "testing"
)

// 被测试的函数
func HelloTom() string {
    return "Jerry"
}

//测试函数
func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    assert.Equal(t, output, expectOutput)
}

单元测试-覆盖率

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

单元测试-mock

打桩函数:采用替换的思想,将原始数据替换成虚拟的数据

package test

import (
 "bou.ke/monkey"
 "github.com/stretchr/testify/assert"
 "testing"
)

func TestProcessFirstLine(t *testing.T) {
 firstLine := ProcessFirstLine()
 assert.Equal(t, "line00", firstLine)
}

func TestProcessFirstLineWithMock(t *testing.T) {
 monkey.Patch(ReadFirstLine, func() string {
  return "line110"
 })
 defer monkey.Unpatch(ReadFirstLine)
 line := ProcessFirstLine()
 assert.Equal(t, "line000", line)
}

项目实战

需求描述

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

组件工具

介绍下开发涉及的基础组件和工具,首先是gin, 高性能开源的go web框架,我们基于gin搭建web服务器,在课程手册应该提到了,这里我们只是简单的使用,主要涉及路由分发,不会涉及其他复杂的概念。

因为我们引入了web框架,所以就涉及go module依赖管理,如前面依赖管理课程内容讲解,我们首先通过go mod是初始化go mod管理配置文件,然后go get下载gin依赖,这里显示用了V1.3.0版本。

有了框架依赖,我们只需要关注业务本身的实现,从reposity --> service  --> contoller我们一步步实现。希望大家能跟上我的节奏,从0~1 实现这个项目,如果时间问题,大家可以一步步copy一下,主要是走一半开发思路。

分层结构

整体分为三层,repository数据层,service逻辑层,controller视图层 数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件, 通过文件操作拉取话题, 帖子数据;数据层面向逻辑层,对service层透明, 屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接模型是不变的。

Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层;

Cortroller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好,

各层的代码逻辑和Java相类似,可以采用协程的方式提高性能

大家在后期做项目开发中,一定要思考流程是否可以并,通过压榨CPU,降低接口耗时,不要一味的串行实现, 浪费多核cpu的资源。

Router

构建路由,将web访问路径与controller的方法相绑定