目录
前言
一、高性能日志库zap
1 - zap Quick Start
-
zap日志库官网地址:https://github.com/uber-go/zap
-
zap的优点:性能高
-
Zap提供了两种类型的日志记录器—Sugared Logger和Logger
- 在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录
- 在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它甚至比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录
-
为什么logger的效率更高:因为logger指明了类型,zap就不会启用go的反射,这样效率就比Sugared Logger更高;但即使是Sugared Logger也比一般的日志库性能高很多了
-
SugaredLogger使用
package main
import (
"time"
"go.uber.org/zap"
)
func main() {
url := "https//www.baidu.com"
logger, _ := zap.NewProduction() // 生产环境下使用
// logger, _ := zap.NewDevelopment() // 开发环境下使用
defer logger.Sync() // flushes buffer, if any
sugar := logger.Sugar() // 使用sugar的实例,更方便的记录日志
sugar.Infow("failed to fetch URL",
// Structured context as loosely typed key-value pairs.
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)
}
- logger使用
package main
import (
"go.uber.org/zap"
)
func main() {
url := "https//www.baidu.com"
logger, _ := zap.NewProduction() // 生产环境下使用
// logger, _ := zap.NewDevelopment() // 开发环境下使用
defer logger.Sync() // flushes buffer, if any
// 这种效率比较高,就是因为指明了类型,zap就不会启用go的反射,这样效率就比较高
logger.Info("failed to fetch URL",
zap.String("url", url),
zap.Int("nums", 3),
)
}
2 - zap文件输出
package main
import (
"time"
"go.uber.org/zap"
)
func NewLogger() (*zap.Logger, error) {
cfg := zap.NewProductionConfig()
cfg.OutputPaths = []string{ //可以定位到多个文件中
"./myproject.log",
"stderr",
"stdout",
}
return cfg.Build()
}
func main() {
logger, err := NewLogger()
if err != nil {
panic(err)
//panic("初始化logger失败")
}
su := logger.Sugar()
defer su.Sync()
url := "https://www.baidu.com"
su.Info("failed to fetch URL",
// Structured context as strongly typed Field values.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
}
二、项目集成zap和router
这里主要要做的两件事情:
1、初始化zap和初始化router分离出独立的模块
2、在模块router中实现与api的绑定
- user_web\initialize\init_logger.go:初始化日志
package initialize
import "go.uber.org/zap"
func InitLogger() {
logger, _ := zap.NewDevelopment()
zap.ReplaceGlobals(logger)
}
- user_web\initialize\init_router.go:初始化router
package initialize
import (
"web_api/user_web/router"
"github.com/gin-gonic/gin"
)
func Routers() *gin.Engine {
Router := gin.Default()
ApiGroup := Router.Group("v1")
router.InitUserRouter(ApiGroup)
return Router
}
- user_web\router\router_user.go:user的RouterGroup
package router
import (
"web_api/user_web/api"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func InitUserRouter(Router *gin.RouterGroup) {
UserRouter := Router.Group("user")
zap.S().Info("配置用户相关的url")
{
UserRouter.GET("list", api.GetUserList)
}
}
- user_web\api\api_user.go:api对外接口
package api
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func GetUserList(ctx *gin.Context) {
zap.S().Debug("获取用户列表")
}
- user_web\main.go:main
package main
import (
"fmt"
"web_api/user_web/initialize"
"go.uber.org/zap"
)
func main() {
port := 8081
//2. 初始化logger
initialize.InitLogger()
//3. 初始化routers
Router := initialize.Routers()
/*
1. S()可以获取一个全局的sugar,可以让我们自己设置一个全局的logger
2. 日志是分级别的,debug, info , warn, error, fetal
debug最低,fetal最高,如果配置成info,所有比info低的都不会输出
NewProduction默认日志级别为info
NewDevelopment默认日志级别为debug
3. S函数和L函数很有用, 提供了一个全局的安全访问logger的途径
*/
zap.S().Debugf("启动服务器, 端口: %d", port)
if err := Router.Run(fmt.Sprintf(":%d", port)); err != nil {
zap.S().Panic("启动失败:", err.Error())
}
}
三、gin调用grpc服务
1 - YApi测试
- 启动YApi测试:参考地址
- user_web\initialize\init_router.go中将路径修改为
ApiGroup := Router.Group("/u/v1")
- YApi中注意修改端口号为8081
- user_web\initialize\init_router.go中将路径修改为
2 - proto生成
- 拷贝user_srv项目下的user.proto,并重新生成:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";
service User{
rpc GetUserList(PageInfo) returns (UserListResponse); // 用户列表
rpc GetUserByMobile(MobileRequest) returns (UserInfoResponse); //通过mobile查询用户
rpc GetUserById(IdRequest) returns (UserInfoResponse); //通过id查询用户
rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 添加用户
rpc UpdateUser(UpdateUserInfo) returns (google.protobuf.Empty); // 更新用户
rpc CheckPassWord(PasswordCheckInfo) returns (CheckResponse); //检查密码
}
message PageInfo {
uint32 pn = 1;
uint32 pSize = 2;
}
message UserInfoResponse {
int32 id = 1;
string passWord = 2;
string mobile = 3;
string nickName = 4;
uint64 birthDay = 5;
string gender = 6;
int32 role = 7;
}
message UserListResponse {
int32 total = 1;
repeated UserInfoResponse data = 2;
}
message CreateUserInfo {
string nickName = 1;
string passWord = 2;
string mobile = 3;
}
message MobileRequest{
string mobile = 1;
}
message IdRequest {
int32 id = 1;
}
message UpdateUserInfo {
int32 id = 1;
string nickName = 2;
string gender = 3;
uint64 birthDay = 4;
}
message PasswordCheckInfo {
string password = 1;
string encryptedPassword = 2;
}
message CheckResponse{
bool success = 1;
}
3 - gin调用grpc服务
- user_web\global\response\rsp_user.go:增加返回消息,用户对象的封装
package response
import (
"fmt"
"time"
)
type JsonTime time.Time
// 内部自动调用MarshalJSON方法
func (j JsonTime) MarshalJSON() ([]byte, error) {
var stmp = fmt.Sprintf("\"%s\"", time.Time(j).Format("2006-01-02"))
return []byte(stmp), nil
}
type UserResponse struct {
Id int32 `json:"id"`
NickName string `json:"name"`
Birthday JsonTime `json:"birthday"`
Gender string `json:"gender"`
Mobile string `json:"mobile"`
}
- user_web\api\api_user.go:
- 统一规范错误类型提示
- 实现用户列表接口查询
package api
import (
"context"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"web_api/user_web/global/response"
"web_api/user_web/proto"
)
func HandleGrpcErrorToHttp(err error, c *gin.Context) {
//将grpc的code转换成http的状态码
if err != nil {
if e, ok := status.FromError(err); ok {
switch e.Code() {
case codes.NotFound:
c.JSON(http.StatusNotFound, gin.H{
"msg": e.Message(),
})
case codes.Internal:
c.JSON(http.StatusInternalServerError, gin.H{
"msg:": "内部错误",
})
case codes.InvalidArgument:
c.JSON(http.StatusBadRequest, gin.H{
"msg": "参数错误",
})
case codes.Unavailable:
c.JSON(http.StatusInternalServerError, gin.H{
"msg": "用户服务不可用",
})
default:
c.JSON(http.StatusInternalServerError, gin.H{
"msg": e.Code(),
})
}
return
}
}
}
func GetUserList(ctx *gin.Context) {
ip := "127.0.0.1"
port := 50051
//拨号连接用户grpc服务器
userConn, err := grpc.Dial(fmt.Sprintf("%s:%d", ip, port), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
zap.S().Errorw("[GetUserList] 连接 【用户服务失败】", "msg", err.Error())
}
//生成grpc的client并调用接口
userSrvClient := proto.NewUserClient(userConn)
rsp, err := userSrvClient.GetUserList(context.Background(), &proto.PageInfo{
Pn: 0,
PSize: 0,
})
if err != nil {
zap.S().Errorw("[GetUserList] 查询 【用户列表】 失败")
HandleGrpcErrorToHttp(err, ctx)
return
}
result := make([]interface{}, 0)
for _, value := range rsp.Data {
user := response.UserResponse{
Id: value.Id,
NickName: value.NickName,
//Birthday: time.Time(time.Unix(int64(value.BirthDay), 0)).Format("2006-01-02"),
Birthday: response.JsonTime(time.Unix(int64(value.BirthDay), 0)),
Gender: value.Gender,
Mobile: value.Mobile,
}
result = append(result, user)
}
ctx.JSON(http.StatusOK, result)
}
- 测试结论
- YApi的docker需要开启
- user_srv需要开启,端口50051
- user_web的http端口,8081
四、viper配置管理
1 - viper简介
- Viper简介:Viper适用于Go应用程序的完整配置解决方案;它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式
- Viper的特性
- 设置默认值
- 从JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件读取配置信息
- 实时监控和重新读取配置文件(可选)
- 从环境变量中读取
- 从远程配置系统(etcd或Consul)读取并监控配置变化
- 从命令行参数读取配置
- 从buffer读取配置
- 显式配置值
- Viper地址:https://github.com/spf13/viper
- config.yaml:
name: 'user-web'
mysql:
host: '127.0.0.1'
port: 3306
package main
import (
"fmt"
"github.com/spf13/viper"
)
type ServerConfig struct {
ServiceName string `mapstructure:"name"`
Port int `mapstructure:"port"`
}
func main() {
v := viper.New()
//文件的路径如何设置
v.SetConfigFile("viper_test/config.yaml")
if err := v.ReadInConfig(); err != nil {
panic(err)
}
serverConfig := ServerConfig{}
if err := v.Unmarshal(&serverConfig); err != nil {
panic(err)
}
fmt.Println(serverConfig)
fmt.Printf("%v", v.Get("name"))
}
2 - viper环境隔离与动态监控
- 需求思考:如何实现,不用改任何代码而且线上和线上的配置文件能隔离开
- 解决方案:
- 采用环境变量的方式来确定是生产环境还是开发环境
- 使用
fsnotify
文件变化通知库来实现动态监控
- main.go
package main
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"time"
)
//如何将线上和线下的配置文件隔离
//不用改任何代码而且线上和线上的配置文件能隔离开
type MysqlConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct {
ServiceName string `mapstructure:"name"`
MysqlInfo MysqlConfig `mapstructure:"mysql"`
}
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
return viper.GetBool(env)
//刚才设置的环境变量 想要生效 我们必须得重启goland
}
func main() {
debug := GetEnvInfo("DEV_CONFIG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("viper_test/%s_pro.yaml", configFilePrefix)
if debug {
configFileName = fmt.Sprintf("viper_test/%s_debug.yaml", configFilePrefix)
}
v := viper.New()
//文件的路径如何设置
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
serverConfig := ServerConfig{}
if err := v.Unmarshal(&serverConfig); err != nil {
panic(err)
}
fmt.Println(serverConfig)
fmt.Printf("%v", v.Get("name"))
//viper的功能 - 动态监控变化
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file channed: ", e.Name)
_ = v.ReadInConfig()
_ = v.Unmarshal(&serverConfig)
fmt.Println(serverConfig)
})
time.Sleep(time.Second * 300)
}
- yaml
//config_pro.yaml
name: 'user-web'
mysql:
host: '127.0.0.2'
port: 3309
//config_debug.yaml
name: 'user-web2'
mysql:
host: '127.0.0.1'
port: 3306
3 - gin集成viper
- 需要集成的配置包含
- user_srv的服务配置信息
- user_web的服务配置信息
- yaml
//config_debug.yaml
name: 'user-web'
port: '8081'
user_srv:
host: '127.0.0.1'
port: '50051'
//config_pro.yaml
name: 'user-web'
port: '8031'
user_srv:
host: '127.0.0.1'
port: '50052'
- user_web\config\config.go:添加user_srv和user_web的配置struct
package config
type UserSrvConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name" json:"name"`
Port int `mapstructure:"port" json:"port"`
UserSrvInfo UserSrvConfig `mapstructure:"user_srv" json:"user_srv"`
}
- user_web\global\global.go:为了让ServerConfig可以在其他文件中读取,在global.go中添加全局变量
package global
import "web_api/user_web/config"
var (
ServerConfig *config.ServerConfig = &config.ServerConfig{}
)
- user_web\initialize\init_config.go:初始化读取配置信息
package initialize
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap"
"web_api/user_web/global"
)
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
return viper.GetBool(env)
//刚才设置的环境变量 想要生效 我们必须得重启goland
}
func InitConfig() {
debug := GetEnvInfo("DEV_CONFIG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("%s_pro.yaml", configFilePrefix)
if debug {
configFileName = fmt.Sprintf("%s_debug.yaml", configFilePrefix)
}
v := viper.New()
//文件的路径如何设置
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
//这个对象如何在其他文件中使用 - 全局变量
if err := v.Unmarshal(&global.ServerConfig); err != nil {
panic(err)
}
zap.S().Infof("配置信息: &v", global.ServerConfig)
//viper的功能 - 动态监控变化
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
zap.S().Infof("配置文件产生变化: &s", e.Name)
_ = v.ReadInConfig()
_ = v.Unmarshal(&global.ServerConfig)
zap.S().Infof("配置信息: &v", global.ServerConfig)
})
}
- main.go
- 添加viper的初始化:
initialize.InitConfig()
- 服务启动端口修改为全局的对象:
global.ServerConfig.Port
- 添加viper的初始化:
package main
import (
"fmt"
"web_api/user_web/global"
"web_api/user_web/initialize"
"go.uber.org/zap"
)
func main() {
//1. 初始化logger
initialize.InitLogger()
//2. 初始化配置文件
initialize.InitConfig()
//3. 初始化routers
Router := initialize.Routers()
/*
1. S()可以获取一个全局的sugar,可以让我们自己设置一个全局的logger
2. 日志是分级别的,debug, info , warn, error, fetal
debug最低,fetal最高,如果配置成info,所有比info低的都不会输出
NewProduction默认日志级别为info
NewDevelopment默认日志级别为debug
3. S函数和L函数很有用, 提供了一个全局的安全访问logger的途径
*/
zap.S().Debugf("启动服务器, 端口: %d", global.ServerConfig.Port)
if err := Router.Run(fmt.Sprintf(":%d", global.ServerConfig.Port)); err != nil {
zap.S().Panic("启动失败:", err.Error())
}
}