? 这篇文章,我打算自己做一个自己常用的一些 Go 语言的通用工具包,比如 MySQL、Redis 的连接,日志配置等。
? 我们先用 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 端口对外开放。
? 详细配置可以查看官网。
? 详情可以查看官网。
? 这里使用 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)
}
// 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")
}
}
package main
import "Config/config"
// Init 初始化配置
func Init() {
config.InitConfig()
}
func main() {
Init()
}
? 用下面的命令启动一个 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 安装到项目中:
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"`
}
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")
}
}
? 我们这里基于 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))
}
}