上手Go青训营Web项目快看过来(下) | 青训营

前言

今天我们一起来完成我们的第一个Go语言web项目,上篇文章我们已经把service 层的代码写到一半了,今天继续努力,不知道之前文章的小伙伴看这里

代码编写

编写service层

定义的QueryPageInfoFlow结构体回顾

Go 复制代码
type QueryPageInfoFlow struct {
	topicIdStr string
	topicId    uint64
	PageInfo   *entity.PageInfo
	topic      *entity.Topic
	postList   []*entity.Post
}

整个service的处理逻辑就是把需要处理的数据存储在结构体里面,通过结构体在各个环节里面实现数据共享,而不是通过传参的形式来共享数据。

首先是第一个流程:参数校验

代码如下

Go 复制代码
// 参数校验
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 主要负责和视图进行交互,像参数校验这样的逻辑,除非不可避免,不然一般不放在这里面。

接下来要进行的是:准备数据

代码如下

Go 复制代码
// 获取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来实现协程同步。这里使用两个协程来实现这个功能的原因主要是,这两个功能是独立的,没有先后关系,互不依赖,互不影响,所以可以使用协程来分别执行两个功能。

最后一个部分是:数据组装 代码如下

Go 复制代码
// 组装数据
func (f *QueryPageInfoFlow) packageInfo() error {
	f.PageInfo = &entity.PageInfo{
		Topic:    f.topic,
		PostList: f.postList,
	}
	return nil
}

entity包下的PageInfo

Go 复制代码
package entity

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

页面上显示的数据应该是像PageInfo这样的结构体的构造,所以我们需要把数据组装到PageInfo内部,然后把组装好的PageInfo对象作为处理的结果返回给controller 层,所以接下来就是我们的controller层解析了。

编写controller层代码

controller 层我们需要定一个统一返回对象,这样对于前端调用来说,后端返回的结果才是统一的,可以理解这其实就是一种通信协议 ,通信的双方约定好数据的格式,然后才开始通信。
统一返回对象的定义

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
}

我给这个统一返回对象取名为ResultWrapper,顾名思义就是后端返回给前端结果的包装器。Data字段才是真正的后端业务返回的数据,而作为与视图 交互的controller 层自然可能直接把数据返回给前端调用,因为这样对于前端来说没有统一的格式,不好处理,所以,我定义了这个结果包装器。结构体里面三个字段各司其职,Code字段负责返回状态码,用于标识当前业务的状态是成功还是失败,亦或者拓展更多的状态,比如把失败的情况细分成更多的子情况。Msg字段负责返回业务的提示语,前端界面有时需要展示提示语,但是如果写死的前端代码里面的话,就缺少了灵活性,修改需求的时候需要同时修订后端和前端,反之如果把提示语由后端来传入,能再功能更改的时候改动更少的代码。Data字段就不细讲,前面已经提到过。

这个any类型作为interface{}的别名,做到了兼容所有类型,因为这是一个空接口,能接纳所有的类型。如果还有小伙伴不知道为什么空接口能接纳所有类型,那还得去复习复习Go语言的接口的特性。Go语言的接口不需要显式地实现,比如java语言中的implements xxxInterface这样的语句,Go语言的接口类型,只要是实现了所有接口方法的对象指针,都能被接口容纳,interface{}没有方法需要实现,所以能接纳所有的类型。

我下面还封装了两个快捷方法,属于是封装提取重复代码,提高编码效率的小工具类,没有什么需要特别讲的。接下来就需要编写真正的controller方法了。

Go 复制代码
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对象来返回数据。

接下来说说如何映射方法路径

Go 复制代码
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里面的代码

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分支哟

相关推荐
CallBack8 个月前
Typora+PicGo+阿里云OSS搭建个人图床,纵享丝滑!
前端·青训营笔记
Taonce1 年前
站在Android开发者的角度认识MQTT - 源码篇
android·青训营笔记
AB_IN1 年前
打开抖音会发生什么 | 青训营
青训营笔记
monster1231 年前
结营感受(go) | 青训营
青训营笔记
翼同学1 年前
实践记录:使用Bcrypt进行密码安全性保护和验证 | 青训营
青训营笔记
hu1hu_1 年前
Git 的正确使用姿势与最佳实践(1) | 青训营
青训营笔记
星曈1 年前
详解前端框架中的设计模式 | 青训营
青训营笔记
tuxiaobei1 年前
文件上传漏洞 Upload-lab 实践(中)| 青训营
青训营笔记
yibao1 年前
高质量编程与性能调优实战 | 青训营
青训营笔记
小金先生SG1 年前
阿里云对象存储OSS使用| 青训营
青训营笔记