Bootstrap

Gin 学习笔记

教程地址: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 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:    ":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 router

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

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

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

type HandlerUser struct{}

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


04-发送验证码

在这里插入图片描述

  • 建立消息模型
package model

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
}
  • 手机验证
package lib

import "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 model

const (
	NoLegalMobile BusinessCode = 2001
)

  • 修改getCaptcha,返回123456为验证码
package user

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

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

import (
	"context"
	"time"
)

type Cache interface {
	Put(ctx context.Context, key string, value string, expire time.Duration) error
	Get(ctx context.Context, key string) (string, error)
}

  • user.go,加入redis保存
package user

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

import (
	"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.Logger

type 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.ISO8601TimeEncoder
	encoderConfig.TimeKey = "time"
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
	encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
	return 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.Path
		query := c.Request.URL.RawQuery
		c.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 bool
				if 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 main

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

	//log
	lc := &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)
	<-quit

	log.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 user

import (
	"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: 6379
  password: ""
  db: 0
  • config/config.go
package config

import (
	"Gin_gPRC/lib"
	"github.com/go-redis/redis/v8"
	"github.com/spf13/viper"
	"log"
)

var Conf = InitConfig()

type Config struct {
	viper *viper.Viper
	SC    *ServerConfig
}

type ServerConfig struct {
	Name string
	Addr 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()
	// 初始化zapLog
	conf.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的方式,后续继续记录

;