泉州有专门帮做网站的吗郑州网站推广技术
本文目录
- 1. 工作环境准备
- 2. 优雅启停
- 抽取封装
- 3. 路由
- 4.验证码接口
- 导入redis支持
1. 工作环境准备
在GoProject
路径下创建work工作区,依次输入以下命令完成工作目录的构建。
mkdir msproject
go work init
mkdir project-user
cd project-user
go mod init test.com/project-user
cd ..
go work use ./project-user
在路径下安装gin框架。
go get -u github.com/gin-gonic/gin
2. 优雅启停
在main.go
中创建优雅启停的代码,如下所示。
package mainimport ("context""github.com/gin-gonic/gin""log""net/http""os""os/signal""syscall""time"
)func main() {r := gin.Default()srv := &http.Server{Addr: ":80",Handler: r,}go func() {log.Printf("web server running is %s \n", srv.Addr)if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalln(err)}}()quit := make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quitlog.Println("shutting down project web server...")ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {log.Fatalln("web server shutdown,cause by:", err)}select {case <-ctx.Done():log.Println("关闭超时")}log.Println("web server stop success...")
}
下面来说明每个代码模块的作用:
go func() {log.Printf("web server running is %s \n", srv.Addr)if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalln(err)}}()
启动一个 HTTP Web 服务器,并在后台以独立的协程运行。调用 srv.ListenAndServe()
方法,启动服务器并开始监听客户端请求。如果在启动过程中发生错误,并且该错误不是因为服务器被正常关闭(即错误不是 http.ErrServerClosed
),则会通过 log.Fatalln
记录错误信息并终止程序运行。整个过程被封装在一个匿名函数中,并通过 go func()
的方式启动为一个独立的协程,这样可以避免阻塞主线程,从而让主线程可以继续执行其他任务,例如处理关闭信号等操作。
quit := make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quit
quit := make(chan os.Signal)
创建一个信号通道 quit,用于接收操作系统发送的信号。这个通道的类型是 os.Signal
,专门用于传递信号事件
。
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
使用 signal.Notify
函数将通道 quit
注册到信号监听中,指定监听的信号类型为 syscall.SIGINT
和 syscall.SIGTERM
。SIGINT 是用户通过键盘输入(如 Ctrl+C)发送的中断信号,通常用于请求程序终止。SIGTERM 是一个更通用的终止信号,通常由系统或进程管理工具发送,用于请求程序优雅地关闭。当程序接收到这些信号中的任何一个时,操作系统会将信号发送到 quit 通道。
这是一个阻塞操作
,表示从 quit 通道中接收信号。程序会在这里暂停执行,直到通道中接收到一个信号(即程序接收到 SIGINT 或 SIGTERM)
。一旦接收到信号,程序会继续执行后续的关闭逻辑。
也就是说,当程序接收到用户或系统发送的中断或终止信号时,它不会直接退出,而是触发后续的清理操作(如关闭服务器、释放资源等),从而确保程序能够安全地关闭。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
创建了一个带有超时限制的上下文 ctx,超时时间为 5 秒。context.WithTimeout
函数基于 context.Background()
创建了一个新的上下文,并为其设置了超时时间。如果在 5 秒内没有完成关闭操作,上下文将被取消。defer cancel()
确保在函数返回时调用 cancel() 函数
,从而释放与上下文相关的资源,避免资源泄漏
。
if err := srv.Shutdown(ctx); err != nil {log.Fatalln("web server shutdown, cause by:", err)
}
Shutdown 方法
会等待所有正在处理的请求完成,然后关闭服务器。这里的上下文 ctx
用于限制关闭操作的执行时间
。
select {case <-ctx.Done():log.Println("wait timeout...")}
回顾一下select语法,其 是 Go 语言中用于同时监听多个通道操作的语法结构。select
语句会阻塞,直到其中一个 case 中的通道操作可以执行(即通道准备好发送或接收数据)。
ctx.Done()
是 context.Context
提供的一个通道,当上下文被取消(cancel() 被调用)或超时(如果设置了超时时间)时,该通道会被关闭。
<-ctx.Done()
表示从 ctx.Done()
通道中接收数据。如果通道被关闭(即上下文被取消或超时),case 会被触发。
运行代码,可以验证所写的功能。
抽取封装
因为其他模块可能都有用到启停,所以需要把这个模块抽取到公共模块common中去。
创建一个新的common
目录,然后编写一个Run
文件,并且加入到work工作目录中,D:\GoProject\msproject> go work use ./project-common
,Run.go
的代码如下。
package commonimport ("context""github.com/gin-gonic/gin""log""net/http""os""os/signal""syscall""time"
)func Run(r *gin.Engine, svrName string, addr string) {srv := &http.Server{Addr: addr,Handler: r,}go func() {log.Printf("%s running is %s \n", svrName, srv.Addr)if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalln(err)}}()quit := make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quitlog.Printf("shutting down project %s ... \n", svrName)ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {log.Fatalf("%s shutdown,cause by: %v", svrName, err)}select {case <-ctx.Done():log.Println("wait timeout...")}log.Printf("%s stop success... \n", svrName)
}
这样我们就可以在main.go
中直接调用common的run了(这里把common包命名为了srv)。
这样就可以正常使用了,按下ctrl+c也能够正常启停了。
3. 路由
各个模块的路由不可能都写在mian函数中,所以需要分摊下去,由各个模块自己去管理对应的路由。
所以这就需要实现接口,然后实现对应的接口即可。
现在讲一下实现思路,首先需要构建项目文件结构如下。
router.go
的代码如下。
package routerimport ("github.com/gin-gonic/gin""test.com/project-user/api/user"
)// Router 接口
type Router interface {Route(r *gin.Engine)
}type RegisterRouter struct {
}func New() *RegisterRouter {return &RegisterRouter{}
}func (*RegisterRouter) Route(ro Router, r *gin.Engine) {ro.Route(r)
}func InitRouter(r *gin.Engine) {rg := New()rg.Route(&user.RouterUser{}, r)
}
user.go
代码如下,主要是执行handler,处理请求。
package userimport "github.com/gin-gonic/gin"type HandlerUser struct {
}func (*HandlerUser) getCaptcha(ctx *gin.Context) {ctx.JSON(200, "getCaptcha success")
}
route.go
代码如下,主要是实现了对应的Route接口。
package userimport "github.com/gin-gonic/gin"type RouterUser struct {
}func (*RouterUser) Route(r *gin.Engine) {h := &HandlerUser{}r.POST("project/login/getCaptcha", h.getCaptcha)
}
运行后,发送请求,能够正常响应。
这里画了一个流程图,来表示说明。
4.验证码接口
我们在 project-common
文件夹下创建model.go
文件,然后定义返回的Result
模型。
package commontype BusinessCode inttype Result struct {Code BusinessCode `json:"code"`Msg string `json:"msg"`Data any `json:"data"`
}func (r *Result) Success(data any) *Result {r.Code = 200r.Msg = "success"r.Data = datareturn r
}func (r *Result) Fail(code BusinessCode, msg string) *Result {r.Code = coder.Msg = msgreturn r
}
然后在user.go
中来创建对应的返回代码。
验证一下接口请求,没问题。
定义手机号验证的逻辑,在common下编写validata.go
代码。
package commonimport "regexp"// VerifyMobile 验证手机合法性
func VerifyMobile(mobile string) bool {if mobile == "" {return false}regular := "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$"reg := regexp.MustCompile(regular)return reg.MatchString(mobile)
}
使用 regexp.MustCompile
将正则表达式编译为一个正则对象 reg。最后,调用 reg.MatchString(mobile)
方法,检查传入的手机号码是否与正则表达式匹配。如果匹配,则返回 true,表示手机号码有效;否则返回 false,表示手机号码无效。
接着我们优化user.go
逻辑处理代码,来优化整个登录流程。
func (*HandlerUser) getCaptcha(ctx *gin.Context) {rsp := &common.Result{}//1.获取参数moblie := ctx.PostForm("mobile")//2.校验参数if !common.VerifyMoblie(moblie) {ctx.JSON(http.StatusOK, rsp.Fail(model.NoLegalMoblie, "手机号不合法"))return}//3.生成验证码(随机4位或者6位)code := "123456"//4.调用短信平台(三方,放入go协程中执行,接口可以快速响应,短信几秒到无所谓)go func() {time.Sleep(1 * time.Second)log.Println("短信平台调用成功,发送短信")//5.存储验证码redis,设置过期时间15分钟即可log.Printf("将手机号和验证码存入redis成功:REGISTER %s : %s", moblie, code)}()ctx.JSON(http.StatusOK, rsp.Success(code))
}
在上面我们用到了定义的常量NolegalMoblie
,需要创建一个新的文件夹和code.go
代码,如下所示。
导入redis支持
在project-user
路径下导入redis模块。
go get github.com/go-redis/redis/v8
上面的代码并没有对redis的支持,现在我们需要考虑引用redis了。
但是redis后续可能会缓存在mysql或者mongo或者别的存储器当中,需要不断变换代码,那么就不方便,这个时候就需要用到接口,面向接口编程,低耦合,高内聚。
这里我们进行了如下改动,首先定义了cache的相关接口。
然后在定义在redis.go中来定义实现的方法
最后更新user.go中的相关代码即可。
type HandlerUser struct {cache repo.Cache
}func New() *HandlerUser {return &HandlerUser{cache: dao.Rc,}
}func (h *HandlerUser) getCaptcha(ctx *gin.Context) {rsp := &common.Result{}//1.获取参数moblie := ctx.PostForm("mobile")fmt.Println(moblie)//2.校验参数if !common.VerifyMoblie(moblie) {ctx.JSON(http.StatusOK, rsp.Fail(model.NoLegalMoblie, "手机号不合法"))return}//3.生成验证码(随机4位或者6位)code := "123456"//4.调用短信平台(三方,放入go协程中执行,接口可以快速响应,短信几秒到无所谓)go func() {time.Sleep(1 * time.Second)log.Println("短信平台调用成功,发送短信")// redis 假设后续缓存在mysql或者mongo当中,也有可能存储在别的当中// 所以考虑用接口实现,面向接口编程“低耦合,高内聚“// 5.存储验证码redis,设置过期时间15分钟即可c, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()err := h.cache.Put(c, "REGISTER_"+moblie, code, 15*time.Minute)if err != nil {log.Printf("验证码存入redis出错,causer by :%v\n", err)}log.Printf("将手机号和验证码存入redis成功:REGISTER %s : %s", moblie, code)}()ctx.JSON(http.StatusOK, rsp.Success(code))
}