教程地址:https://www.bilibili.com/video/BV1FV4y1C72M?spm_id_from=333.788.videopod.sections&vd_source=707ec8983cc32e6e065d5496a7f79ee6
01-项目搭建
- 各常用目录的说明:
https://github.com/golang-standards/project-layout/blob/master/README_zh.md
02-优雅启停
- 用gin启动web服务器
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: ":5000",Handler: r,}// 通过协程启动服务go func() {log.Printf("server listen at %s", srv.Addr)if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalf("listen: %s\n", err)}}()// 制作按Ctrl+C退出功能,此处阻塞quit := make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quit// 系统停止开始log.Println("Shutdown Server ...")// 等待2秒ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()// 停止服务if err := srv.Shutdown(ctx); err != nil {log.Fatal("Server Shutdown:", err)}// 2秒后打印select {case <-ctx.Done():log.Println("timeout of 2 seconds.")}log.Println("Server exiting")
}
03-路由
- 路由通过InitRouter初始化;路由与api操作分离开来;api的路由设置与路由执行函数分开
- router/router.go :路由启动,并挂载user模块的路由
package routerimport ("Gin_gPRC/api/user""github.com/gin-gonic/gin"
)// Router 接口,规定里面有一个Route函数
type Router interface {Route(r *gin.Engine)
}// 定义一个RegisterRouter 注册类,这个类有一个Route方法
// Route方法接收一个符合Router接口规范的对象
// Route方法里运行了接收对象里的Route方法,用以绑定路由
type RegisterRouter struct{}
func New() *RegisterRouter {return &RegisterRouter{}
}
func (*RegisterRouter) Route(router Router, r *gin.Engine) {router.Route(r)
}// 初始化路由
func InitRouter(r *gin.Engine) {router := New()// 把user.RouterUser对象给到注册类,委托运行了user里的Route方法,传递r参数router.Route(&user.RouterUser{}, r)
}
- api/user/route.go;注册了/login/getCaptcha的POST接口,执行函数写再user.go中
package userimport "github.com/gin-gonic/gin"type RouterUser struct{}func (*RouterUser) Route(r *gin.Engine) {handler := &HandlerUser{}r.POST("/login/getCaptcha", handler.getCaptcha)
}
- api/user/user.go;执行POST
package userimport "github.com/gin-gonic/gin"type HandlerUser struct{}func (*HandlerUser) getCaptcha(ctx *gin.Context) {ctx.JSON(200, "getCaptcha test")
}
04-发送验证码
- 建立消息模型
package modeltype 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 = 200r.Msg = "success"r.Data = datareturn r
}func (r *Result) Fail(code BusinessCode, msg string) *Result {r.Code = coder.Msg = msgreturn r
}
- 手机验证
package libimport "regexp"func CheckMobile(mobile string) bool {if mobile == "" {return false}regular := "^1[3-9]\\d{9}$"reg := regexp.MustCompile(regular)return reg.MatchString(mobile)
}
- 运行状态代码
package modelconst (NoLegalMobile BusinessCode = 2001
)
- 修改getCaptcha,返回123456为验证码
package userimport ("Gin_gPRC/lib""Gin_gPRC/model""github.com/gin-gonic/gin""log""time"
)type HandlerUser struct{}func (*HandlerUser) getCaptcha(ctx *gin.Context) {//ctx.JSON(200, "getCaptcha test")rsp := &model.Result{}//1. 获取参数mobile := ctx.PostForm("mobile")//2. 校验参数if !lib.CheckMobile(mobile) {ctx.JSON(200, rsp.Fail(model.NoLegalMobile, "手机号码错误"))return}//3. 生成验证码code := "123456"//4. 调用短信平台接口go func() {time.Sleep(2 * time.Second)log.Println("短信平台调用成功")}()ctx.JSON(200, rsp.Success(code))
}
05-redis操作
- redis.go,安装:go get github.com/go-redis/redis/v8
package daoimport ("context""github.com/go-redis/redis/v8""time"
)var Rc *RedisCache
type RedisCache struct {rdb *redis.Client
}func init() {rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379",Password: "",DB: 0,})Rc = &RedisCache{rdb: rdb}
}func (rc *RedisCache) Put(ctx context.Context, key string, value string, expire time.Duration) error {err := rc.rdb.Set(ctx, key, value, expire).Err()return err
}func (rc *RedisCache) Get(ctx context.Context, key string) (string, error) {result, err := rc.rdb.Get(ctx, key).Result()return result, err
}
- repo/cache.go,定义Cache的接口,再由dao里的redis.go去实现
package repoimport ("context""time"
)type Cache interface {Put(ctx context.Context, key string, value string, expire time.Duration) errorGet(ctx context.Context, key string) (string, error)
}
- user.go,加入redis保存
package userimport ("Gin_gPRC/dao""Gin_gPRC/lib""Gin_gPRC/model""Gin_gPRC/repo""context""github.com/gin-gonic/gin""log""time"
)type HandlerUser struct {cache repo.Cache
}func New() *HandlerUser {return &HandlerUser{cache: dao.Rc,}
}func (h *HandlerUser) getCaptcha(ctx *gin.Context) {//ctx.JSON(200, "getCaptcha test")rsp := &model.Result{}//1. 获取参数mobile := ctx.PostForm("mobile")//2. 校验参数if !lib.CheckMobile(mobile) {ctx.JSON(200, rsp.Fail(model.NoLegalMobile, "手机号码错误"))return}//3. 生成验证码code := "123456"//4. 调用短信平台接口go func() {time.Sleep(2 * time.Second)log.Println("短信平台调用成功")// 制作一个超时的上下文c, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()// redis加入err := h.cache.Put(c, "REGISTER"+mobile, code, 15*time.Minute)if err != nil {log.Printf("验证码存入redis出错,%v \n", err)}}()ctx.JSON(200, rsp.Success(code))
}
06-日志
安装:go get -u go.uber.org/zap
安装:go get -u github.com/natefinch/lumberjack
- logs.go
package libimport ("github.com/gin-gonic/gin""github.com/natefinch/lumberjack""go.uber.org/zap""go.uber.org/zap/zapcore""net""net/http""net/http/httputil""os""runtime/debug""strings""time"
)var lg *zap.Loggertype LogConfig struct {DebugFileName string `json:"debugFileName"`InfoFileName string `json:"infoFileName"`WarnFileName string `json:"warnFileName"`MaxSize int `json:"maxSize"`MaxAge int `json:"maxAge"`MaxBackups int `json:"maxBackups"`
}func InitLogger(cfg *LogConfig) (err error) {writeSyncerDebug := getLogWriter(cfg.DebugFileName, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)writeSyncerInfo := getLogWriter(cfg.InfoFileName, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)writeSyncerWarn := getLogWriter(cfg.WarnFileName, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)encoder := getEncoder()debugCore := zapcore.NewCore(encoder, writeSyncerDebug, zapcore.DebugLevel)infoCore := zapcore.NewCore(encoder, writeSyncerInfo, zapcore.InfoLevel)warnCore := zapcore.NewCore(encoder, writeSyncerWarn, zapcore.WarnLevel)consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())std := zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel)core := zapcore.NewTee(debugCore, infoCore, warnCore, std)lg = zap.New(core, zap.AddCaller())zap.ReplaceGlobals(lg)return}func getEncoder() zapcore.Encoder {encoderConfig := zap.NewProductionEncoderConfig()encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoderencoderConfig.TimeKey = "time"encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoderencoderConfig.EncodeDuration = zapcore.SecondsDurationEncoderencoderConfig.EncodeCaller = zapcore.ShortCallerEncoderreturn zapcore.NewJSONEncoder(encoderConfig)
}func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {lumberJackLogger := &lumberjack.Logger{Filename: filename,MaxSize: maxSize,MaxBackups: maxBackup,MaxAge: maxAge,}return zapcore.AddSync(lumberJackLogger)
}func GinLogger() gin.HandlerFunc {return func(c *gin.Context) {start := time.Now()path := c.Request.URL.Pathquery := c.Request.URL.RawQueryc.Next()cost := time.Since(start)lg.Info(path,zap.Int("status", c.Writer.Status()),zap.String("method", c.Request.Method),zap.String("path", path),zap.String("query", query),zap.String("ip", c.ClientIP()),zap.String("user-agent", c.Request.UserAgent()),zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),zap.Duration("cost", cost),)}
}func GinRecovery(stack bool) gin.HandlerFunc {return func(c *gin.Context) {defer func() {if err := recover(); err != nil {var brokenPipe boolif ne, ok := err.(*net.OpError); ok {if se, ok := ne.Err.(*os.SyscallError); ok {if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {brokenPipe = true}}}httpRequest, _ := httputil.DumpRequest(c.Request, false)if brokenPipe {lg.Error(c.Request.URL.Path,zap.Any("error", err),zap.String("request", string(httpRequest)))c.Error(err.(error))c.Abort()return}if stack {lg.Error("[Recovery from panic]",zap.Any("error", err),zap.String("request", string(httpRequest)),zap.String("stack", string(debug.Stack())))} else {lg.Error("[Recovery from panic]",zap.Any("error", err),zap.String("request", string(httpRequest)))}c.AbortWithStatus(http.StatusInternalServerError)}}()c.Next()}
}
- main.go里加入log
package mainimport ("Gin_gPRC/lib""Gin_gPRC/router""context""github.com/gin-gonic/gin""log""net/http""os""os/signal""syscall""time"
)func main() {r := gin.Default()//loglc := &lib.LogConfig{DebugFileName: "./logs/debug.log",InfoFileName: "./logs/info.log",WarnFileName: "./logs/warn.log",MaxSize: 500,MaxAge: 28,MaxBackups: 3,}err := lib.InitLogger(lc)if err != nil {log.Fatal(err)}router.InitRouter(r)srv := &http.Server{Addr: ":5000",Handler: r,}go func() {log.Printf("server listen at %s", srv.Addr)if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalf("listen: %s\n", err)}}()quit := make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quitlog.Println("Shutdown Server ...")ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {log.Fatal("Server Shutdown:", err)}select {case <-ctx.Done():log.Println("timeout of 2 seconds.")}log.Println("Server exiting")
}
- 在user.go里应用
package userimport ("Gin_gPRC/dao""Gin_gPRC/lib""Gin_gPRC/model""Gin_gPRC/repo""context""github.com/gin-gonic/gin""go.uber.org/zap""time"
)type HandlerUser struct {cache repo.Cache
}func New() *HandlerUser {return &HandlerUser{cache: dao.Rc,}
}func (h *HandlerUser) getCaptcha(ctx *gin.Context) {//ctx.JSON(200, "getCaptcha test")rsp := &model.Result{}//1. 获取参数mobile := ctx.PostForm("mobile")//2. 校验参数if !lib.CheckMobile(mobile) {ctx.JSON(200, rsp.Fail(model.NoLegalMobile, "手机号码错误"))return}//3. 生成验证码code := "123456"//4. 调用短信平台接口go func() {time.Sleep(2 * time.Second)zap.L().Info("短信平台调用成功")// 制作一个超时的上下文c, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()// redis加入err := h.cache.Put(c, "REGISTER"+mobile, code, 15*time.Minute)if err != nil {zap.L().Error("验证码存入redis出错" + err.Error())}}()ctx.JSON(200, rsp.Success(code))
}
07-配置
安装:go get github.com/spf13/viper
- config.yaml
server:name: "Gin_gRPC"addr: "127.0.0.1:5000"
zap:debugFileName: "./logs/debug.log"infoFileName: "./logs/info.log"warnFileName: "./logs/warn.log"maxSize: 500,maxAge: 28,maxBackups: 3
redis:host: "localhost"port: 6379password: ""db: 0
- config/config.go
package configimport ("Gin_gPRC/lib""github.com/go-redis/redis/v8""github.com/spf13/viper""log"
)var Conf = InitConfig()type Config struct {viper *viper.ViperSC *ServerConfig
}type ServerConfig struct {Name stringAddr string
}func InitConfig() *Config {conf := &Config{viper: viper.New()}//workDir, _ := os.Getwd()// 确定配置文件的名称、类型与位置conf.viper.SetConfigName("config")conf.viper.SetConfigType("yaml")conf.viper.AddConfigPath("./")err := conf.viper.ReadInConfig()if err != nil {log.Fatalf("Fatal error config file: %s \n", err)}// 读取Server的配置conf.ReadServerConfig()// 初始化zapLogconf.InitZapLog()return conf
}func (c *Config) InitZapLog() {lc := &lib.LogConfig{DebugFileName: c.viper.GetString("zap.debugFileName"),InfoFileName: c.viper.GetString("zap.infoFileName"),WarnFileName: c.viper.GetString("zap.warnFileName"),MaxSize: c.viper.GetInt("zap.maxSize"),MaxAge: c.viper.GetInt("zap.maxAge"),MaxBackups: c.viper.GetInt("zap.maxBackups"),}err := lib.InitLogger(lc)if err != nil {log.Fatal(err)}
}func (c *Config) ReadServerConfig() {sc := &ServerConfig{}sc.Name = c.viper.GetString("server.name")sc.Addr = c.viper.GetString("server.addr")c.SC = sc
}func (c *Config) ReadRedisConfig() *redis.Options {return &redis.Options{Addr: c.viper.GetString("redis.host") + ":" + c.viper.GetString("redis.port"),Password: c.viper.GetString("redis.password"),DB: c.viper.GetInt("redis.db"),}
}
- 在main.go中应用
func main() {r := gin.Default()config.InitConfig()router.InitRouter(r)srv := &http.Server{Addr: config.Conf.SC.Addr,Handler: r,}...
}
- 在redis.go中应用
func init() {rdb := redis.NewClient(config.Conf.ReadRedisConfig())Rc = &RedisCache{rdb: rdb}
}
结束:
对于微服务,考虑尝试下使用Go-micro + Gin的方式,后续继续记录