自定义工具包:加快项目开发速度

发布时间:2024年01月18日

? 这篇文章,我打算自己做一个自己常用的一些 Go 语言的通用工具包,比如 MySQL、Redis 的连接,日志配置等。

MySQL

启动 MySQL 服务

? 我们先用 docker 起一个 MySQL 服务,便于我们后面的连接。

? 用 docker 启动一个 MySQL 容器:

docker run --name go_mysql -e MYSQL_ROOT_PASSWORD=root -d -p 8086:3306 mysql:8.0

? 登入容器内的 MySQL:

docker run -it --network=host --name=ft  mysql:8.0 mysql -h 0.0.0.0  -P 8086 -u root -p

? 这样,我们的 MySQL 服务就启动了。

? 以后每次重新进入该容器,可以使用下述命令:

docker start -i ft

? 我们在数据库中创建一个用户表,用于后面和数据库进行交互操作:

create database user;
use user;

create table if not exists t_user (
    id int not null auto_increment,
    name varchar(103) not null,
    age int not null,
    gender varchar(30) not null,
    password varchar(255) not null default '',
    nickname varchar(100) not null default '',
    create_time timestamp null default current_timestamp comment '创建时间',
    creator varchar(100) not null default '',
    modify_time timestamp null default current_timestamp on update current_timestamp,
    modifier varchar(188) not null default '',
    primary key (id)
);

? 这里我们还需要注意的一个点,就是需要将 服务器对应的端口进行开放,不然本地的项目无法连接上服务器上的数据库,由于我们这里将服务器上的 8086 端口映射到容器内的 3306 端口,所以我们需要将服务器的 8086 端口对外开放。

使用 Viper 进行项目配置

? 详细配置可以查看官网

使用 gorm 框架进行数据库连接

? 详情可以查看官网

编写配置信息文件

? 这里使用 yml 格式:

# Config.yml
db:
  host: "101.200.158.100"
  port: 8086
  user: "root"
  password: "root"
  dbname: "user"
  max_idle_conn: 5
  max_open_conn: 20
  max_idle_time: 300
全局配置文件包
// config.go
package config

import (
	"fmt"
	"github.com/spf13/viper"
	"sync"
	"time"
)

var (
	config GlobalConfig // 全局配置文件
	once   sync.Once
)

// GetGlobalConf 全局配置文件构造函数
func GetGlobalConf() *GlobalConfig {
	once.Do(readConf)
	return &config
}

type DbConf struct {
	Host        string        `yaml:"host" mapstructure:"host"`                   // 主机号
	Port        string        `yaml:"port" mapstructure:"port"`                   // 端口号
	User        string        `yaml:"user" mapstructure:"user"`                   // 用户
	Password    string        `yaml:"password" mapstructure:"password"`           // 密码
	Dbname      string        `yaml:"Dbname" mapstructure:"Dbname"`               // 数据库名
	MaxIdleConn int           `yaml:"max_idle_conn" mapstructure:"max_idle_conn"` // 最大空闲连接数
	MaxOpenConn int           `yaml:"max_open_conn" mapstructure:"max_open_conn"` // 最大连接数
	MaxIdleTime time.Duration `yaml:"max_idle_time" mapstructure:"max_idle_time"` // 最大空闲时间
}

type GlobalConfig struct {
	DbConf DbConf `yaml:"db" mapstructure:"db"` // 数据库配置
}

// 将配置文件中的信息 加载到 全局配置信息结构体中
func readConf() {
	viper.SetConfigName("Config")   // 配置文件名
	viper.SetConfigType("yml")      // 配置文件类型
	viper.AddConfigPath("./config") // 配置文件路径
	viper.AddConfigPath("../config")
	err := viper.ReadInConfig() // 读取配置信息
	if err != nil {
		panic("read config file err:" + err.Error())
	}

	err = viper.Unmarshal(&config) // 将配置文件中的信息反序列化到结构体中
	if err != nil {
		panic("config file unmarshal err:" + err.Error())
	}
	// TODO: 这里需要用日志打印
	fmt.Printf("%+v\n", config)
    
    // 热更新
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		// 配置文件发生变更之后会调用的回调函数
		readConf()
		fmt.Println("Config file changed:", e.Name)
	})
}

