这篇文章,我打算自己做一个自己常用的一些 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: 8086user: "root"password: "root"dbname: "user"max_idle_conn: 5max_open_conn: 20max_idle_time: 300
全局配置文件包
// config.go
package configimport ("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 utilimport ("Config/config""fmt""gorm.io/driver/mysql""gorm.io/gorm""sync"
)var (db *gorm.DBdbOnce 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 errordb, 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 modelimport "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 {CreateModelModifyModelID 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 utilimport ("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 mainimport "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 # 6379rdb: 0passwd: ''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 utilimport ("Config/config""context""fmt""github.com/redis/go-redis/v9""sync"
)var (redisConn *redis.ClientredisOnce sync.Once
)func initRedis() {redisConfig := config.GetGlobalConf().RedisConffmt.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 utilimport ("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: 200max_age: 30max_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 utilimport ("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) {// 获取用于日志轮换的WriteSyncerwriteSyncer := 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.ISO8601TimeEncoderencoderConfig.TimeKey = "time"encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoderencoderConfig.EncodeDuration = zapcore.SecondsDurationEncoderencoderConfig.EncodeCaller = zapcore.ShortCallerEncoderreturn 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.Pathquery := c.Request.URL.RawQueryc.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 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 {// 使用zap.Error记录错误信息lg.Error(c.Request.URL.Path,zap.Any("error", err),zap.String("request", string(httpRequest)),)// 如果连接已断开,我们无法向其写入状态。c.Error(err.(error)) // nolint: errcheckc.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 utilimport ("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))}
}