GO语言工程实践课后作业:实现思路、代码以及路径记录 | 青训营

75 阅读8分钟

需求描述

  • 发布话题和回帖

  • 本地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

目录结构

image.png

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级别。

协程对比线程的优势:

  1. 存在于用户态,可操作性强,调度可由自己控制。
  2. 更轻量,所需资源更少。

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)
}

并发的通信

并发程序之间的通信,一般都是通过共享内存的形式实现通信,临界区一般需要加锁保护。

communication

而go语言采取的是通过通信来实现共享内存,这个过程是反过来的,但用起来更为直观。

Channel

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相似,甚至更简单。

参考