本文目录
- 1. 回顾
- 2. Zap日志
- 3. 配置
- 4. 引入gprc
- 梳理gRPC思路
- 优雅关闭gRPC
1. 回顾
上篇文章我们进行了路由搭建,引入了redis,现在来看看对应的效果。
首先先把前端跑起来,然后点击注册获取验证码。
再看看控制台输出和redis是否已经有记录,验证没问题,现在redis这个环节是打通了。
2. Zap日志
go中原生的日志比较一般,我们可以集成一个流行的日志库进来。
这里用uber开源的zap日志库,在common路径下安装zap:go get -u go.uber.org/zap。
然后再安装一个日志分割库,go get -u github.com/natefinch/lumberjack
,因为日志的存储有几种方式,比如按照日志级别将日志记录到不同的文件,按照业务来分别记录不同级别的日志,按照包结构划分记录不同级别日志。debug级别以上记录一个,info以上记录一个,warn以上记录一个
。
在common
路径下创建logs.go
,然后编写对应的代码。
package logsimport ("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:"max_age"`MaxBackups int `json:"max_backups"`
}// InitLogger 初始化Logger
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) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可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)
}// GinLogger 接收gin框架默认的日志
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),)}
}// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {return func(c *gin.Context) {defer func() {if err := recover(); err != nil {// Check for a broken connection, as it is not really a// condition that warrants a panic stack trace.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)),)// If the connection is dead, we can't write a status to it.c.Error(err.(error)) // nolint: errcheckc.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地方进行更改了,比如下面的地方。
然后来验证一下是否能正常生成日志文件,正常生成了,没问题。
3. 配置
日志我们用了zap做集成,算是一个改进,但是配置比较复杂, 所以我们这里需要进一步优化这个配置。
配置我们引入viper
进行操作,也非常简单,直接上图和代码吧,在user里边装viper这个包。
go get github.com/spf13/viper
首先在user下面创建cofig目录,然后创建config.yaml
配置文件,然后创建config.go
代码读取配置。
config.go
的代码如下所示。
package configimport ("github.com/go-redis/redis/v8""github.com/spf13/viper""log""os""test.com/project-common/logs"
)var C = 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(workDir + "/config")conf.viper.AddConfigPath("etc/msproject/user")//读取configerr := conf.viper.ReadInConfig()if err != nil {log.Fatalln(err)}conf.ReadServerConfig()conf.InitZapLog() //初始化zap日志return conf
}func (c *Config) InitZapLog() {//从配置中读取日志配置,初始化日志lc := &logs.LogConfig{DebugFileName: c.viper.GetString("zap.debugFileName"),InfoFileName: c.viper.GetString("zap.infoFileName"),WarnFileName: c.viper.GetString("zap.warnFileName"),MaxSize: c.viper.GetInt("maxSize"),MaxAge: c.viper.GetInt("maxAge"),MaxBackups: c.viper.GetInt("maxBackups"),}err := logs.InitLogger(lc)if err != nil {log.Fatalln(err)}
}func (c *Config) ReadServerConfig() {sc := &ServerConfig{}sc.Name = c.viper.GetString("server.name")sc.Addr = c.viper.GetString("server.addr")c.SC = sc
}// 读redis的配置
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"), // no password setDB: c.viper.GetInt("db"), // use default DB}
}
对应的redis.go
中原本new一个redis客户端的代码也需要更改了,改为已有的读取配置的函数 ReadRedisConfig()
。
并且把原来main.go中关于zap的相关配置文件删除即可。
然后重新启动下,看看是否能够运行,ok,启动没问题。
4. 引入gprc
可以通过引入一个API把对应的服务连起来,可以把各种服务提出来,然后通过API进行定义。
在api\proto
下新建一个名为login.service.proto
的文件,然后编写代码。
syntax = "proto3";
package login.service.v1;
option go_package = "project-user/pkg/service/login.service.v1";message CaptchaMessage {string mobile = 1;
}
message CaptchaResponse{
}
service LoginService {rpc GetCaptcha(CaptchaMessage) returns (CaptchaResponse) {}
}
然后在proto路径下,运行命令:protoc --go_out=./gen --go_opt=paths=source_relative --go-grpc_out=./gen --go-grpc_opt=paths=source_relative login_service.proto
,就可以生成对应文件了。
因为是第一版,所以我们先在gen下生成,然后复制移动到service下面,防止后面不断根据功能进行修改,而导致新生成的被覆盖。
那么我们来看看这login-service_grpc.pb.go
文件到底生成了什么东西。
LoginServiceClient
是一个接口,定义了客户端可以调用的 GetCaptcha 方法。该方法接收一个 CaptchaMessage
请求,返回一个 CaptchaResponse
响应。
loginServiceClient
是 LoginServiceClient
的具体实现,通过 NewLoginServiceClient
函数创建。它使用 grpc.ClientConnInterface
来发起 RPC 调用。
在 loginServiceClient.GetCaptcha
方法中,通过 c.cc.Invoke
发起 GetCaptcha
方法的 RPC 调用。它将请求数据序列化并发送到服务器,然后等待响应。
LoginServiceServer
是服务器端的接口,定义了 GetCaptcha 方法。所有实现该接口的服务器端逻辑必须嵌入 UnimplementedLoginServiceServer
,以确保向前兼容性。
UnimplementedLoginServiceServer
提供了一个默认的未实现方法的错误响应
,返回 codes.Unimplemented
状态码。
也就屙是说,接口还包含一个方法 mustEmbedUnimplementedLoginServiceServer()
,这是一个空方法
,用于确保实现者嵌入了 UnimplementedLoginServiceServer
。
这是 UnimplementedLoginServiceServer
的 GetCaptcha
方法的默认实现。它返回 nil
作为响应,并通过 status.Errorf
返回一个带有 codes.Unimplemented
状态码的错误,表明该方法未被实现。这种设计确保了即使服务实现者没有实现某些方法,调用这些方法时也不会导致程序崩溃,而是返回一个明确的错误。
mustEmbedUnimplementedLoginServiceServer()
是一个空方法,用于确保服务实现者嵌入了 UnimplementedLoginServiceServer
。
testEmbeddedByValue()
是一个辅助方法,用于在运行时检查 UnimplementedLoginServiceServer
是否被正确嵌入(通过值而不是指针)。这避免了在方法调用时出现空指针引用。
type LoginServiceServer interface {GetCaptcha(context.Context, *CaptchaMessage) (*CaptchaResponse, error)mustEmbedUnimplementedLoginServiceServer()
}// UnimplementedLoginServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedLoginServiceServer struct{}func (UnimplementedLoginServiceServer) GetCaptcha(context.Context, *CaptchaMessage) (*CaptchaResponse, error) {return nil, status.Errorf(codes.Unimplemented, "method GetCaptcha not implemented")
}
func (UnimplementedLoginServiceServer) mustEmbedUnimplementedLoginServiceServer() {}
func (UnimplementedLoginServiceServer) testEmbeddedByValue() {}
所以主要是为了,确保向前兼容性:通过嵌入 UnimplementedLoginServiceServer
,服务实现者可以在未来版本中添加新方法,而不会破坏现有实现。
梳理gRPC思路
首先我们实现了gRPC,那么原本的api下面的user相关的我们可以删除了。
来看看我们实现了什么。
首先main.go
中的相关代码如下。
然后在service中我们实现了login_service.go
代码,如下。
package login_service_v1import ("context""errors""fmt""go.uber.org/zap""log"common "test.com/project-common""test.com/project-common/logs""test.com/project-user/pkg/dao""test.com/project-user/pkg/repo""time"
)type LoginService struct {UnimplementedLoginServiceServercache repo.Cache
}func New() *LoginService {return &LoginService{cache: dao.Rc,}
}func (ls LoginService) GetCaptcha(ctx context.Context, msg *CaptchaMessage) (*CaptchaResponse, error) {//1.获取参数moblie := msg.Mobilefmt.Println(moblie)//2.校验参数if !common.VerifyMoblie(moblie) {return nil, errors.New("手机号不合法")}//3.生成验证码(随机4位或者6位)code := "123456"//4.调用短信平台(三方,放入go协程中执行,接口可以快速响应,短信几秒到无所谓)go func() {time.Sleep(1 * time.Second)zap.L().Info("短信平台调用成功,发送短信 INFO")logs.LG.Debug("短信平台调用成功,发送短信 debug")zap.L().Error("短信平台调用成功,发送短信 error")// redis 假设后续缓存在mysql或者mongo当中,也有可能存储在别的当中// 所以考虑用接口实现,面向接口编程“低耦合,高内聚“// 5.存储验证码redis,设置过期时间15分钟即可c, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()err := ls.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)}()return &CaptchaResponse{}, nil
}
并且在router中更新了如下代码。
package routerimport ("github.com/gin-gonic/gin""google.golang.org/grpc""log""net""test.com/project-user/config"loginServiceV1 "test.com/project-user/pkg/service/login.service.v1"
)// 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)
}var routers []Routerfunc InitRouter(r *gin.Engine) {for _, ro := range routers {ro.Route(r)}
}type gRPCConfig struct {Addr stringRegisterFunc func(*grpc.Server)
}func RegisterGrpc() *grpc.Server {c := gRPCConfig{Addr: config.C.GC.Addr,RegisterFunc: func(g *grpc.Server) {//注册loginServiceV1.RegisterLoginServiceServer(g, loginServiceV1.New())}}s := grpc.NewServer()c.RegisterFunc(s)lis, err := net.Listen("tcp", c.Addr)if err != nil {log.Println("cannot listen")}//把服务放到协程里边go func() {err = s.Serve(lis)if err != nil {log.Println("server started error", err)return}}()return s
}
好,有点复杂,这里我们画图梳理下关系。
首先在router.go
文件中,我们声明了RegisterGrpc()
,这是gRPC服务的入口点,主要是配置grpc配置,包括服务地址还有注册函数,并且创建gRPC实例,然后注册登录服务,最后是启动gRPC服务器(在goroutine中运行的。)
在 login_service.go
中:LoginService
结构体:实现了 gRPC 服务接口,New() 函数:创建 LoginService
实例,GetCaptcha()
方法:实现具体的验证码获取业务逻辑。
所以调用关系如下。
所以为什么要使用协程?因为如果不使用协程,s.Serve(lis)
会阻塞主线程,导致后续代码无法继续运行,这样可以运行gRPC服务器与HTTP服务器(gin)
同时运行。
gRPC
可以独立运行,不影响主程序的其他功能。
优雅关闭gRPC
在main函数中,还有个stop,这是闭包函数,说实话这是第一次看到闭包函数的使用场景,首先我们捕获了外部变量gc,gc也就是gRPC服务器实例,然后定义了服务关闭的具体行为,也就是停止gRPC服务,作为参数传给srv.Run。
当Run函数接受一个stop函数作为参数,注释一种依赖注入的设计模式,当收到指令之后,会把gRPC给关闭了。
虽然 stop 函数被传入,但它并不会立即执行,代码会在 <-quit 这行被阻塞。只有当程序收到 SIGINT 或 SIGTERM 信号时(比如按 Ctrl+C),才会继续往下执行,然后才会检查 stop != nil 并执行 stop 函数。