这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
工程进阶
携程: goroutine
运行以下代码,观察输出可知 i 恒为 5,而 j 的输出不定,在 0-4 内变化
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
fmt.Println(i, j)
}(i)
}
wg.Wait()
观察:当循环次数增加至 500 时,绝大部分 i 的输出为 500,有小于边界值的输出出现 思考:当 i 为个数级时,携程总在循环结束后执行,说明创建携程需要花费一定的时间;当 i 为百级时,携程在循环中执行,说明 goroutine 的创建资源消耗小于常规语言中线程创建的消耗(如 java,便无法复现
chan
golang 提倡通过通信(chan)实现共享内存,而不是通过共享内存实现通信
// channel 保证了并发的安全(顺序)性
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)
}
锁
在多线程程序中,锁是确保线程安全的实现中不可或缺的一环,golang 中常用的锁为 sync.Mutex
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
//time.Sleep(time.Second),不优雅
println("Without lock", x) // < 10000
x = 0
for i := 0; i < 5; i++ {
go addWithLock() // = 10000
}
time.Sleep(time.Second)
println("With lock", x)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
思考:addWithoutLock
中算得 x 数值偏小,其原因为在多携程下各携程间感知变量更新不及时所致,常用解决方案为加锁,如本例中的 sync.Mutex,亦或是 CAS 等
工程结构与包管理系统的演变
origin
在终端输入以下代码,查看当前 golang 项目的工作目录结构
[user@server $GOPATH]# du -d 1
经典的 golang 项目应该具备以下三个目录
- bin 编译的二进制文件
- pkg 编译中间产物 用于加速编译
- src 源码
传统项目结构有一个弊端,就是无法实现依赖的版本控制
Go Vendor
为了解决上述问题,go vendor 出现了。在项目创建时会额外生成 vendor
目录用于存放依赖副本,由此查找依赖顺序变为 vendor -> GOPATH。但在实际使用时仍存在弊端:go直接依赖源码,无法区分依赖的版本,依赖的项目依赖两个不兼容版本的依赖,构建失败
Go Moudle
为了彻底解决而此类问题,golang 官方推出 go moudle 工具,其实现版本控制的三要素对应如下:
- 配置文件 go.mod
- 中心仓库 Proxy
- 本地工具 go get/mod
在 go.mod 文件中,有许多标识符,其中:
- indirect 间接依赖模块标识
- /vN 主版本2+的模块会在路径后增加此兼容后缀
- incompatible 标识主版本2+且没有go.mod文件的依赖
golang 官方同样规定了语义化版本的规则:
- 正式版本 {MINOR}.${PATCH}
- commit版本 vX.0.0-yyyymmddhhmmss-abcdefgh1234
测试
在 golang 中,测试文件需以 _test.go
结尾,随即便可使用 go test xx_test.go
进行测试。
一般来说,测试代码的主函数如下
func TestMain(m *testing.M) {
// do init
code := m.Run() // 执行全部测试
// do close
os.Exit(code)
}
单元测试
单元测试的函数名以 Test 开头,通常用来单独测试某一函数的功能:
func TestHelloIllTamer(t *testing.T) {
output := HelloIllTamer()
expectOutput := "IllTamer"
//if output != expectOutput {
// t.Errorf("Not match: expected %s but %s", expectOutput, output)
//}
assert.Equal(t, expectOutput, output)
}
mock 测试
mock 在英文中是伪造的意思,他的作用在于利用反射机制替换某些如读取文件之类的函数,达到测试不受影响且幂等的结果:
func TestHelloIllTamerMock(t *testing.T) {
monkey.Patch(DoHelloIllTamer, func() string {
return "IllTamer"
})
defer monkey.Unpatch(DoHelloIllTamer)
output := HelloIllTamer()
assert.Equal(t, "IllTamer", output)
}
基准测试
基准测试是指专门用于测试函数性能的测试项,以 Benchmark 开头。使用 gobench xxx_test.go
运行测试代码中的基准测试
// 串行测试
// BenchmarkRandom-16 191787325 6.262 ns/op
func BenchmarkRandom(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
Random(10)
}
}
// 并行测试(rand函数内置锁,并发性能下降)
// BenchmarkRandomParallel-16 19625052 53.18 ns/op
func BenchmarkRandomParallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Random(10)
}
})
}
作业
作业要求在原有查询 topic 和 post 的基础上,实现对 post 的新增支持。本人实现的完整代码如下
package main
import (
"bufio"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"os"
"strconv"
"sync"
"time"
)
// 回帖后端实战
func main() {
if err := Init("./resource/04/"); err != nil {
fmt.Println(err)
os.Exit(-1)
}
engine := gin.Default()
engine.GET("/community/page/get/:id", func(context *gin.Context) {
topicId := context.Param("id")
data := QueryPageInfo(topicId)
context.JSON(200, data)
})
engine.POST("/community/page/add/:parent_id", func(context *gin.Context) {
parentId := context.Param("parent_id")
content := context.Param("content")
data := AddPost(parentId, content)
context.JSON(200, data)
})
err := engine.Run()
if err != nil {
return
}
}
var (
initParam *InitParam
initOnce sync.Once
)
type InitParam struct {
filePath string
}
func NewInitParamInstance() *InitParam {
initOnce.Do(func() {
initParam = &InitParam{}
})
return initParam
}
func Init(filePath string) error {
NewInitParamInstance().filePath = filePath
err := initTopicIndexMap(filePath)
if err != nil {
return err
}
err = initPostIndexMap(filePath)
if err != nil {
return err
}
return nil
}
// --------------------------------------------------
// controller
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{1, "topicId 解析失败", err.Error()}
}
pageInfo, err := DoQueryPageInfo(topicId)
if err != nil {
return &PageData{2, "查询失败", err.Error()}
}
return &PageData{0, "查询成功", pageInfo}
}
func AddPost(parentIdStr string, content string) *PageData {
if len(content) == 0 {
return &PageData{1, "content can not be null", nil}
}
parentId, err := strconv.ParseInt(parentIdStr, 10, 64)
if err != nil {
return &PageData{1, "parentId 解析失败", err.Error()}
}
newId, err := DoAddPost(parentId, content, time.Now().Unix())
if err != nil {
return &PageData{2, "Add failed", err.Error()}
}
return &PageData{0, "Success", newId}
}
// --------------------------------------------------
// service
type PageInfo struct {
Topic *Topic
PostList []*Post
}
type QueryPageInfoFlow struct {
topicId int64
pageInfo *PageInfo
topic *Topic
posts []*Post
}
type AddPostFlow struct {
parentId int64
content string
createTime int64
postId int64
}
func DoQueryPageInfo(topicId int64) (*PageInfo, error) {
return NewQueryPageInfoFlow(topicId).Do()
}
func DoAddPost(parentId int64, content string, createTime int64) (int64, error) {
return NewAddPostFlow(parentId, content, createTime).Do()
}
func NewQueryPageInfoFlow(topId int64) *QueryPageInfoFlow {
return &QueryPageInfoFlow{
topicId: topId,
}
}
func NewAddPostFlow(parentId int64, content string, createTime int64) *AddPostFlow {
return &AddPostFlow{
parentId: parentId,
content: content,
createTime: createTime,
}
}
func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
if err := f.checkParam(); 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
}
func (f *QueryPageInfoFlow) checkParam() error {
// do nothing
return nil
}
func (f *QueryPageInfoFlow) prepareInfo() error {
var wg sync.WaitGroup
wg.Add(2)
// 获取 topic 信息
go func() {
defer wg.Done()
topic := NewTopicDaoInstance().QueryTopicById(f.topicId)
f.topic = topic
}()
// 获取 post 列表
go func() {
defer wg.Done()
posts := NewPostDaoInstance().QueryPostListById(f.topicId)
f.posts = posts
}()
wg.Wait()
return nil
}
func (f *QueryPageInfoFlow) packPageInfo() error {
if f.topic == nil || f.posts == nil {
return fmt.Errorf("not found")
}
f.pageInfo = &PageInfo{
Topic: f.topic,
PostList: f.posts,
}
return nil
}
func (f *AddPostFlow) Do() (int64, error) {
if err := f.checkParam(); err != nil {
return -1, err
}
if err := f.preparePostId(); err != nil {
return -1, err
}
if err := f.addPost(); err != nil {
return -1, err
}
return f.postId, nil
}
func (f *AddPostFlow) checkParam() error {
topic := NewTopicDaoInstance().QueryTopicById(f.parentId)
if topic == nil {
return fmt.Errorf("can't find topic %d", f.parentId)
}
return nil
}
func (f *AddPostFlow) preparePostId() error {
lock.Lock()
f.postId = NewPostDaoInstance().GetAndRefreshPostId()
lock.Unlock()
return nil
}
// preparePostId has attached lock, so it's unnecessary to lock this
func (f *AddPostFlow) addPost() error {
post := Post{f.postId, f.parentId, f.content, f.createTime}
err := NewPostDaoInstance().AddPost(&post)
if err != nil {
return err
}
lock.Lock()
// 需确保操作的原子性
err = NewPostDaoInstance().RefreshPostMap(&post)
lock.Unlock()
if err != nil {
return err
}
return nil
}
// --------------------------------------------------
// repository
type TopicDao struct {
}
type PostDao struct {
idIndex int64
}
var (
topicDao *TopicDao
topicOnce sync.Once
lock sync.Mutex
)
var (
postDao *PostDao
postOnce sync.Once
)
func NewTopicDaoInstance() *TopicDao {
topicOnce.Do(func() {
topicDao = &TopicDao{}
})
return topicDao
}
func NewPostDaoInstance() *PostDao {
postOnce.Do(func() {
postDao = &PostDao{}
})
return postDao
}
// QueryTopicById 根据 topicId 查询话题
func (*TopicDao) QueryTopicById(topicId int64) *Topic {
return topicIndexMap[topicId]
}
// QueryPostListById 根据 topicId 查询帖子列表
func (*PostDao) QueryPostListById(topicId int64) []*Post {
return postIndexMap[topicId]
}
func (dao *PostDao) GetAndRefreshPostId() int64 {
dao.idIndex++
return dao.idIndex
}
func (*PostDao) AddPost(post *Post) error {
filePath := NewInitParamInstance().filePath
open, err := os.Open(filePath + "post")
if err != nil {
return err
}
writer := bufio.NewWriter(open)
buf, err := json.Marshal(post)
_, err = writer.Write(buf)
if err != nil {
return err
}
return nil
}
func (*PostDao) RefreshPostMap(post *Post) error {
posts := postIndexMap[post.ParentId]
posts = append(posts, post)
postIndexMap[post.ParentId] = posts
return nil
}
type Topic struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
type Post struct {
Id int64 `json:"id"`
ParentId int64 `json:"parent_id"`
Content string `json:"content"`
CreateTime int64 `json:"create_time"`
}
var (
// topicId -> Topic
topicIndexMap map[int64]*Topic
// parentId -> []Post
postIndexMap map[int64][]*Post
)
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
err := json.Unmarshal([]byte(text), &topic)
if 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
}
postTmpMap := make(map[int64][]*Post)
// 逐行读写
scanner := bufio.NewScanner(open)
instance := NewPostDaoInstance()
for scanner.Scan() {
text := scanner.Text()
var post Post
err := json.Unmarshal([]byte(text), &post)
if err != nil {
return err
}
posts, ok := postTmpMap[post.ParentId]
if !ok { // key 不存在
postTmpMap[post.ParentId] = []*Post{}
}
posts = append(posts, &post)
postTmpMap[post.ParentId] = posts
if post.Id > instance.idIndex {
instance.idIndex = post.Id
}
}
postIndexMap = postTmpMap
return nil
}