// InitConfig TODO: 初始化日志
func InitConfig() {
	globalConf := GetGlobalConf()
	fmt.Println("GlobalConf:", globalConf)
}
MySQL 数据库连接文件
// db.go
package util

import (
	"Config/config"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"sync"
)

var (
	db     *gorm.DB
	dbOnce sync.Once
)

// 连接数据库
func openDB() {
	mysqlConf := config.GetGlobalConf().DbConf
	// 连接语句
	connArgs := fmt.Sprintf("%s:%s@(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local",
		mysqlConf.User, mysqlConf.Password, mysqlConf.Host, mysqlConf.Port, mysqlConf.Dbname)
	fmt.Println("mdb addr: " + connArgs)
	var err error
	db, err = gorm.Open(mysql.Open(connArgs), &gorm.Config{}) // 使用默认配置连接数据库
	if err != nil {
		panic("failed to connect database")
	}
	// 获取底层 sql.Db 连接对象
	sqlDB, err := db.DB()
	if err != nil {
		panic("fetch db connection err:" + err.Error())
	}

	sqlDB.SetMaxOpenConns(mysqlConf.MaxOpenConn)
	sqlDB.SetMaxIdleConns(mysqlConf.MaxIdleConn)
	sqlDB.SetConnMaxLifetime(mysqlConf.MaxIdleTime)
}

func GetDB() *gorm.DB {
	dbOnce.Do(openDB)
	return db
}
测试模型和测试代码

? 这里用一个用户信息来进行测试:

// model.go
package model

import "time"

// CreateModel 内嵌 model
type CreateModel struct {
	Creator    string    `gorm:"type:varchar(100);not null;default ''"`
	CreateTime time.Time `gorm:"autoCreateTime"` // 在创建时自动生成时间
}

// ModifyModel 内嵌 model
type ModifyModel struct {
	Modifier   string    `gorm:"type:varchar(100);not null;default ''"`
	ModifyTime time.Time `gorm:"autoUpdateTime"` // 在更新记录时自动生成时间
}

type User struct {
	CreateModel
	ModifyModel
	ID       int    `gorm:"column:id"`
	Name     string `gorm:"column:name"`
	Gender   string `gorm:"column:gender"`
	Age      int    `gorm:"column:age"`
	PassWord string `gorm:"column:password"`
	NickName string `gorm:"column:nickname"`
}

// TableName 需要使用钩子函数指定数据库表名
func (t *User) TableName() string {
	return "t_user"
}

? 这里用单元测试测试 5 个功能,分别是连接、增加、修改、删除、查询:

package util

import (
	"Config/config"
	"Config/model"
	"fmt"
	"os"
	"testing"
	"time"
)

func TestMain(m *testing.M) {
	config.InitConfig()
	os.Exit(m.Run())
}

func TestGetDB(t *testing.T) {
	db := GetDB()
	if db == nil {
		t.Errorf("database connection failed")
	}
}

func TestInsert(t *testing.T) {
	db := GetDB()
	user := model.User{
		CreateModel: model.CreateModel{
			Creator:    "admin", // 请替换为实际的创建者
			CreateTime: time.Now(),
		},
		ModifyModel: model.ModifyModel{
			Modifier:   "admin", // 请替换为实际的修改者
			ModifyTime: time.Now(),
		},
		ID:       1,
		Name:     "ft",
		Gender:   "Male",
		Age:      25,
		PassWord: "password123",
		NickName: "johnny",
	}
	if err := db.Create(user).Error; err != nil {
		t.Errorf("createUser failed: %v", err)
	}
}

func TestSelect(t *testing.T) {
	db := GetDB()
	message := &model.User{}
	if err := db.Where("name=?", "ft").First(message).Error; err != nil {
		t.Error("database name=ft is not exits")
	} else {
		fmt.Printf("%+v\n", message)
	}
}

