Go语言入门工程实践Web项目 | 青训营

86 阅读4分钟

前言

项目的的源代码来源于博主jun_chiang.通过对博主代码的学习,我对课程内容有了更全面的了解.感谢博主jun_chiang!

1编写结构体

根据课上给的例子编写结构体

image.png

代码文件放在文件夹entity中,代码内容如下

package entity
type Topic struct {  
Id uint64  
Title string  
Content string  
CreateTime time.Now().Unix() 
}
package entity
type Post struct {  
Id uint64  
ParentId uint64  
Content string  
CreateTime time.Now().Unix()  
}

因为id是数字,并且大于1,所以id类型为无符号整数,title和content都是字符串,create_time是时间戳. 在之后的代码中还会用到结构体pageinfo,用于存储返回查询结果.

package entity  
  
// 页面信息结构体  
type PageInfo struct {  
Topic *Topic  
PostList []*Post  
}

2模拟编写数据库

我新建了文件夹repository,文件夹里有两个文件,一个文件存储json数据,另一个文件是.go文件,用来读取json数据,模拟数据库.

package repository  
  
import (  
"encoding/json"  
"io"  
"os"  
"test/entity"  
)  
  
// 为类型定义别名 如果没有 ‘=’ 就是定义新的类型  
type Topic = entity.Topic  
type Post = entity.Post  
  
// 存储数据的变量  
var (  
// 定义话题索引  
TopicIndexMap map[uint]*Topic = make(map[uint]*Topic, 5)  
// 定义并初始化话题回复索引  
PostIndexMap map[uint][]*Post = make(map[uint][]*entity.Post, 5)  
)  
  
// 从文件初始话题索引  
func InitTopicIndexMap() error {  
// 读取json文件  
jsonFile, err := os.Open("repository/initial_data.json")  
if err != nil {  
return err  
}  
// 延迟关闭文件流  
defer jsonFile.Close()  
// 读取全部文件  
var topics []Topic  
byteValue, err := io.ReadAll(jsonFile)  
if err != nil {  
return err  
}  
// json反序列化  
err = json.Unmarshal(byteValue, &topics)  
if err != nil {  
return err  
}  
for i := range topics {  
topic := topics[i]  
// 以ID为key,把对象指针放到Map里面去  
TopicIndexMap[uint(topic.Id)] = &topic  
}  
return nil  
}

根据课程4.6的内容,我使用两个map来存储数据,把结构体的id作为key,结构体指针作为value,这样根据id查找我们需要的结构体对象的时间复杂度为O(1).

函数InitTopicIndexMap读取json文件中的内容并进行反序列化,然后将json文件中的所有内容存入map中.

3实现dao层

在课程中,没有定义dao层,通过截图来看,应该是将dao层内容写入了repository中. 我在这里模仿博主jun_chiang的写法.我先创建dao文件夹,文件夹中有文件topic_dao.go,topic_dao_impl.go. 这是topic_dao.go文件的源码

package dao  
  
import "test/entity"  
  
// TopicDao定义了查询Topic的方式  
type TopicDao interface {  
// 根据Topic的ID查询Topi对象  
QueryTopicById(id uint) (*entity.Topic, error)  
}

这是topic_dao_impl.go文件的源码

package memory  
  
import (  
"sync"  
"test/entity"  
"test/repository"  
)  
  
// 定义结构体  
type TopicDaoImpl struct {  
}  
  
var (  
// 声明结构体变量  
topicDaoImpl *TopicDaoImpl  
initialOnce sync.Once  
)  
  
// 获取TopicDaoImpl的全局唯一实例  
func NewTopicDaoImplInstance() *TopicDaoImpl {  
// 单例模式  
initialOnce.Do(func() {  
// 为结构体变量赋值  
topicDaoImpl = &TopicDaoImpl{}  
})  
// 返回已经赋值的topicDaoImpl结构体对象指针  
return topicDaoImpl  
}  
  
