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

134 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第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," ")
   }
}

结果 image.png

并发安全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()
}

结果

image.png

依赖管理

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

依赖管理三要素

  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 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 增加需要的依赖,删除不需要的依赖

单元测试

规则

  1. 所有测试文件以 _test.go 结尾
  2. func TestXxx(*testing.T)
  3. 初始化逻辑放在TestMain中 可以用第三方包比较期望值与结果--assert

覆盖率

go test __test.go judgment.go --cover

可以测试不同情况以增加代码覆盖率

Mock

快速Mock函数

  1. 为一个函数打桩
  2. 为一个方法打桩 作用:脱离依赖进行测试

基准测试

测试程序性能与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

image.png