GoTeams-1项目基础搭建
【GoTeams】-1:项目基础搭建
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.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 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)。
这样就可以正常使用了,按下ctrl+c也能够正常启停了。
3. 路由
各个模块的路由不可能都写在mian函数中,所以需要分摊下去,由各个模块自己去管理对应的路由。
所以这就需要实现接口,然后实现对应的接口即可。
现在讲一下实现思路,首先需要构建项目文件结构如下。
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)
}
运行后,发送请求,能够正常响应。
这里画了一个流程图,来表示说明。
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
中来创建对应的返回代码。
验证一下接口请求,没问题。
定义手机号验证的逻辑,在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
代码,如下所示。
导入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))
}