// 实现Topic_dao的方法  
func (*TopicDaoImpl) QueryTopicById(id uint) (*entity.Topic, error) {  
return repository.TopicIndexMap[id], nil  
}

这里采用了单例模式,我不太清楚这样做的优势是什么,博主也没有讲.

4实现service层

service层要实现查询topic的功能,所以就只有一个文件query_page_info.go,因为这个demo只有一个功能.

这一层的功能主要就是控制业务的流程,分为三个部分

1 参数校验
2 准备数据
3 组装实体

这三个部分对应三个函数,分别是checkParam(),prepareInfo(),packPageInfo(); 这是query_page_info的源码

package service  
  
import (  
"errors"  
"strconv"  
"sync"  
"test/dao"  
"test/dao/memory"  
"test/entity"  
)  
  
type QueryPageInfoFlow struct {  
topicIdStr string  
topicId uint  
PageInfo *entity.PageInfo  
topic *entity.Topic  
postList []*entity.Post  
}  
  
// 通过TopicId查询页面信息的方法  
func QueryPageInfo(topicIdStr string) (*entity.PageInfo, error) {  
return NewQueryPageInfoFlow(topicIdStr).Do()  
}  
  
// 工厂函数,返回查询流程结构体的指针,用于启动流程  
func NewQueryPageInfoFlow(topicIdStr string) *QueryPageInfoFlow {  
return &QueryPageInfoFlow{  
topicIdStr: topicIdStr,  
}  
}  
  
// 查询页面信息的整个流程  
func (f *QueryPageInfoFlow) Do() (*entity.PageInfo, error) {  
// 参数校验  
if err := f.checkParam(); err != nil {  
return nil, err  
}  
// 准备数据  
if err := f.prepareInfo(); err != nil {  
return nil, err  
}  
// 组装实体  
if err := f.packageInfo(); err != nil {  
return nil, err  
}  
return f.PageInfo, nil  
}  
  
// 参数校验  
func (f *QueryPageInfoFlow) checkParam() error {  
topicId, err := strconv.ParseUint(f.topicIdStr, 0, 64)  
if err != nil {  
return errors.New("parse topicIdStr to uint64 failed")  
}  
if topicId <= 0 {  
return errors.New("topic id must larger than 0")  
}  
f.topicId = uint(topicId)  
return nil  
}  
  
// 获取Topic的信息以及对应的Post列表的信息  
func (f *QueryPageInfoFlow) prepareInfo() error {  
var wg sync.WaitGroup  
wg.Add(2)  
var topicErr, postErr error  
// 获取Topic的信息  
go func() {  
defer wg.Done()  
var topicDao dao.TopicDao = memory.NewTopicDaoImplInstance()  
topic, err := topicDao.QueryTopicById(f.topicId)  
if err != nil {  
topicErr = err  
return  
}  
f.topic = topic  
}()  
// 获取Post列表的信息  
go func() {  
defer wg.Done()  
// 利用接口实现多态  
var postDao dao.PostDao = memory.NewPostDaoImplInstance()  
postList, err := postDao.QueryPostListByTopicId(f.topicId)  
if err != nil {  
postErr = err  
return  
}  
f.postList = postList  
}()  
wg.Wait()  
  
if topicErr != nil {  
return topicErr  
}  
if postErr != nil {  
return postErr  
}  
return nil  
}  
  
// 包装数据  
func (f *QueryPageInfoFlow) packageInfo() error {  
f.PageInfo = &entity.PageInfo{  
Topic: f.topic,  
PostList: f.postList,  
}  
return nil  
}

checkParam()实现校验参数功能,实现的逻辑是判断topicid是否大于0,如果不大于0将报错.

prepareInfo()实现准备数据功能,实现的逻辑是通过dao层的函数QueryTopicById()获取id=topicid的topic,通过函数QueryPostListByTopicId()获取post.

packageInfo()实现组装实体功能,实现的逻辑是将查询到的topic和postList存入pageinfo中.

5实现controller

