目录

GoTeams-1项目基础搭建

【GoTeams】-1:项目基础搭建

https://i-blog.csdnimg.cn/direct/e6374f0612eb4287ae09798285a4e014.png

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 main

import (
	"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)
	<-quit

	log.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.SIGINTsyscall.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 会被触发。

运行代码,可以验证所写的功能。

https://i-blog.csdnimg.cn/direct/3d0db5acdd3c4b27971179ac87e18c7d.png

抽取封装

因为其他模块可能都有用到启停,所以需要把这个模块抽取到公共模块common中去。

创建一个新的 common 目录,然后编写一个 Run 文件,并且加入到work工作目录中, D:\GoProject\msproject> go work use ./project-commonRun.go 的代码如下。

package common

import (
	"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)
	<-quit

	log.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)。

https://i-blog.csdnimg.cn/direct/ed611f70b0564dc59b1465166323b17c.png

这样就可以正常使用了,按下ctrl+c也能够正常启停了。

https://i-blog.csdnimg.cn/direct/9841d17c1c894682ae16936647db4fb0.png

3. 路由

各个模块的路由不可能都写在mian函数中,所以需要分摊下去,由各个模块自己去管理对应的路由。

所以这就需要实现接口,然后实现对应的接口即可。

现在讲一下实现思路,首先需要构建项目文件结构如下。

https://i-blog.csdnimg.cn/direct/feb41b1803754c94a4c883e5944b75a5.png

router.go 的代码如下。

package router

import (
	"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 user

import "github.com/gin-gonic/gin"

type HandlerUser struct {
}

func (*HandlerUser) getCaptcha(ctx *gin.Context) {
	ctx.JSON(200, "getCaptcha success")
}

route.go 代码如下,主要是实现了对应的Route接口。

package user

import "github.com/gin-gonic/gin"

type RouterUser struct {
}

func (*RouterUser) Route(r *gin.Engine) {
	h := &HandlerUser{}
	r.POST("project/login/getCaptcha", h.getCaptcha)
}

运行后,发送请求,能够正常响应。

https://i-blog.csdnimg.cn/direct/d54019dc9cd5458e891de49187efc80b.png

这里画了一个流程图,来表示说明。

https://i-blog.csdnimg.cn/direct/371ec26b532c47ccb87b78ed1faa99bb.png

4.验证码接口

我们在 project-common 文件夹下创建 model.go 文件,然后定义返回的 Result 模型。

package common

type BusinessCode int

type Result struct {
	Code BusinessCode `json:"code"`
	Msg  string       `json:"msg"`
	Data any          `json:"data"`
}

func (r *Result) Success(data any) *Result {
	r.Code = 200
	r.Msg = "success"
	r.Data = data
	return r
}

func (r *Result) Fail(code BusinessCode, msg string) *Result {
	r.Code = code
	r.Msg = msg
	return r
}

然后在 user.go 中来创建对应的返回代码。

https://i-blog.csdnimg.cn/direct/83497de808ad411389c420fe957db2c5.png

验证一下接口请求,没问题。

https://i-blog.csdnimg.cn/direct/cdf903008dcb4847afcb2aebb02b6b03.png


定义手机号验证的逻辑,在common下编写 validata.go 代码。

package common

import "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 代码,如下所示。

https://i-blog.csdnimg.cn/direct/813b3a266e514c85b3367b1f55832d00.png

导入redis支持

project-user 路径下导入redis模块。

go get github.com/go-redis/redis/v8

上面的代码并没有对redis的支持,现在我们需要考虑引用redis了。

但是redis后续可能会缓存在mysql或者mongo或者别的存储器当中,需要不断变换代码,那么就不方便,这个时候就需要用到接口,面向接口编程,低耦合,高内聚。

这里我们进行了如下改动,首先定义了cache的相关接口。

https://i-blog.csdnimg.cn/direct/f78a799351b8473a99a0eae82e02cf41.png

然后在定义在redis.go中来定义实现的方法

https://i-blog.csdnimg.cn/direct/59142d0b3f944c798cf93c96c4e14b48.png

最后更新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))
}