这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
语言进阶
Go语言多协程Goroutine
并发和并行
并发是多线程程序在一个核的cpu上运行(时间片切换)
并行是多线程程序在多个核的cpu上运行
协程: 用户态,轻量级线程,栈MB级别。
线程: 内核态,线程跑多个协程,栈KB级别。
Goroutine:
1、通过通信共享内存 (√)
2、通过共享内存实现通信
Channel
make(chan 元素类型,[缓冲大小])
无缓冲通道:导致发送协程与接收协程同步化,同步通道
make(chan int)
有缓冲通道: 参数二为数量 可以防止channel中通信接收阻塞通道,超出空间仍会堵塞
make(chan int,2)
//样例
func CalSquare() {
src := make(chan int)
//有缓冲队列: dest消耗速度较慢 src生产快
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 {
//复杂操作
fmt.Print(i," ")
}
}
结果
并发安全Lock
lock sync.Mutex
func(){
lock.lock()
//操作
lock.unlock()
}
实际开发应该避免对共享内存的操作
WaitGroup
内部维护了计数器 存在方法:
- Add() 计数器+n (n>0)
- Done() 计数器-1
- Wait() 等待计数器至0
//样例
func hello(i int) {
println("hello goroutine: ", +i)
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
wg.Done()
}(i)
}
wg.Wait()
}
结果
依赖管理
GOPATH---->Go Vendor---->Go Module
GOPATH
- bin 项目编译的二进制文件
- pkg 项目编译的中间产物,加速编译
- src 项目源码
项目代码直接依赖src下的代码
go get 下载最新版本的包到src目录
缺点: - 无法实现package的多版本控制
Go Vendor
- 项目目录下增加vendor文件,所有依赖包副本形式放在&ProjectRoot/vender 依赖寻址: vendor => GOPATH 缺点:
- 无妨控制依赖的版本、
- 更新项目又可能出现依赖冲突,导致编译错误
Go Module
- 通过go.mod文件管理依赖包版本
- 通过 go get/go mod指令工具管理依赖包 Go Module -- 定义版本规则 通过工具管理羡慕依赖关系 储存位置: GOPATH
依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
依赖配置 go.mod
module github.com/wangkechun/go-by-example //依赖管理单元
go 1.18 //原生库
//单元依赖
require (
modulepath version (//indirect)
)
依赖配置 version
语义化版本
${MAJOR}.${MINOR}.${PATCH}
v2.3.0
v大版本不兼容.新增前后兼容.代码bug修复
基于commit伪版本
vx.0.0-时间戳-哈希校验码
依赖配置 indirect
//非直接依赖
require(
modulepath version //indirect
)
依赖配置 incompatible
没有go.mod文件并且主版本2+的依赖 在version后+incomatible
标识出可能存在不兼容代码逻辑
依赖分发 Proxy
从Proxy拉取它缓存的依赖
依赖分发 变量GOPRoxy
GOPROXY="url1,url2,direct(源站)"
工具
go get ample.rg/pkg
@update 默认major最新版
@none 删除依赖
@vx.x.x tag版本语义
@xxxxx 定的commit
@master 分支的最新版
go mod
init 初始化 创建go.mod文件
download 下载模块到本地缓存
tidy 增加需要的依赖,删除不需要的依赖
单元测试
规则
- 所有测试文件以 _test.go 结尾
- func TestXxx(*testing.T)
- 初始化逻辑放在TestMain中 可以用第三方包比较期望值与结果--assert
覆盖率
go test __test.go judgment.go --cover
可以测试不同情况以增加代码覆盖率
Mock
快速Mock函数
- 为一个函数打桩
- 为一个方法打桩 作用:脱离依赖进行测试
基准测试
测试程序性能与cpu损耗
func BenchmarkXxxx(b *testing .B){
}
项目实践
分层结构
- 数据层 :数据model,外部数据的增删改查
- 逻辑层 :业务Entity,处理核心业务逻辑输出
- 视图层 :视图view,处理和外部的交互逻辑
data
以本地文件的形式储存数据,再由数据层进行操作
topic数据文件
{"id":1,"title":"青训营来啦!","content":"小姐姐,快到碗里来~","create_time":1650437625}
{"id":2,"title":"青训营来啦!","content":"小哥哥,快到碗里来~","create_time":1650437640}
post数据文件
{"id":1,"parent_id":1,"content":"小姐姐快来1","create_time":1650437616}
{"id":2,"parent_id":1,"content":"小姐姐快来2","create_time":1650437617}
{"id":3,"parent_id":1,"content":"小姐姐快来3","create_time":1650437618}
{"id":4,"parent_id":1,"content":"小姐姐快来4","create_time":1650437619}
{"id":5,"parent_id":1,"content":"小姐姐快来5","create_time":1650437620}
{"id":6,"parent_id":2,"content":"小哥哥快来1","create_time":1650437621}
{"id":7,"parent_id":2,"content":"小哥哥快来2","create_time":1650437622}
{"id":8,"parent_id":2,"content":"小哥哥快来3","create_time":1650437623}
{"id":9,"parent_id":2,"content":"小哥哥快来4","create_time":1650437624}
{"id":10,"parent_id":2,"content":"小哥哥快来5","create_time":1650437625}
数据层Repository
db_init.go
提供初始化操作,读取本地文件,以map类型生成对应索引 即topicIndexMap与postIndexMap
package repository
import (
"bufio"
"encoding/json"
"os"
)
//数据储存
var (
//储存主题数据指针的map
topicIndexMap map[int64]*Topic
//储存帖子数据指针列表的map
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
//将json字符串转换为数据结构
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
//将json字符串转换为数据结构
if err := json.Unmarshal([]byte(text), &post); err != nil {
return err
}
posts, ok := postTmpMap[post.ParentId]
//若不存在 则初始化ParentId,[]*Post
if !ok {
postTmpMap[post.ParentId] = []*Post{&post}
continue
}
posts = append(posts, &post)
postTmpMap[post.ParentId] = posts
}
postIndexMap = postTmpMap
return nil
}
topic.go
以单例模式获取TopicDao,对topicIndexMap进行操作
package repository
import "sync"
type Topic struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type TopicDao struct {
}
var (
topicDao *TopicDao
//只会执行一次 单例模式
topicOnce sync.Once
)
//生成topicDao
func NewTopicDapInstance() *TopicDao {
//只会执行一次
topicOnce.Do(func() {
topicDao = &TopicDao{}
})
return topicDao
}
//查询操作
func (*TopicDao) QueryTopicById(id int64) *Topic {
return topicIndexMap[id]
}
post.go
以单例模式获取postDao,对postIndexMap进行操作
package repository
import "sync"
type Post struct {
Id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type PostDao struct {
}
var (
postDao *PostDao
postOnce sync.Once
)
func NewPostInstance() *PostDao {
postOnce.Do(func() {
postDao = &PostDao{}
})
return postDao
}
func (*PostDao) QueryPostsByParentId(parentId int64) []*Post {
return postIndexMap[parentId]
}
逻辑层Service
query_page_info.go
提供查询方法
流程:传入topicId,初始化NewQueryPageInfoFlow,调用初始化NewQueryPageInfoFlow.Do()方法(参数检验-->多协程获取Repository层数据-->将获取到的数据传入初始化NewQueryPageInfoFlow.PageInfo返回)
package service
import (
"errors"
"example.com/repository"
"sync"
)
//返回页面信息
type PageInfo struct {
Topic *repository.Topic
PostList []*repository.Post
}
//查询页面信息by topicId
func QueryPageInfo(topicId int64) (*PageInfo, error) {
return NewQueryPageInfoFlow(topicId).Do()
}
func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
return &QueryPageInfoFlow{
topicId: topId,
}
}
//查询页面信息
type QueryPageInfoFlow struct {
topicId int64
pageInfo *PageInfo
topic *repository.Topic
posts []*repository.Post
}
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
if err := f.checkParm(); err != nil {
return nil, err
}
if err := f.prepareInfo(); err != nil {
return nil, err
}
if err := f.packPageInfo(); err != nil {
return nil, err
}
return f.pageInfo, nil
}
//检验传入topicId
func (f *QueryPageInfoFlow) checkParm() error {
if f.topicId <= 0 {
return errors.New("topic id must be larger than 0")
}
return nil
}
//准备PageInfo
func (f *QueryPageInfoFlow) prepareInfo() error {
//多协程获取值
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
topic := repository.NewTopicDapInstance().QueryTopicById(f.topicId)
f.topic = topic
}()
go func() {
defer wg.Done()
posts := repository.NewPostInstance().QueryPostsByParentId(f.topicId)
f.posts = posts
}()
wg.Wait()
return nil
}
//打包PageInfo
func (f *QueryPageInfoFlow) packPageInfo() error {
f.pageInfo = &PageInfo{
Topic: f.topic,
PostList: f.posts,
}
return nil
}
视图层Controller
传入topicId,返回PageData
package controller
import (
"example.com/service"
"strconv"
)
//返回信息
type PageData struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
//查询返回数据
func QueryPageInfo(topicIdStr string) *PageData {
//传入参数转化
topicId, err := strconv.ParseInt(topicIdStr, 10, 64)
//传入参数错误
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
//调用service层方法查询
pageInfo, err := service.QueryPageInfo(topicId)
//查询错误
if err != nil {
return &PageData{
Code: -1,
Msg: err.Error(),
}
}
//正常返回
return &PageData{
Code: 0,
Msg: "success",
Data: pageInfo,
}
}
Router
通过gin搭建web框架
package main
import (
"example.com/controller"
"example.com/repository"
"github.com/gin-gonic/gin"
"os"
)
func main() {
//1.初始化数据索引
if err := Init("./data/"); err != nil {
os.Exit(-1)
}
//2.初始化引擎配置
r := gin.Default()
//3.构建路由
r.GET("/community/page/get/:id", func(context *gin.Context) {
//获取path变量获取topicId
topicId := context.Param("id")
//调用controller层查询
data := controller.QueryPageInfo(topicId)
//json化返回数据
context.JSONP(200, data)
})
err := r.Run()
if err != nil {
return
}
}
//初始化本地data
func Init(filePath string) error {
if err := repository.Init(filePath); err != nil {
return err
}
return nil
}
运行
go run server.go
通过http://localhost:8080/community/page/get/:id访问接口
\\通过curl访问
curl url || json