controller层包含两个文件common.go和query_page_info.go,前一个文件用来定义一个统一返回对象.另一个文件用来实现返回查询结果. 这是common.go的源码

package controller  
  
// 任意类型的数据  
type any = interface{}  
  
// 返回给视图的结果的包装器  
type ResultWrapper struct {  
  
/*  
2000 代表成功  
4000 代表失败  
*/  
Code int32  
Msg string  
Data any  
}  
  
// 返回错误响应对象的快捷方法  
func ServerFailed(msg string) *ResultWrapper {  
wrapper := ResultWrapper{  
Code: 4000,  
Msg: msg,  
Data: nil,  
}  
return &wrapper  
}  
  
// 返回正确响应对象的快捷方法  
// data是要返回给前端的数据  
func ServerSuccess(data any) *ResultWrapper {  
wrapper := ResultWrapper{  
Code: 2000,  
Msg: "success",  
Data: data,  
}  
return &wrapper  
}

返回数据包含三个部分,Code字段负责返回状态码,用于标识当前业务的状态是成功还是失败,成功返回2000,失败则返回4000.Msg字段负责返回业务的提示语,如果成功返回success,如果失败则返回err.Error().err是service.QueryPageInfo(topicIdStr)返回的错误.Data字段是service层查询到的数据。 这是query_page_info.go的源码

package controller  
  
import (  
"net/http"  
"test/service"  
  
"github.com/gin-gonic/gin"  
)  
  
func QueryPageInfo(c *gin.Context) {  
// 获取URL链接中的ID  
topicIdStr := c.Param("topicId")  
data, err := service.QueryPageInfo(topicIdStr)  
if err != nil {  
c.JSON(http.StatusOK, ServerFailed(err.Error()))  
return  
}  
c.JSON(http.StatusOK, ServerSuccess(data))  
}

代码的实现逻辑是将 topicIdStr作为参数传给函数service.QueryPageInfo(topicIdStr) ,然后函数将查询到的数据赋值给data.然后发送json数据.

router

在课程中router有四大功能分别是:初始化数据索引,初始化引擎配置,构建路由,启动服务.在这里分成了两部分,router.go文件只实现启动服务功能,其他三大功能由main.go文件实现.

router的功能是启动服务.

源码如下

package main  
  
import (  
"github.com/gin-gonic/gin"  
"test/controller"  
)  
  
func initRouter(r *gin.Engine) {  
apiRouter := r.Group("demo1")  
  
apiRouter.GET("/queryPageInfo/:topicId", controller.QueryPageInfo)  
}

main

main文件实现的功能是初始化数据索引,初始化引擎配置,构建路由这三个功能.

package main  
  
import (  
"fmt"  
"test/repository"  
"github.com/gin-gonic/gin"  
)  
  
func main() {  
err := repository.InitTopicIndexMap()  
if err != nil {  
fmt.Println("数据库初始化出错:", err.Error())  
}  
  
r := gin.Default()  
  
initRouter(r)  
  
r.Run(":8081")  
}

运行结果

项目运行后,可以看到8081端口已经开启了监听

image.png

输入命令后可以看到请求topic成功.

返回信息如下.

PS E:\Users\Dell\GolandProjects\test> curl http://localhost:8081/demo1/queryPageInfo/1

StatusCode : 200
StatusDescription : OK
Content : {"Code":2000,"Msg":"success","Data":{"Topic":{"Id":1,"Title":"青训营项目实战","Content":"一起来挑战自己","CreateTime":"2023-07-27 15:06:07"},"PostList":null}} RawContent : HTTP/1.1 200 OK
Content-Length: 172 Content-Type: application/json; charset=utf-8 Date: Tue, 01 Aug 2023 04:36:06 GMT {"Code":2000,"Msg":"success","Data":{"Topic":{"Id":1,"Title":"青训营项目实战","Cont...

Forms : {} Headers : {[Content-Length, 172], [Content-Type, application/json; charset=utf-8], [Date, Tue, 01 Aug 2023 04:36:06 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 172