需求描述
-
发布话题和回帖
-
本地ID生成需要保证不重复,唯一性
-
Append文件,更新索引
框架(Gin)
官方介绍
Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架, 优于 httprouter,速度提高了近 40 倍。如果你需要极好的性能,使用 Gin 吧。
引入Gin框架
1.下载并安装 gin:
shell
$ go get -u github.com/gin-gonic/gin
2.将 gin 引入到代码中:
arduino
import "github.com/gin-gonic/gin"
源码介绍
本项目相关代码已上传至github message_board
目录结构
controller视图层
主要负责处理用户输入的数据,并相应相应数据,包含两个文件
-
publish.go用户发布数据的接口,包括发布话题,以及回复
-
query_page_info.go用户查看数据的接口,可查看话题和对应的回复
data数据库
存储数据的文件,因为只是一个简单的项目,所以并没有用到数据库,而是使用文件存储json格式的文件,包含两个文件
post: 所有回复的信息topic:所有话题的信息
repository 数据层
负责数据库的增删改查,为上层提供了简单易用的接口,包含三个文件
-
db_init.go从文件中读取出所有的数据,并存储到对应的Map中,便于后续的查找 -
post.go- 实现了通过话题id查找对应回复
- 实现了新增回复
- 实现了查找id的最大值(用于实现唯一id)
-
topic.go- 实现了通过id查找对应话题
- 实现了新增话题
- 实现了查找id的最大值(用于实现唯一id)
service
处理业务逻辑,对数据层的数据进行处理,并将结果返回给视图层,只包含一个文件
query_page_info.go: 查找话题及相应回复
server.go
- 程序入口,定义路由,处理路由,调用接口响应请求;
功能实现
实现思路
1.获取数据
发布话题首先要获取用户要发布的内容,包括标题和内容,如果是回复的话,需要有回复话题的id还有内容,我在设置路由时,获取到了用户的输入,校验完数据后传给controller进行处理
2.处理数据
获取完数据后,需要对数据进行封装处理,使其成为一个结构体,需要给它设置一个id,还有时间戳, 时间戳可以通过time.now.Unix()获取
3.获取唯一id
我的实现思路是,找到所有id中的最大值,然后加一,
4.保存数据
将结构体序列化为JSON格式,追加到文件末尾,同时更新索引
- 若是发表话题,直接新增索引就可以
- 若是发表回复,需要先判断,当前话题有没有回复,没有的话需要新创建一个数组
具体实现
在server.go定义了两个路由,一个是发布话题/commuity/page/publish/topic,一个是发表回复/commuity/page/publish/topic,两个都是post方法,因为没有实现前端,所以这里使用postman进行接口测试,这两个路由主要是接受用户输入的信息,然后交给controller处理,这里涉及到gin获取post参数的方式
我认为,用户发表话题前端大概率是表单方式提交,所以这里默认请求参数为Form表单,使用PostForm方法可以获取对应参数,方法参数为表单控件的名称
此处参考了掘金的一篇文章Gin框架获取请求参数的各种方式详解
go
r.POST("/community/page/publish/topic", func(context *gin.Context) {
title := context.PostForm("title")
content := context.PostForm("content")
data, err2 := cotroller.PublishTopic(title, content)
if err2 != nil {
return
}
context.JSON(200, data)
})
获取到用户的数据后,我们要对其进行封装,首先要找到id的最大值,在读文件时,我们将所有话题的信息存储到了topicIndexMap中,这是一个map的数据结构,key为id,value为话题信息的结构体Topic 遍历所有的key,找到最大值,再加一就是我们新插入值的id
go
func (*TopicDao) FindMaxId() int64 {
id := int64(math.MinInt64)
for k, _ := range topicIndexMap {
if k > id {
id = k
}
}
return id
}
若是发表回复的话,由于postIndexMap是一个树形的结构,不方便我们查找最大值,所以从文件读取数据时,我们将所有回复信息都保存到了一个数组中,然后再遍历数组构造Map,有了这个数组,就可以很简单的实现查找最大值
有了id后,就可以对用户输入的信息进行封装了,由于逻辑比较简单,我们直接在controller层实现了,
go
func PublishTopic(title string, content string) (topic repository.Topic, err error) {
id := repository.NewTopicDaoInstance().FindMaxId() + 1
time := time.Now().Unix()
topic, err = repository.NewTopicDaoInstance().CreateTopic(id, title, content, time)
if err != nil {
return topic, err
}
return topic, nil
}
数据封装好需要存储到文件中,并更新索引
封装数据
go
newTopic := Topic{
Id: id,
Title: title,
Content: content,
CreateTime: createTime,
}
更新索引
go
topicIndexMap[newTopic.Id] = &newTopic
保存到文件
go
f, err := os.OpenFile("./data/topic", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) //打开文件
if err != nil {
return newTopic, err
}
defer f.Close() //在函数执行完之后关闭文件
marshal, _ := json.Marshal(newTopic) //序列化
if _, err = f.WriteString(string(marshal) + "\n"); err != nil { //写入文件
return newTopic, err
}
保存文件时,由于默认每条数据后会换行,会导致下次读数据时读到空行报错Initunexpected end of JSON input,因此在读文件时,我们需要加一个判断,若当前行为空字符串"",直接结束文件读取
并发和Goroutine
并发和并行的区别
并发是指一个时间段内,有几个程序都在同一个CPU上运行,但任意一个时刻点上只有一个程序在处理机上运行。 (并发,指的是多个事情,在同一时间段内同时发生了。)
并行是指一个时间段内,有几个程序都在几个CPU上运行,任意一个时刻点上,有多个程序在同时运行,并且多道程序之间互不干扰。 (并行,指的是多个事情,在同一时间点上同时发生了。)
并发的多个任务之间是互相抢占资源的。 并行的多个任务之间是不互相抢占资源的。 只有在多CPU的情况中,才会发生并行。 否则,看似同时发生的事情,其实都是并发执行的
线程与协程的区别
线程:是比进程更小粒度的运行单位,存在于内核态,需要操作系统来调度,内存消耗是MB级别。
协程:是比线程更小的粒度,通过m:n的比例在一个线程中再细分出来的单位,存在于用户态,用户可以自由调度,内存消耗是KB级别。
协程对比线程的优势:
- 存在于用户态,可操作性强,调度可由自己控制。
- 更轻量,所需资源更少。
Goroutine
go语言的go关键字跑的就是协程,我们称为goroutine。
关于协程背后更多的故事,可以看这个视频 go协程实现原理 ,我们这里只讲简单使用。
用法
简单用法如下:
go
package main
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func main() {
//go的风格来说一般都喜欢运行一个闭包
go func(j int) {
hello(j)
}(i)
}
并发的通信
并发程序之间的通信,一般都是通过共享内存的形式实现通信,临界区一般需要加锁保护。
而go语言采取的是通过通信来实现共享内存,这个过程是反过来的,但用起来更为直观。
Channel
通过内置函数 make 可以得到两种类型的 channel 。
注意:channel是类似于引用的一个类型,如果直接通过var声明定义是没法初始化得到内部内存的,故记得通过make创建channel。还有就是记得不用的时候关闭。
channel的使用
channel的简单使用如下:
go
func main() {
var src chan int
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 {//消费者1
dest <- i * i
}
}()
for i := range dest {//消费者2
println(i)
}
}
使用带缓冲channel的好处
在一个生产者消费者模型中,生产者的生产效率远高于消费者,那么可以使用带缓冲的channel,防止生产者因为等待消费者消费过程而产生阻塞。反之对消费者来说也是受用的。
并发安全
互斥锁
go语言并没有对加锁机制的弃用,标准库里面仍然有sync.Mutex。
以下为简单加锁实现并发安全:
go
package main
import (
"fmt"
"sync"
"time"
)
var(
x int
mut sync.Mutex
)
func AddWithLock() {
mut.Lock()
for i:=0;i<2000;i++ {
x++
}
mut.Unlock()
}
func AddWithoutLock() {
for i:=0;i<2000;i++ {
x++
}
}
func main() {
//开五个协程的锁版本,再打印最终结果
for i := 0; i < 5; i++ {
go AddWithoutLock()
}
//等待上面的协程执行结束
time.Sleep(time.Second)
fmt.Println(x)
//有锁版本
x = 0
for i:=0;i<5;i++{
go AddWithLock()
}
time.Sleep(time.Second)
fmt.Println(x)
}
计数器
WaitGroup,通过Add(a)计时器+a,通过Done()计数器-1,通过Wait()阻塞直到计数器为0。这个东西我觉得有些类似于操作系统的信号量。
以下为实例:
go
package main
import (
"fmt"
"sync"
)
func hello(){
fmt.Println("hello")
}
func main() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
defer wg.Done()
hello()
}()
}
wg.Wait()
}
总结
go语言,跟java中springboot相似,甚至更简单。