前言
今天我们一起来完成我们的第一个Go语言web项目,上篇文章我们已经把service层的代码写到一半了,今天继续努力,不知道之前文章的小伙伴看这里
代码编写
编写service层
定义的QueryPageInfoFlow结构体回顾
type QueryPageInfoFlow struct {
topicIdStr string
topicId uint64
PageInfo *entity.PageInfo
topic *entity.Topic
postList []*entity.Post
}
整个service的处理逻辑就是把需要处理的数据存储在结构体里面,通过结构体在各个环节里面实现数据共享,而不是通过传参的形式来共享数据。
首先是第一个流程:参数校验
代码如下
// 参数校验
func (f *QueryPageInfoFlow) checkParam() error {
topicId, err := strconv.ParseUint(f.topicIdStr, 0, 64)
if err != nil {
return errors.New("parse topicIdStr to uint failed")
}
if topicId <= 0 {
return errors.New("topic id must larger than 0")
}
f.topicId = topicId
return nil
}
为了最大程度的把复杂逻辑集成在service层,这里直接接收controller层的字符串形式的topicId
参数,把转换的错误处理和其他校验都放在了checkParam
方法里面,这样就不用在controller里面转换参数的类型了。这样做是为了职责分明,controller主要负责和视图进行交互,像参数校验这样的逻辑,除非不可避免,不然一般不放在这里面。
接下来要进行的是:准备数据
代码如下
// 获取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 = dao.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 = dao.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
}
这里面包含了几个知识点,接下来简单介绍一下。首先就是goroutine
,这是Go语言的特色功能:协程。可以理解成虚拟线程,切换的开销比系统线程要小,所以能更轻松的支持高并发。启动一个协程非常简单,只需要一个go关键字。go func(形参){方法体}(实参)
,我们通常采用匿名函数式的方式来创建一个协程(也可以直接go 方法调用
),形参作为当前作用域和协程的桥梁,可以把当前作用域的变量通过最后一个括号(实参)
在调用的时候传入到协程里面去。
相信小伙伴们发现这个语法看起来有点奇怪,匿名函数见过,这后面怎么又多一个小括号呀。其实第二个小括号就是用来调用这个匿名函数的,这也就是第一个小括号里面我写的形参,第二个小括号里面我写的实参的原因。
在prepareInfo
这个方法中,启动了两个协程,一个负责使用topicId
去查询指定的主题信息,一个负责使用topicId
去查询指定主题的回复列表,这里使用了sync.WaitGroup
来实现协程同步。这里使用两个协程来实现这个功能的原因主要是,这两个功能是独立的,没有先后关系,互不依赖,互不影响,所以可以使用协程来分别执行两个功能。
最后一个部分是:数据组装
代码如下
// 组装数据
func (f *QueryPageInfoFlow) packageInfo() error {
f.PageInfo = &entity.PageInfo{
Topic: f.topic,
PostList: f.postList,
}
return nil
}
entity包下的PageInfo
package entity
// 页面信息结构体
type PageInfo struct {
Topic *Topic
PostList []*Post
}
页面上显示的数据应该是像PageInfo
这样的结构体的构造,所以我们需要把数据组装到PageInfo
内部,然后把组装好的PageInfo
对象作为处理的结果返回给controller层,所以接下来就是我们的controller层解析了。
编写controller层代码
在controller层我们需要定一个统一返回对象,这样对于前端调用来说,后端返回的结果才是统一的,可以理解这其实就是一种通信协议,通信的双方约定好数据的格式,然后才开始通信。
统一返回对象的定义
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
}
我给这个统一返回对象取名为ResultWrapper
,顾名思义就是后端返回给前端结果的包装器。Data
字段才是真正的后端业务返回的数据,而作为与视图交互的controller层自然可能直接把数据返回给前端调用,因为这样对于前端来说没有统一的格式,不好处理,所以,我定义了这个结果包装器。结构体里面三个字段各司其职,Code
字段负责返回状态码,用于标识当前业务的状态是成功还是失败,亦或者拓展更多的状态,比如把失败的情况细分成更多的子情况。Msg
字段负责返回业务的提示语,前端界面有时需要展示提示语,但是如果写死的前端代码里面的话,就缺少了灵活性,修改需求的时候需要同时修订后端和前端,反之如果把提示语由后端来传入,能再功能更改的时候改动更少的代码。Data
字段就不细讲,前面已经提到过。
这个any
类型作为interface{}
的别名,做到了兼容所有类型,因为这是一个空接口,能接纳所有的类型。如果还有小伙伴不知道为什么空接口能接纳所有类型,那还得去复习复习Go语言的接口的特性。Go语言的接口不需要显式地实现,比如java语言中的implements xxxInterface
这样的语句,Go语言的接口类型,只要是实现了所有接口方法的对象指针,都能被接口容纳,interface{}
没有方法需要实现,所以能接纳所有的类型。
我下面还封装了两个快捷方法,属于是封装提取重复代码,提高编码效率的小工具类,没有什么需要特别讲的。接下来就需要编写真正的controller方法了。
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/jun-chiang/go-web-demo1/service"
)
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))
}
代码看起来非常简洁,目前看到c.Param("topicId")
可能还不太理解,因为我把方法的定义与方法路径映射分开了,这种方法也是大型项目开发的常用方法。c.JSON
就是向前端调用返回JSON数据了,不是通过return对象来返回数据。
接下来说说如何映射方法路径
package main
import (
"github.com/gin-gonic/gin"
"github.com/jun-chiang/go-web-demo1/controller"
)
func initRouter(r *gin.Engine) {
apiRouter := r.Group("demo1")
apiRouter.GET("/queryPageInfo/:topicId", controller.QueryPageInfo)
}
我建立了一个单独的router.go文件来存放这个代码,因为这个代码和其他函数的功能是不一样的,这个函数主要负责把handler映射到指定的URL上面去,路径参数的话就采用:topicId
的形式,其他传参形式的话小伙伴们可以下来多多了解,把代码修修改改,实践一下,这个函数是在main.go里面调用的,接下来看看main.go里面的代码
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/jun-chiang/go-web-demo1/repository"
)
func main() {
err := repository.InitTopicIndexMap()
if err != nil {
fmt.Println("数据库初始化出错:", err.Error())
}
r := gin.Default()
initRouter(r)
r.Run(":8081")
}
这里面很简单,就是调用了模拟数据仓库的初始化,然后创建一个web服务,初始化路由,然后指定端口,项目就跑起来啦。
收工!
总结
这个项目虽然不难,但是对于像我这样的新手来说还是遇到了不少的坑,在查阅资料的过程中自己又收获了许多课外的知识,所以自己又对项目进行了一小部分的拓展,改成了自己觉得更顺眼的模样。写得不好的地方还请各位大佬多多指正,你们的批评是我前进路上重要的助力,最后附上整个项目的Github地址:github.com/jun-chiang/…
PS:代码在v0分支哟