Go 语言进阶 - 工程进阶 | 青训营笔记

41 阅读3分钟

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

重点内容

  1. 并行 VS 并发
  2. 依赖管理
  3. 测试

知识点介绍

  • Goroutine

    协程:用户态,轻量级线程,栈MB级别

    线程:内核态,线程跑多个协程,栈KB级别

    快速 打印hello goroutine :0 ~ hello goroutine : 4

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)
}
// 结果为乱序输出
// 说明并行
  • CSP (Communicating Sequential Processes)

    提倡通过 通信共享内存 而不是通过共享内存实现通信

  • Channel

    make(chan 元素类型,[缓冲大小])

// 无缓冲通道 也被称为同步通道 
make(chan int)
// 有缓冲通道
make(chan int,2)
  • 并发安全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
    }
}
  • WaitGroup
func hello(i int){
    println("hello goroutine : " + fmt.Sprint(i))
}
​
func HelloGoRoutine(){
    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()
}
  • Go Module

    通过go.mod 文件管理依赖包版本

    通过go get/go mod 指令工具管理依赖包

  • 依赖管理三要素

    配置文件,描述依赖 go.mod

    中心仓库管理依赖库 Proxy

    本地工具 go get/mod

  • go mod

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

    回归测试(手动、终端回归固定场景

    集成测试(自动化回归测试,集成一个功能维度

    单元测试(开发阶段对模块进行测试

  • 单元测试规则

    所有测试文件都以_test.go结尾

    func TestXxx(t *testing.T)

    初始化逻辑放入TestMain中

    func TestMain(m *testing.M){
        
        // 测试前:数据装载、配置初始化等前置工作
        code := m.Run()
        // 测试后:释放资源等收尾工作
        os.Exit(code)
    }
    
  • 单元测试通过覆盖率判断是否完善

    go test Xxx_test.go Xxx.go --cover

    一般覆盖率:50%~60%,较高覆盖率80%+

    测试分支相互独立、全面覆盖

    测试单元粒度足够小函数单一职责

  • Mock

    为一个函数打桩

// Patch replaces a fucntion with another
func Patch(target, replacement interface{}) *PatchGuard {
    t := reflect.ValueOf(target)
    r := reflect.ValueOf(replacement)
    patchValue(t,r)
    return &PatchGuard{t,r}
}
​
// Unpatch removes any monkey patches on target
// returns whether target was patched in the first place
func Unpatch(target interface{}) bool {
    return unpatchValue(reflect.ValueOf(target))
}
monkey.Patch(ReadFirstLine, func() string {
    return "line110"
}) // 对ReadFirstLine函数进行打桩使其一直返回line110
  • 分层结构

    数据层(Repository):数据Model,外部数据的增删改查

    逻辑层(Service):业务Entity,处理核心业务逻辑输出

    视图层(Controller):视图View,处理和外部的交互逻辑

实践

社区话题页面

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

课后总结

主要是通过展示话题项目,对开发流程的简单介绍。

作业

Handler

func PublishTopic(uidStr, title, content string) *PageData {
    uid, _ := strconv.ParseInt(uidStr, 10, 64)
​
    topicId, err := service.PublishTopic(uid, title, content)
    if err != nil {
        return &PageData{
            Code: -1,
            Msg:  err.Error(),
        }
    }
    return &PageData{
        Code:0,
        Msg: "success",
        Data:map[string]int64{
            "topic_id":topicId,
        },
    }
}

Route

r.POST("/community/topic/do", func(c *gin.Context) {
        uid, _ := c.GetPostForm("uid")
        title, _ := c.GetPostForm("title")
        content, _ := c.GetPostForm("content")
        data := handler.PublishTopic(uid, title, content)
        c.JSON(200, data)
    })

Repository

func (*TopicDao) CreateTopic(topic *Topic) error {
    if err := db.Create(topic).Error; err != nil {
        util.Logger.Error("insert topic err:" + err.Error())
        return err
    }
    return nil
}

Service

publish_topic

func PublishTopic(userId int64, title, content string) (int64 error) {
    return NewPublishTopicFlow(userId, title, content).Do()
}
​
func NewPublishTopicFlow(userId int64, title, content string) *PublishTopicFlow {
    return &PublishTopicFlow{
        userId:  userId,
        title:   title,
        content: content,
    }
}
​
type PublishTopicFlow struct {
    userId  int64
    title   string
    content string
​
    topicId int64
}
​
func (f *PublishTopicFlow) Do() (int64, error) {
    if err := f.checkParam(); err != nil {
        return 0, err
    }
    if err := f.publish(); err != nil {
        return 0, err
    }
    return f.topicId, nil
}
​
func (f *PublishTopicFlow) checkParam() error {
    if f.userId <= 0 {
        return errors.New("userId id must be larger than 0")
    }
    if utf8.RuneCountInString(f.title) >= 30 {
        return errors.New("title length must be less than 30")
    }
    if utf8.RuneCountInString(f.content) >= 500 {
        return errors.New("content length must be less than 500")
    }
    return nil
}
​
func (f *PublishTopicFlow) publish() error {
    topic := &repository.Topic{
        UserId:     f.userId,
        Title:      f.title,
        Content:    f.content,
        CreateTime: time.Now(),
    }
    if err := repository.NewTopicDaoInstance().CreateTopic(topic); err != nil {
        return err
    }
    f.topicId = topic.Id
    return nil
}
​

publish_topic_test

func TestPublishTopic(t *testing.T) {
    // 生成超出长度的字符串进行测试
    errString1 := getFixedLenString("测试", 40, 't')
    errString2 := getFixedLenString("测试", 550, 't')
    type args struct {
        userId  int64
        title   string
        content string
    }
    tests := []struct {
        name    string
        args    args
        wantErr bool
    }{
        {
            name: "测试发布帖子1",
            args: args{
                userId:  1,
                title:   "用户1测试发布帖子",
                content: "用户1测试发布帖子",
            },
            wantErr: false,
        },
        {
            name: "测试发布帖子2",
            args: args{
                userId:  0,
                title:   "用户1测试发布帖子",
                content: "用户1测试发布帖子",
            },
            wantErr: true,
        },
        {
            name: "测试发布帖子3",
            args: args{
                userId:  1,
                title:   errString1,
                content: "用户1测试发布帖子",
            },
            wantErr: true,
        },
        {
            name: "测试发布帖子4",
            args: args{
                userId:  1,
                title:   "用户1测试发布帖子",
                content: errString2,
            },
            wantErr: true,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := PublishTopic(tt.args.userId, tt.args.title, tt.args.content)
            if (err != nil) != tt.wantErr {
                t.Errorf("PublishTopic() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
        })
    }
}
​
func getFixedLenString(str string, length int, char byte) string {
    if len(str) == 0 {
        return ""
    }
​
    if len(str) == length {
        return str
    }
​
    //超出切后面
    if len(str) > length {
        return string(str[:length])
    }
​
    //缺少添加char
    if len(str) < length {
        slice := make([]byte, length-len(str))
        for k := range slice {
            slice[k] = char
        }
        return string(append(slice, []byte(str)...))
    }
​
    return ""
}
​