func TestUpdate(t *testing.T) {
	db := GetDB()
	if err := db.Model(model.User{}).Where("name=?", "ft").Update("name", "fengtao").Error; err != nil {
		t.Error("update user failed")
	}
}

func TestDelete(t *testing.T) {
	db := GetDB()
	if err := db.Where("name=?", "fengtao").Delete(&model.User{}).Error; err != nil {
		t.Error("delete user failed")
	}
}
main 主函数
package main

import "Config/config"

// Init 初始化配置
func Init() {
	config.InitConfig()
}

func main() {
	Init()
}

Redis

启动 Redis 服务

? 用下面的命令启动一个 Redis 服务:

docker run --name go_redis -d -p 8089:6379 redis:6.2-rc2

? 然后用下面的命令在 Redis 容器中启动 redis-cli,连接到 Redis 服务器:

docker exec -it b8f50ba94db0 redis-cli -h 0.0.0.0 -p 6379

? 或者通过连接主机上的 8089 端口,因为刚刚做了映射,所以这其实相当于是访问同一个 Redis 服务器:

docker run -it --network=host --name=ft_redis redis:6.2-rc2 redis-cli -h 0.0.0.0 -p 8089

:::warning

? 同时,也不要忘记将服务器中对应的 8089 和 6379 端口开发,否则是无法连接上的。

:::

安装 Redis

? 使用下面的命令将 Redis 安装到项目中:

go get "github.com/redis/go-redis/v9"
编写配置信息文件
// Config.yml
redis:
  rhost: "101.200.158.100"
  rport: 8089 # 6379
  rdb: 0
  passwd: ''
  poolsize: 100
全局配置文件包
// config.go
type RedisConf struct {
	Host     string `yaml:"rhost" mapstructure:"rhost"`
	Port     int    `yaml:"rport" mapstructure:"rport"`
	DB       int    `yaml:"rdb" mapstructure:"rdb"`
	Password string `yaml:"passwd" mapstructure:"passwd"`
	PoolSize int    `yaml:"poolsize" mapstructure:"poolsize"`
}

type GlobalConfig struct {
	DbConf    DbConf    `yaml:"db" mapstructure:"db"` // 数据库配置
	RedisConf RedisConf `yaml:"redis" mapstructure:"redis"`
}
Redis 数据库连接文件
package util

import (
	"Config/config"
	"context"
	"fmt"
	"github.com/redis/go-redis/v9"
	"sync"
)

var (
	redisConn *redis.Client
	redisOnce sync.Once
)

func initRedis() {
	redisConfig := config.GetGlobalConf().RedisConf
	fmt.Printf("redisConfig ====== %+v\n", redisConfig)
	addr := fmt.Sprintf("%s:%d", redisConfig.Host, redisConfig.Port)
	redisConn = redis.NewClient(&redis.Options{
		Addr:     addr,
		Password: redisConfig.Password,
		DB:       redisConfig.DB,
		PoolSize: redisConfig.PoolSize,
	})
	if redisConn == nil {
		panic("failed to call redis.NewClient")
	}

	res, err := redisConn.Set(context.Background(), "abc", 100, 60).Result()
	fmt.Printf("res ======== %v, err ======= %v\n", res, err)
	_, err = redisConn.Ping(context.Background()).Result()
	if err != nil {
		fmt.Printf("Failed to ping redis, err:%s\n", err)
	}
}

func GetRedisCli() *redis.Client {
	redisOnce.Do(initRedis)
	return redisConn
}
测试代码
package util

import (
	"context"
	"testing"
	"time"
)

func TestGetRedisCli(t *testing.T) {
	redis := GetRedisCli()
	if redis == nil {
		t.Errorf("Failed Redis database connection\n")
	} else {
		t.Log("success connected to Redis.")
	}
}

func TestRedisAdd(t *testing.T) {
	redis := GetRedisCli()
	if redis == nil {
		t.Errorf("Failed Redis database connection\n")
	}
	_, err := redis.Set(context.Background(), "ft", "18", 60*time.Second).Result()
	if err != nil {
		t.Errorf("failed to add a key-value to redis, err:%s\n", err)
	}
	value, err := redis.Get(context.Background(), "ft").Result()
	if err != nil {
		t.Errorf("failed to add a key-value to redis, err:%s\n", err)
	}

	if value == "18" {
		t.Log("success quart key-value")
	} else if value == "" {
		t.Log("key-value expired")
	}
}

