当前位置: 首页 > article >正文

【GoTeams】-2:项目基础搭建(下)

在这里插入图片描述

本文目录

  • 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 logs

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:"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.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)
}

// GinLogger 接收gin框架默认的日志
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),
		)
	}
}

// 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 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)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					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地方进行更改了,比如下面的地方。
在这里插入图片描述
然后来验证一下是否能正常生成日志文件,正常生成了,没问题。
正常生成了

3. 配置

日志我们用了zap做集成,算是一个改进,但是配置比较复杂, 所以我们这里需要进一步优化这个配置。

配置我们引入viper进行操作,也非常简单,直接上图和代码吧,在user里边装viper这个包。

go get github.com/spf13/viper

首先在user下面创建cofig目录,然后创建config.yaml配置文件,然后创建config.go代码读取配置。

在这里插入图片描述

config.go的代码如下所示。

package config

import (
	"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.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(workDir + "/config")
	conf.viper.AddConfigPath("etc/msproject/user")

	//读取config
	err := 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 set
		DB:       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 响应。

loginServiceClientLoginServiceClient 的具体实现,通过 NewLoginServiceClient 函数创建。它使用 grpc.ClientConnInterface 来发起 RPC 调用。

loginServiceClient.GetCaptcha 方法中,通过 c.cc.Invoke 发起 GetCaptcha 方法的 RPC 调用。它将请求数据序列化并发送到服务器,然后等待响应。

在这里插入图片描述

LoginServiceServer 是服务器端的接口,定义了 GetCaptcha 方法。所有实现该接口的服务器端逻辑必须嵌入 UnimplementedLoginServiceServer,以确保向前兼容性。
UnimplementedLoginServiceServer 提供了一个默认的未实现方法的错误响应,返回 codes.Unimplemented 状态码。

在这里插入图片描述

也就屙是说,接口还包含一个方法 mustEmbedUnimplementedLoginServiceServer(),这是一个空方法,用于确保实现者嵌入了 UnimplementedLoginServiceServer

这是 UnimplementedLoginServiceServerGetCaptcha 方法的默认实现。它返回 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_v1

import (
	"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 {
	UnimplementedLoginServiceServer
	cache repo.Cache
}

func New() *LoginService {
	return &LoginService{
		cache: dao.Rc,
	}
}

func (ls LoginService) GetCaptcha(ctx context.Context, msg *CaptchaMessage) (*CaptchaResponse, error) {

	//1.获取参数
	moblie := msg.Mobile
	fmt.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 router

import (
	"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 []Router

func InitRouter(r *gin.Engine) {
	for _, ro := range routers {
		ro.Route(r)
	}
}

type gRPCConfig struct {
	Addr         string
	RegisterFunc 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 函数。


http://www.kler.cn/a/574051.html

相关文章:

  • BGP服务器主要是指什么?
  • 硬件学习【1】74HC165D-并行信号输入-串行输出
  • VSCode 配置优化指南:打造极致高效的前端开发环境
  • 系统架构设计师—软件工程基础篇—软件开发方法
  • 【无标题】养老护理初级考题抽取——2大题抽1+7小题抽2-共有432种可能。
  • 【LeetCode 热题 100】438. 找到字符串中所有字母异位词 | python 【中等】
  • go语言因为前端跨域导致无法访问到后端解决方案
  • vim基本操作及常用命令
  • WPS条件格式:B列的值大于800,并且E列的值大于B列乘以0.4时,这一行的背景标红
  • 蓝桥与力扣刷题(蓝桥 数字三角形)
  • AT89S51 单片机手册解读:架构、功能与应用深度剖析
  • R语言——数据类型
  • 单例模式的五种实现方式
  • MATLAB中lookAheadBoundary函数用法
  • 【前端基础】Day 6 CSS定位
  • 护照阅读器在汽车客运站流程中的应用
  • 洛谷P1102 A-B 数对
  • OceanBase-obcp-v3考试资料梳理
  • qt作业day5
  • Spring Cloud Alibaba学习 5- Seata入门使用