GO语言上手与工程实践 | 青训营笔记

121 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。主要内容为对第一次与第二次课程的学习总结。

go语言基础

相关内容已总结在上一篇笔记中juejin.cn/post/709480…

go并发

  • Goroutine

在Go语言中,每一个并发的执行单元叫作一个goroutine。和操作系统的线程调度区别在于,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。

Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数。

  • Channel

Channel类似一个管道,方便并发核心单元通讯。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。

创建一个channel:

ch := make(chan int) // ch has type 'chan int' 无缓冲
ch := make(chan int,2)  // 有缓冲

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。

带缓存的Channel内部持有一个元素队列。向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。

关闭一个channel:

close(ch)
  • Lock 声明:
lock sync.Mutex
lock.Lock()
/* code */
lock.Unlock()
  • WaitGroup

Add(delta int)计数器+delta

Done()计数器-1

Wait()阻塞直到计数器为0。

依赖管理

  • Go Module

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

Proxy 管理依赖库

go get/mod指令管理依赖包

测试

  • 单元测试
  1. 测试文件以_test.go结尾
  2. 测试函数func TestXxx(t *testing.T)
  3. 初始化逻辑放到TestMain

覆盖率 go test xxx_test.go xxx.go --cover

项目实践

项目地址:github.com/Moonlight-Z…

该项目用到了一个web框架gin,项目结构采用了Repository、Service、Controller三层架构。

repositorydb_init.go初始化数据库、post.go实现新回帖时保存数据。

service:具体实现请求页面和发布的功能

cotrollerquery_page_info.go用来请求获得页面信息,publish_post.go用来给帖子回帖。

课后作业

  1. 支持发布帖子

  2. 本地 ID 生成需要保证不重复、唯一性

  3. Append 文件,更新索引,注意 Map 的并发安全问题

实现如下:

(1)修改server.go,在main函数中支持发布帖子:

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

(2)在cotroller层新增一个文件publish_topic.go

package cotroller

import (
	"github.com/Moonlight-Zhao/go-project-example/service"
)


func PublishTopic(title, content string) *PageData {
	topicId, err := service.PublishTopic(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,
		},
	}

}

(3)在service层新增一个文件publish_topic.go

package service

import (
	"errors"
	"time"
	"unicode/utf16"
	"github.com/Moonlight-Zhao/go-project-example/repository"
)

func PublishTopic(title, content string) (int64, error) {
	return NewPublishTopicFlow(title, content).Do()
}

func NewPublishTopicFlow(title, content string) *PublishTopicFlow {
	return &PublishTopicFlow{
        title: title,
		content: content,
	}
}

type PublishTopicFlow struct {
    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 len(utf16.Encode([]rune(f.content))) >= 500 {
		return errors.New("content length must be less than 500")
	}
	return nil
}

func (f *PublishTopicFlow) publish() error {
	topic := &repository.Topic{
        Title:      f.title,
		Content:    f.content,
		CreateTime: time.Now().Unix(),
	}
	id, err := idGen.NextId()
	if err != nil {
		return err
	}
	topic.Id = id
	if err := repository.NewTopicDaoInstance().InsertTopic(topic); err != nil {
		return err
	}
	f.topicId = topic.Id
	return nil
}

(4)repository在topic.go中新增函数InsertTopic

func (*TopicDao) InsertTopic(topic *Topic) error {
	f, err := os.OpenFile("./data/topic", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
	if err != nil {
		return err
	}

	defer f.Close()
	marshal, _ := json.Marshal(topic)
	if _, err = f.WriteString(string(marshal)+"\n"); err != nil {
		return err
	}

	rwMutex.Lock()
	_, ok := topicIndexMap[topic.Id]
	if !ok {
		topicIndexMap[topic.Id] = topic
	}
	rwMutex.Unlock()
	return nil
}