func TestRedisDel(t *testing.T) {
	redis := GetRedisCli()
	if redis == nil {
		t.Errorf("Failed Redis database connection\n")
	}
	_, err := redis.Del(context.Background(), "ft").Result()
	if err != nil {
		t.Errorf("failed del key, err:%s\n", err)
	} else {
		t.Log("success del key")
	}
}

Log

安装 zap 日志库

? 我们这里基于 Zap 实现在用 Gin 框架开发中常用的两个中间件:Logger() 和 Recovery(),这样我们就可以使用我们的日志库来接收 gin 框架默认输出的日志了。

go get -u go.uber.org/zap	// zap 日志库
go get -u github.com/gin-gonic/gin // gin 框架
go get github.com/natefinch/lumberjack // 滚动日志库
编写配置信息文件
// Config.yml
Log:
  level: "debug"
  # log_path: "" # 这是相对于单元测试的路径
  filename: "../log/Config/Config.log"
  max_size: 200
  max_age: 30
  max_backups: 7
全局配置文件包
// Config.go
type LogConfig struct {
	Level    string `yaml:"level" mapstructure:"level"`
	Filename string `yaml:"filename" mapstructure:"filename"`
	//LogPath    string `yaml:"log_path" mapstructure:"log_path"`
	MaxSize    int `yaml:"maxsize" mapstructure:"maxsize"`
	MaxAge     int `yaml:"max_age" mapstructure:"max_age"`
	MaxBackups int `yaml:"max_backups" mapstructure:"max_backups"`
}
自定义日志工具包
package util

import (
	"Config/config"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/natefinch/lumberjack"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

var lg *zap.Logger

// InitLogger 初始化Logger
func InitLogger(cfg *config.LogConfig) (err error) {
	// 获取用于日志轮换的WriteSyncer
	writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
	// 获取日志编码器
	encoder := getEncoder()

	// 解析日志级别
	var l = new(zapcore.Level)
	err = l.UnmarshalText([]byte(cfg.Level))
	if err != nil {
		return
	}

	// 创建新的zap Logger核心
	core := zapcore.NewCore(encoder, writeSyncer, l)

	// 创建新的zap Logger实例,并添加调用者信息
	lg = zap.New(core, zap.AddCaller())
	// 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
	zap.ReplaceGlobals(lg)
	return
}

// getEncoder 返回zapcore.Encoder实例
func getEncoder() zapcore.Encoder {
	// JSON编码器的配置
	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)
}

// getLogWriter 返回zapcore.WriteSyncer实例,使用lumberjack.Logger作为日志轮换的底层日志记录器
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)
		// 使用zap.Info记录信息
		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 恢复项目可能出现的panic,并使用zap记录相关信息
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// 检查是否是断开的连接,这不需要panic堆栈跟踪。
				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 {
					// 使用zap.Error记录错误信息
					lg.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// 如果连接已断开,我们无法向其写入状态。
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					// 使用zap.Error记录带有堆栈跟踪的错误信息
					lg.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					// 使用zap.Error记录不带堆栈跟踪的错误信息
					lg.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

? 在使用时,需要注意,先使用初始化函数 InitLogger 将配置文件中的信息读取出来。

测试代码
package util

import (
	"Config/config"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"testing"
)

func TestLog(t *testing.T) {
	if err := InitLogger(&config.GetGlobalConf().LogConfig); err != nil {
		t.Errorf("Failed to initialize logger: " + err.Error())
	}

	r := gin.New()
	r.Use(GinLogger(), GinRecovery(true))
	r.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"name": "ft",
			"age":  19,
		})
	})
	if err := r.Run(":8080"); err != nil {
		zap.L().Fatal("failed to start server, err:", zap.Error(err))
	}
}
文章来源:https://blog.csdn.net/m0_62264224/article/details/135672585
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。