教程地址: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的方式,后续继续记录