提交 9b11dbc1 authored 作者: mooncake's avatar mooncake

utils

上级 76f3cb6b
SHELL := /bin/bash SHELL := /bin/bash
PROJECT_NAME := "olog" PROJECT_NAME := "opkg"
PKG := "$(PROJECT_NAME)" PKG := "$(PROJECT_NAME)"
PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/ | grep -v /api/ | grep -v /cmd/) PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/ | grep -v /api/ | grep -v /cmd/)
......
...@@ -2,10 +2,11 @@ package config ...@@ -2,10 +2,11 @@ package config
import ( import (
"fmt" "fmt"
"olog/utils/fileutils"
"os" "os"
"path/filepath" "path/filepath"
"gitlab.wanzhuangkj.com/tush/opkg/xutils/fileutils"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
......
module olog module gitlab.wanzhuangkj.com/tush/opkg
go 1.24.3 go 1.24.3
require ( require (
github.com/bwmarrin/snowflake v0.3.0
github.com/duke-git/lancet/v2 v2.3.8
github.com/gin-gonic/gin v1.11.0
github.com/huandu/xstrings v1.5.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
google.golang.org/grpc v1.76.0 google.golang.org/grpc v1.76.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
gorm.io/plugin/dbresolver v1.6.2
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
) )
差异被折叠。
package entity
import (
"bytes"
"fmt"
"github.com/gin-gonic/gin"
ctxutils "gitlab.wanzhuangkj.com/tush/opkg/xutils/ctxutils"
)
type CopyHttpReq struct {
Method string `json:"method"`
URL string `json:"url"`
Headers map[string][]string `json:"headers"`
Body string `json:"body"`
}
type CopyHttpRsp struct {
Headers map[string][]string `json:"headers"`
Body string `json:"body"`
}
func SetCopyReq(c *gin.Context, buf *bytes.Buffer) {
copyHttpReq := &CopyHttpReq{}
copyHttpReq.Method = c.Request.Method
copyHttpReq.URL = c.Request.URL.String()
copyHttpReq.Headers = c.Request.Header
copyHttpReq.Body = buf.String()
c.Set(ctxutils.KeyReqBody, copyHttpReq)
}
func GetCopyReq(c *gin.Context) *CopyHttpReq {
if val, isExist := c.Get(ctxutils.KeyReqBody); isExist {
if req, ok := val.(*CopyHttpReq); ok {
return req
}
}
return nil
}
func SetCopyRsp(c *gin.Context, buf *bytes.Buffer) {
copyHttpRsp := &CopyHttpRsp{}
copyHttpRsp.Headers = c.Writer.Header()
copyHttpRsp.Body = buf.String()
c.Set(ctxutils.KeyRspBody, copyHttpRsp)
}
func GetCopyRsp(c *gin.Context) *CopyHttpRsp {
if val, isExist := c.Get(ctxutils.KeyRspBody); isExist {
if rsp, ok := val.(*CopyHttpRsp); ok {
return rsp
}
}
return nil
}
func SetCopyApiReq(c *gin.Context, buf *bytes.Buffer) {
copyHttpReq := &CopyHttpReq{}
copyHttpReq.Method = c.Request.Method
copyHttpReq.URL = c.Request.URL.String()
copyHttpReq.Headers = c.Request.Header
copyHttpReq.Body = buf.String()
c.Set(fmt.Sprintf(ctxutils.KeyOApiReqBody, ctxutils.GinTraceID(c)), copyHttpReq)
}
func GetCopyApiReq(c *gin.Context) *CopyHttpReq {
if val, isExist := c.Get(fmt.Sprintf(ctxutils.KeyOApiReqBody, ctxutils.GinTraceID(c))); isExist {
if req, ok := val.(*CopyHttpReq); ok {
return req
}
}
return nil
}
func SetCopyApiRsp(c *gin.Context, buf *bytes.Buffer) {
copyHttpRsp := &CopyHttpRsp{}
copyHttpRsp.Headers = c.Writer.Header()
copyHttpRsp.Body = buf.String()
c.Set(fmt.Sprintf(ctxutils.KeyOApiRspBody, ctxutils.GinTraceID(c)), copyHttpRsp)
}
func GetCopyApiRsp(c *gin.Context) *CopyHttpRsp {
if val, isExist := c.Get(fmt.Sprintf(ctxutils.KeyOApiRspBody, ctxutils.GinTraceID(c))); isExist {
if rsp, ok := val.(*CopyHttpRsp); ok {
return rsp
}
}
return nil
}
package httpContentType
import (
"net/http"
"strings"
)
func IsMedia(header http.Header) bool {
ct := header.Get("Content-Type")
isMedia := false
if strings.Contains(ct, "image") {
isMedia = true
}
return isMedia
}
差异被折叠。
package olog package logger
type Config struct { type Config struct {
Format string `yaml:"format" json:"format" mapstructure:"format"` Format string `yaml:"format" json:"format" mapstructure:"format"`
......
package olog package logger
import ( import (
"fmt" "fmt"
......
package olog package logger
import ( import (
"testing" "testing"
......
package olog package logger
import ( import (
"encoding/json" "encoding/json"
......
package olog package logger
import ( import (
"fmt" "fmt"
......
package olog package logger
import ( import (
"strings" "strings"
......
package olog package logger
import ( import (
"strings" "strings"
......
package olog package logger
import ( import (
"fmt" "fmt"
......
package olog package logger
import ( import (
"errors" "errors"
......
// Package sgorm is a library encapsulated on gorm.io/gorm
package sgorm
import (
"reflect"
"time"
"github.com/huandu/xstrings"
"gorm.io/gorm"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/dbclose"
)
type DB = gorm.DB
var ErrRecordNotFound = gorm.ErrRecordNotFound
const (
// DBDriverMysql mysql driver
DBDriverMysql = "mysql"
// DBDriverPostgresql postgresql driver
DBDriverPostgresql = "postgresql"
// DBDriverTidb tidb driver
DBDriverTidb = "tidb"
// DBDriverSqlite sqlite driver
DBDriverSqlite = "sqlite"
)
// Model embedded structs, add `gorm: "embedded"` when defining table structs
type Model struct {
ID uint64 `gorm:"column:id;AUTO_INCREMENT;primary_key" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// Model2 embedded structs, json tag named is snake case
type Model2 struct {
ID uint64 `gorm:"column:id;AUTO_INCREMENT;primary_key" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// KV map type
type KV = map[string]interface{}
// GetTableName get table name
func GetTableName(object interface{}) string {
tableName := ""
typeof := reflect.TypeOf(object)
switch typeof.Kind() {
case reflect.Ptr:
tableName = typeof.Elem().Name()
case reflect.Struct:
tableName = typeof.Name()
default:
return tableName
}
return xstrings.ToSnakeCase(tableName)
}
// CloseDB close db
func CloseDB(db *gorm.DB) error {
return dbclose.Close(db)
}
package sgorm
import (
"testing"
"github.com/stretchr/testify/assert"
)
type userExample struct {
Model `gorm:"embedded"`
Name string `gorm:"type:varchar(40);unique_index;not null" json:"name"`
Age int `gorm:"not null" json:"age"`
Gender string `gorm:"type:varchar(10);not null" json:"gender"`
}
func TestGetTableName(t *testing.T) {
name := GetTableName(&userExample{})
assert.NotEmpty(t, name)
name = GetTableName(userExample{})
assert.NotEmpty(t, name)
name = GetTableName("table")
assert.Empty(t, name)
}
// Package dbclose provides a function to close gorm db.
package dbclose
import (
"context"
"database/sql"
"time"
"gorm.io/gorm"
)
// Close close gorm db
func Close(db *gorm.DB) error {
if db == nil {
return nil
}
sqlDB, err := db.DB()
if err != nil {
return err
}
checkInUse(sqlDB, time.Second*5)
return sqlDB.Close()
}
func checkInUse(sqlDB *sql.DB, duration time.Duration) {
ctx, _ := context.WithTimeout(context.Background(), duration) //nolint
for {
select {
case <-time.After(time.Millisecond * 250):
if v := sqlDB.Stats().InUse; v == 0 {
return
}
case <-ctx.Done():
return
}
}
}
package dbclose
import (
"database/sql"
"testing"
"time"
"gorm.io/gorm"
)
func TestCloseDB(t *testing.T) {
sqlDB := new(sql.DB)
checkInUse(sqlDB, time.Millisecond*100)
checkInUse(sqlDB, time.Millisecond*600)
db := new(gorm.DB)
defer func() { recover() }()
_ = Close(db)
}
// Package glog provides a gorm logger implementation based on zap.
package glog
import (
"context"
"runtime"
"strconv"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm/logger"
)
const (
FieldFileLine = "file"
)
type gormLogger struct {
gLog *zap.Logger
requestIDKey string
logLevel logger.LogLevel
}
// NewCustomGormLogger custom gorm logger
func NewCustomGormLogger(l *zap.Logger, requestIDKey string, logLevel logger.LogLevel) logger.Interface {
if l == nil {
l, _ = zap.NewProduction()
}
if requestIDKey == "" {
requestIDKey = "X-Request-ID"
}
if logLevel == 0 {
logLevel = logger.Info
}
return &gormLogger{
gLog: l,
requestIDKey: requestIDKey,
logLevel: logLevel,
}
}
// LogMode log mode
func (l *gormLogger) LogMode(level logger.LogLevel) logger.Interface {
l.logLevel = level
return l
}
// Info print info
func (l *gormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Info {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Info(msg, zap.Any("data", data), zap.String("line", FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Warn print warn messages
func (l *gormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Warn {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Warn(msg, zap.Any("data", data), zap.String("line", FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Error print error messages
func (l *gormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Error {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Warn(msg, zap.Any("data", data), zap.String("line", FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Trace print sql message
func (l *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if l.logLevel <= logger.Silent {
return
}
cost := time.Since(begin).String()
sql, rows := fc()
var rowsField zap.Field
if rows == -1 {
rowsField = zap.String("rows", "-")
} else {
rowsField = zap.Int64("rows", rows)
}
var fileLineField zap.Field
fileLine := FileWithLineNum()
ss := strings.Split(fileLine, "/internal/")
if len(ss) == 2 {
fileLineField = zap.String(FieldFileLine, ss[1])
} else {
fileLineField = zap.String(FieldFileLine, fileLine)
}
sql = clearSql(sql)
if err != nil {
l.gLog.Warn("[gorm]",
zap.Error(err),
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
return
}
if l.logLevel >= logger.Info {
l.gLog.Info("[gorm]",
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
return
}
if l.logLevel >= logger.Warn {
l.gLog.Warn("[gorm]",
zap.String("sql", sql),
rowsField,
zap.String("cost", cost),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
}
}
func clearSql(sql string) string {
sql = strings.ReplaceAll(sql, "\n", "")
sql = strings.ReplaceAll(sql, "\r", "")
sql = strings.ReplaceAll(sql, "\t", "")
return sql
}
func requestIDField(ctx context.Context, requestIDKey string) zap.Field {
if requestIDKey == "" {
return zap.Skip()
}
var field zap.Field
if requestIDKey != "" {
if v, ok := ctx.Value(requestIDKey).(string); ok {
field = zap.String(requestIDKey, v)
} else {
field = zap.Skip()
}
}
return field
}
func FileWithLineNum() string {
for i := 2; i < 15; i++ {
_, file, line, ok := runtime.Caller(i)
if ok && !strings.Contains(file, "common/odao") && (!strings.Contains(file, "gorm.io/gorm@") || strings.HasSuffix(file, "_test.go")) {
return file + ":" + strconv.FormatInt(int64(line), 10)
}
}
return ""
}
package glog
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"gorm.io/gorm/logger"
)
func TestNewCustomGormLogger(t *testing.T) {
zapLog, _ := zap.NewDevelopment()
l := NewCustomGormLogger(zapLog, "request_id", logger.Info)
l.LogMode(logger.Info)
ctx := context.WithValue(context.Background(), "request_id", "123")
l.Info(ctx, "info", "foo")
l.Warn(ctx, "warn", "bar")
l.Error(ctx, "error", "foo bar")
l.LogMode(logger.Silent)
l.Trace(ctx, time.Now(), nil, nil)
l.LogMode(logger.Info)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 1
}, nil)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", -1
}, nil)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, logger.ErrRecordNotFound)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, errors.New("Error 1054: Unknown column 'test_column'"))
l.LogMode(logger.Warn)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, logger.ErrRecordNotFound)
}
func Test_requestIDField(t *testing.T) {
ctx := context.WithValue(context.Background(), "request_id", "123")
field := requestIDField(ctx, "")
assert.Equal(t, zap.Skip(), field)
field = requestIDField(ctx, "your request id key")
assert.Equal(t, zap.Skip(), field)
field = requestIDField(ctx, "request_id")
assert.Equal(t, zap.String("request_id", "123"), field)
}
// Package mysql provides a gorm driver for mysql.
package mysql
import (
"database/sql"
"log"
"os"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
mysqlDriver "gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gorm.io/plugin/dbresolver"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/dbclose"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/glog"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
)
// Init mysql
func Init(dsn string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
sqlDB, err := sql.Open("mysql", dsn)
if err != nil {
return nil, xerror.New(err.Error())
}
sqlDB.SetMaxIdleConns(o.maxIdleConns) // set the maximum number of connections in the idle connection pool
sqlDB.SetMaxOpenConns(o.maxOpenConns) // set the maximum number of open database connections
sqlDB.SetConnMaxLifetime(o.connMaxLifetime) // set the maximum time a connection can be reused
db, err := gorm.Open(mysqlDriver.New(mysqlDriver.Config{Conn: sqlDB}), gormConfig(o))
if err != nil {
return nil, xerror.New(err.Error())
}
db.Set("gorm:table_options", "CHARSET=utf8mb4") // automatic appending of table suffixes when creating tables
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, xerror.Errorf("using gorm opentelemetry, err: %v", err)
}
}
if err = sqlDB.Ping(); err != nil {
return nil, xerror.Errorf("ping db failed, err: %v", err)
}
// register read-write separation plugin
if len(o.slavesDsn) > 0 {
err = db.Use(rwSeparationPlugin(o))
if err != nil {
return nil, xerror.New(err.Error())
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, xerror.New(err.Error())
}
}
return db, nil
}
// InitTidb init tidb
func InitTidb(dsn string, opts ...Option) (*gorm.DB, error) {
return Init(dsn, opts...)
}
// gorm setting
func gormConfig(o *options) *gorm.Config {
config := &gorm.Config{
// disable foreign key constraints, not recommended for production environments
DisableForeignKeyConstraintWhenMigrating: o.disableForeignKey,
// removing the plural of an epithet
NamingStrategy: schema.NamingStrategy{SingularTable: true},
}
// print SQL
if o.isLog {
if o.gLog == nil {
config.Logger = logger.Default.LogMode(o.logLevel)
} else {
config.Logger = glog.NewCustomGormLogger(o.gLog, o.requestIDKey, o.logLevel)
}
} else {
config.Logger = logger.Default.LogMode(logger.Silent)
}
// print only slow queries
if o.slowThreshold > 0 {
config.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // use the standard output asWriter
logger.Config{
SlowThreshold: o.slowThreshold,
Colorful: true,
LogLevel: logger.Warn, // set the logging level, only above the specified level will output the slow query log
},
)
}
return config
}
func rwSeparationPlugin(o *options) gorm.Plugin {
slaves := []gorm.Dialector{}
for _, dsn := range o.slavesDsn {
slaves = append(slaves, mysqlDriver.New(mysqlDriver.Config{
DSN: dsn,
}))
}
masters := []gorm.Dialector{}
for _, dsn := range o.mastersDsn {
masters = append(masters, mysqlDriver.New(mysqlDriver.Config{
DSN: dsn,
}))
}
return dbresolver.Register(dbresolver.Config{
Sources: masters,
Replicas: slaves,
Policy: dbresolver.RandomPolicy{},
})
}
// Close close gorm db
func Close(db *gorm.DB) error {
return dbclose.Close(db)
}
package mysql
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestInitMysql(t *testing.T) {
dsn := "root:123456@(192.168.3.37:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := Init(dsn, WithEnableTrace())
if err != nil {
// ignore test error about not being able to connect to real mysql
t.Logf(fmt.Sprintf("connect to mysql failed, err=%v, dsn=%s", err, dsn))
return
}
defer Close(db)
t.Logf("%+v", db.Name())
}
func TestInitTidb(t *testing.T) {
dsn := "root:123456@(192.168.3.37:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, err := InitTidb(dsn)
if err != nil {
// ignore test error about not being able to connect to real tidb
t.Logf(fmt.Sprintf("connect to mysql failed, err=%v, dsn=%s", err, dsn))
return
}
defer Close(db)
t.Logf("%+v", db.Name())
}
func Test_gormConfig(t *testing.T) {
o := defaultOptions()
o.apply(
WithLogging(nil),
WithLogging(nil, 4),
WithSlowThreshold(time.Millisecond*100),
WithEnableTrace(),
WithMaxIdleConns(5),
WithMaxOpenConns(50),
WithConnMaxLifetime(time.Minute*3),
WithEnableForeignKey(),
WithLogRequestIDKey("request_id"),
WithRWSeparation([]string{
"root:123456@(192.168.3.37:3306)/slave1",
"root:123456@(192.168.3.37:3306)/slave2"},
"root:123456@(192.168.3.37:3306)/master"),
WithGormPlugin(nil),
)
c := gormConfig(o)
assert.NotNil(t, c)
err := rwSeparationPlugin(o)
assert.NotNil(t, err)
}
package mysql
import (
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Option set the mysql options.
type Option func(*options)
type options struct {
isLog bool
slowThreshold time.Duration
maxIdleConns int
maxOpenConns int
connMaxLifetime time.Duration
disableForeignKey bool
enableTrace bool
preparedStatement bool
requestIDKey string
gLog *zap.Logger
logLevel logger.LogLevel
slavesDsn []string
mastersDsn []string
plugins []gorm.Plugin
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// default settings
func defaultOptions() *options {
return &options{
isLog: false, // whether to output logs, default off
slowThreshold: time.Duration(0), // if greater than 0, only print logs that are longer than the threshold, higher priority than isLog
maxIdleConns: 3, // set the maximum number of connections in the idle connection pool
maxOpenConns: 50, // set the maximum number of open database connections
connMaxLifetime: 30 * time.Minute, // sets the maximum amount of time a connection can be reused
disableForeignKey: true, // disables the use of foreign keys, true is recommended for production environments, enabled by default
enableTrace: false, // whether to enable link tracing, default is off
requestIDKey: "", // request id key
gLog: nil, // custom logger
logLevel: logger.Info, // default logLevel
}
}
// WithLogging set log sql, If l=nil, the gorm log library will be used
func WithLogging(l *zap.Logger, level ...logger.LogLevel) Option {
return func(o *options) {
o.isLog = true
o.gLog = l
if len(level) > 0 {
o.logLevel = level[0]
}
o.logLevel = logger.Info
}
}
// WithSlowThreshold Set sql values greater than the threshold
func WithSlowThreshold(d time.Duration) Option {
return func(o *options) {
o.slowThreshold = d
}
}
// WithMaxIdleConns set max idle conns
func WithMaxIdleConns(size int) Option {
return func(o *options) {
o.maxIdleConns = size
}
}
// WithMaxOpenConns set max open conns
func WithMaxOpenConns(size int) Option {
return func(o *options) {
o.maxOpenConns = size
}
}
// WithConnMaxLifetime set conn max lifetime
func WithConnMaxLifetime(t time.Duration) Option {
return func(o *options) {
o.connMaxLifetime = t
}
}
// WithEnableForeignKey use foreign keys
func WithEnableForeignKey() Option {
return func(o *options) {
o.disableForeignKey = false
}
}
// WithPrepareStatement use prepared statement
func WithPrepareStatement() Option {
return func(o *options) {
o.preparedStatement = true
}
}
// WithEnableTrace use trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithLogRequestIDKey log request id
func WithLogRequestIDKey(key string) Option {
return func(o *options) {
if key == "" {
key = "request_id"
}
o.requestIDKey = key
}
}
// WithRWSeparation setting read-write separation
func WithRWSeparation(slavesDsn []string, mastersDsn ...string) Option {
return func(o *options) {
o.slavesDsn = slavesDsn
o.mastersDsn = mastersDsn
}
}
// WithGormPlugin setting gorm plugin
func WithGormPlugin(plugins ...gorm.Plugin) Option {
return func(o *options) {
o.plugins = plugins
}
}
package postgresql
import (
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Option set the mysql options.
type Option func(*options)
type options struct {
isLog bool
slowThreshold time.Duration
maxIdleConns int
maxOpenConns int
connMaxLifetime time.Duration
disableForeignKey bool
enableTrace bool
requestIDKey string
gLog *zap.Logger
logLevel logger.LogLevel
plugins []gorm.Plugin
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// default settings
func defaultOptions() *options {
return &options{
isLog: false, // whether to output logs, default off
slowThreshold: time.Duration(0), // if greater than 0, only print logs that are longer than the threshold, higher priority than isLog
maxIdleConns: 3, // set the maximum number of connections in the idle connection pool
maxOpenConns: 50, // set the maximum number of open database connections
connMaxLifetime: 30 * time.Minute, // sets the maximum amount of time a connection can be reused
disableForeignKey: true, // disables the use of foreign keys, true is recommended for production environments, enabled by default
enableTrace: false, // whether to enable link tracing, default is off
requestIDKey: "request_id", // request id key
gLog: nil, // custom logger
logLevel: logger.Info, // default logLevel
}
}
// WithLogging set log sql, If l=nil, the gorm log library will be used
func WithLogging(l *zap.Logger, level ...logger.LogLevel) Option {
return func(o *options) {
o.isLog = true
o.gLog = l
if len(level) > 0 {
o.logLevel = level[0]
}
o.logLevel = logger.Info
}
}
// WithSlowThreshold Set sql values greater than the threshold
func WithSlowThreshold(d time.Duration) Option {
return func(o *options) {
o.slowThreshold = d
}
}
// WithMaxIdleConns set max idle conns
func WithMaxIdleConns(size int) Option {
return func(o *options) {
o.maxIdleConns = size
}
}
// WithMaxOpenConns set max open conns
func WithMaxOpenConns(size int) Option {
return func(o *options) {
o.maxOpenConns = size
}
}
// WithConnMaxLifetime set conn max lifetime
func WithConnMaxLifetime(t time.Duration) Option {
return func(o *options) {
o.connMaxLifetime = t
}
}
// WithEnableForeignKey use foreign keys
func WithEnableForeignKey() Option {
return func(o *options) {
o.disableForeignKey = false
}
}
// WithEnableTrace use trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithLogRequestIDKey log request id
func WithLogRequestIDKey(key string) Option {
return func(o *options) {
if key == "" {
key = "request_id"
}
o.requestIDKey = key
}
}
// WithGormPlugin setting gorm plugin
func WithGormPlugin(plugins ...gorm.Plugin) Option {
return func(o *options) {
o.plugins = plugins
}
}
// Package postgresql provides a gorm driver for postgresql.
package postgresql
import (
"fmt"
"log"
"os"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/dbclose"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/glog"
)
// Init postgresql
func Init(dsn string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
db, err := gorm.Open(postgres.Open(dsn), gormConfig(o))
if err != nil {
return nil, err
}
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// gorm setting
func gormConfig(o *options) *gorm.Config {
config := &gorm.Config{
// disable foreign key constraints, not recommended for production environments
DisableForeignKeyConstraintWhenMigrating: o.disableForeignKey,
// removing the plural of an epithet
NamingStrategy: schema.NamingStrategy{SingularTable: true},
}
// print SQL
if o.isLog {
if o.gLog == nil {
config.Logger = logger.Default.LogMode(o.logLevel)
} else {
config.Logger = glog.NewCustomGormLogger(o.gLog, o.requestIDKey, o.logLevel)
}
} else {
config.Logger = logger.Default.LogMode(logger.Silent)
}
// print only slow queries
if o.slowThreshold > 0 {
config.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // use the standard output asWriter
logger.Config{
SlowThreshold: o.slowThreshold,
Colorful: true,
LogLevel: logger.Warn, // set the logging level, only above the specified level will output the slow query log
},
)
}
return config
}
// Close close gorm db
func Close(db *gorm.DB) error {
return dbclose.Close(db)
}
package postgresql
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
dsn := "host=192.168.3.37 user=root password=123456 dbname=account port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := Init(dsn, WithEnableTrace())
if err != nil {
// ignore test error about not being able to connect to real postgresql
t.Logf(fmt.Sprintf("connect to postgresql failed, err=%v, dsn=%s", err, dsn))
return
}
defer Close(db)
t.Logf("%+v", db.Name())
}
func Test_gormConfig(t *testing.T) {
o := defaultOptions()
o.apply(
WithLogging(nil),
WithLogging(nil, 4),
WithSlowThreshold(time.Millisecond*100),
WithEnableTrace(),
WithMaxIdleConns(5),
WithMaxOpenConns(50),
WithConnMaxLifetime(time.Minute*3),
WithEnableForeignKey(),
WithLogRequestIDKey("request_id"),
WithGormPlugin(nil),
)
c := gormConfig(o)
assert.NotNil(t, c)
}
package query
import "strings"
var defaultMaxSize = 1000
// SetMaxSize change the default maximum number of pages per page
func SetMaxSize(max int) {
if max < 10 {
max = 10
}
defaultMaxSize = max
}
// Page info
type Page struct {
pageIndex int // page number, starting from page 0
pageSize int // number per page
sort string // sort fields, default is id backwards, you can add - sign before the field to indicate reverse order, no - sign to indicate ascending order, multiple fields separated by comma
}
// Page get page value
func (p *Page) Page() int {
return p.pageIndex
}
// Limit number per page
func (p *Page) Limit() int {
return p.pageSize
}
// Size number per page
func (p *Page) Size() int {
return p.pageSize
}
// Sort get sort field
func (p *Page) Sort() string {
return p.sort
}
// Offset get offset value
func (p *Page) Offset() int {
return p.pageIndex * p.pageSize
}
// DefaultPage default page, number 20 per page, sorted by id backwards
func DefaultPage(page int) *Page {
if page < 0 {
page = 0
}
return &Page{
pageIndex: page,
pageSize: 20,
sort: "id DESC",
}
}
// NewPage custom page, starting from page 0.
// the parameter columnNames indicates a sort field, if empty means id descending,
// if there are multiple column names, separated by a comma,
// a '-' sign in front of each column name indicates descending order, otherwise ascending order.
func NewPage(pageIndex int, pageSize int, columnNames string) *Page {
if pageIndex < 0 {
pageIndex = 1
}
if pageSize > defaultMaxSize || pageSize < 1 {
pageSize = defaultMaxSize
}
return &Page{
pageIndex: pageIndex - 1,
pageSize: pageSize,
sort: getSort(columnNames),
}
}
// convert to mysql sort, each column name preceded by a '-' sign, indicating descending order, otherwise ascending order, example:
//
// columnNames="name" means sort by name in ascending order,
// columnNames="-name" means sort by name descending,
// columnNames="name,age" means sort by name in ascending order, otherwise sort by age in ascending order,
// columnNames="-name,-age" means sort by name descending before sorting by age descending.
func getSort(columnNames string) string {
columnNames = strings.TrimSpace(columnNames)
if columnNames == "" {
return "id DESC"
}
names := strings.Split(columnNames, ",")
strs := make([]string, 0, len(names))
for _, name := range names {
if name[0] == '-' && len(name) > 1 {
strs = append(strs, name[1:]+" DESC")
} else {
strs = append(strs, name+" ASC")
}
}
return strings.Join(strs, ", ")
}
// Package query is a library of custom condition queries, support for complex conditional paging queries.
package query
import (
"fmt"
"strings"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"gitlab.wanzhuangkj.com/tush/opkg/xutils/xtime"
)
const (
// Eq equal
Eq = "eq"
// Neq not equal
Neq = "neq"
// Gt greater than
Gt = "gt"
// Gte greater than or equal
Gte = "gte"
// Lt less than
Lt = "lt"
// Lte less than or equal
Lte = "lte"
// Like fuzzy lookup
Like = "like"
// In include
In = "in"
// AND logic and
AND string = "and"
// OR logic or
OR string = "or"
)
var expMap = map[string]string{
Eq: " = ",
Neq: " <> ",
Gt: " > ",
Gte: " >= ",
Lt: " < ",
Lte: " <= ",
Like: " LIKE ",
In: " IN ",
"=": " = ",
"!=": " <> ",
">": " > ",
">=": " >= ",
"<": " < ",
"<=": " <= ",
}
var logicMap = map[string]string{
AND: " AND ",
OR: " OR ",
"&": " AND ",
"&&": " AND ",
"|": " OR ",
"||": " OR ",
"AND": " AND ",
"OR": " OR ",
}
type IPagination interface {
GetOffset() int
GetLimit() int
GetPageIndex() int
GetPageSize() int
}
type Pagination struct {
PageIndex int `json:"pageIndex" form:"pageIndex" binding:"" example:"1"` // 1-based
PageSize int `json:"pageSize" form:"pageSize" binding:"" example:"10"` // 1-based
Sort string `json:"sort,omitempty" form:"sort" binding:"" example:"-id" query:"type:order"` //排序字段,负号表示降序,多个字段排序中间用,(英文逗号)分隔
Unscope bool `json:"-" form:"-" query:"-"`
}
func (x Pagination) Unscoped() bool {
return x.Unscope
}
type CreatedAtPeriod struct {
CreatedAtBegin xtime.DateTime `json:"createdAtBegin" form:"createdAtBegin" gorm:"column:created_at;type:datetime;comment:创建时间" query:"type:gte;column:created_at" example:"2025-01-01 12:20:26"` //开始日期起始 (>=)
CreatedAtEnd xtime.DateTime `json:"createdAtEnd" form:"createdAtEnd" gorm:"column:created_at;type:datetime;comment:创建时间" query:"type:lt;column:created_at" example:"2025-02-01 12:20:26"` //结束日期截止 (<)
}
func (x Pagination) GetOffset() int {
if x.PageIndex <= 0 {
x.PageIndex = 1
}
return (x.PageIndex - 1) * x.GetPageSize()
}
func (x Pagination) GetPageSize() int {
if x.PageSize <= 0 {
x.PageSize = 10
}
return x.PageSize
}
func (x Pagination) GetPageIndex() int {
if x.PageIndex <= 0 {
x.PageIndex = 1
}
return x.PageIndex
}
func (x Pagination) GetLimit() int {
return x.GetPageSize()
}
func (x Pagination) GetSort() string {
if x.Sort == "" {
x.Sort = "id desc"
}
return x.Sort
}
// Params query parameters
type Params struct {
PageIndex int `json:"pageIndex" form:"pageIndex" binding:"gte=1" validate:"required" example:"1"` // 1-based
PageSize int `json:"pageSize" form:"pageSize" binding:"gte=1" validate:"required" example:"10"` // 1-based
Sort string `json:"sort,omitempty" form:"sort" binding:"" validate:"required" example:"id"`
Columns []Column `json:"columns,omitempty" form:"columns"` // not required
}
// Column query info
type Column struct {
Name string `json:"name" form:"name" validate:"required" example:"id"` // column name eg id
Exp string `json:"exp" form:"exp" validate:"required" example:"="` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in
Value interface{} `json:"value" form:"value" validate:"required"` // column value eg 102
Logic string `json:"logic" form:"logic" example:"and"` // logical type, defaults to and when the value is null, with &(and), ||(or)
}
func (c *Column) checkValid() error {
if c.Name == "" {
return xerror.New("field 'name' cannot be empty")
}
if c.Value == nil {
return xerror.New("field 'value' cannot be nil")
}
return nil
}
// converting ExpType to sql expressions and LogicType to sql using characters
func (c *Column) convert() error {
if c.Exp == "" {
c.Exp = Eq
}
if v, ok := expMap[strings.ToLower(c.Exp)]; ok { //nolint
c.Exp = v
if c.Exp == " LIKE " {
c.Value = fmt.Sprintf("%%%v%%", c.Value)
}
if c.Exp == " IN " {
val, ok := c.Value.(string)
if !ok {
return xerror.Newf("invalid value type '%s'", c.Value)
}
iVal := []interface{}{}
ss := strings.Split(val, ",")
for _, s := range ss {
iVal = append(iVal, s)
}
c.Value = iVal
}
} else {
return xerror.Newf("unknown exp type '%s'", c.Exp)
}
if c.Logic == "" {
c.Logic = AND
}
if v, ok := logicMap[strings.ToLower(c.Logic)]; ok { //nolint
c.Logic = v
} else {
return xerror.Newf("unknown logic type '%s'", c.Logic)
}
return nil
}
// ConvertToPage converted to page
func (p *Params) ConvertToPage() (order string, limit int, offset int) { //nolint
page := NewPage(p.PageIndex, p.PageSize, p.Sort)
order = page.sort
limit = page.pageSize
offset = page.pageIndex * page.pageSize
return //nolint
}
// ConvertToGormConditions conversion to gorm-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (p *Params) ConvertToGormConditions() (string, []interface{}, error) {
str := ""
args := []interface{}{}
l := len(p.Columns)
if l == 0 {
return "", nil, nil
}
isUseIN := true
if l == 1 {
isUseIN = false
}
field := p.Columns[0].Name
for i, column := range p.Columns {
if err := column.checkValid(); err != nil {
return "", nil, err
}
err := column.convert()
if err != nil {
return "", nil, err
}
symbol := "?"
if column.Exp == " IN " {
symbol = "(?)"
}
if i == l-1 { // ignore the logical type of the last column
str += column.Name + column.Exp + symbol
} else {
str += column.Name + column.Exp + symbol + column.Logic
}
args = append(args, column.Value)
// when multiple columns are the same, determine whether the use of IN
if isUseIN {
if field != column.Name {
isUseIN = false
continue
}
if column.Exp != expMap[Eq] {
isUseIN = false
}
}
}
if isUseIN {
str = field + " IN (?)"
args = []interface{}{args}
}
return str, args, nil
}
// Conditions query conditions
type Conditions struct {
Columns []Column `json:"columns" form:"columns" binding:"min=1"` // columns info
Sort string `json:"sort,omitempty" form:"sort" binding:"" validate:"required" example:"id"`
}
// CheckValid check valid
func (c *Conditions) CheckValid() error {
if len(c.Columns) == 0 {
return xerror.New("field 'columns' cannot be empty")
}
for _, column := range c.Columns {
err := column.checkValid()
if err != nil {
return err
}
if column.Exp != "" {
if _, ok := expMap[column.Exp]; !ok {
return xerror.Newf("unknown exp type '%s'", column.Exp)
}
}
if column.Logic != "" {
if _, ok := logicMap[column.Logic]; !ok {
return xerror.Newf("unknown logic type '%s'", column.Logic)
}
}
}
return nil
}
// ConvertToGorm conversion to gorm-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (c *Conditions) ConvertToGorm() (string, []interface{}, error) {
p := &Params{Columns: c.Columns}
return p.ConvertToGormConditions()
}
package query
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPage(t *testing.T) {
page := DefaultPage(-1)
t.Log(page.Page(), page.Limit(), page.Sort(), page.Offset())
SetMaxSize(1)
page = NewPage(-1, 100, "id")
t.Log(page.Page(), page.Limit(), page.Sort(), page.Offset())
}
func TestParams_ConvertToPage(t *testing.T) {
p := &Params{
PageIndex: 1,
PageSize: 50,
Sort: "age,-name",
}
order, limit, offset := p.ConvertToPage()
t.Logf("order=%s, limit=%d, offset=%d", order, limit, offset)
}
func TestParams_ConvertToGormConditions(t *testing.T) {
type args struct {
columns []Column
}
tests := []struct {
name string
args args
want string
want1 []interface{}
wantErr bool
}{
// --------------------------- only 1 column query ------------------------------
{
name: "1 column eq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
},
},
want: "name = ?",
want1: []interface{}{"ZhangSan"},
wantErr: false,
},
{
name: "1 column neq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
//Exp: "neq",
Exp: "!=",
},
},
},
want: "name <> ?",
want1: []interface{}{"ZhangSan"},
wantErr: false,
},
{
name: "1 column gt",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Gt,
Exp: ">",
},
},
},
want: "age > ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column gte",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Gte,
Exp: ">=",
},
},
},
want: "age >= ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column lt",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Lt,
Exp: "<",
},
},
},
want: "age < ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column lte",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "age <= ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column like",
args: args{
columns: []Column{
{
Name: "name",
Value: "Li",
Exp: Like,
},
},
},
want: "name LIKE ?",
want1: []interface{}{"%Li%"},
wantErr: false,
},
{
name: "1 column IN",
args: args{
columns: []Column{
{
Name: "name",
Value: "ab,cd,ef",
Exp: In,
},
},
},
want: "name IN (?)",
want1: []interface{}{[]interface{}{"ab", "cd", "ef"}},
wantErr: false,
},
// --------------------------- query 2 columns ------------------------------
{
name: "2 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
},
},
want: "name = ? AND gender = ?",
want1: []interface{}{"ZhangSan", "male"},
wantErr: false,
},
{
name: "2 columns neq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
//Exp: Neq,
Exp: "!=",
},
{
Name: "name",
Value: "LiSi",
//Exp: Neq,
Exp: "!=",
},
},
},
want: "name <> ? AND name <> ?",
want1: []interface{}{"ZhangSan", "LiSi"},
wantErr: false,
},
{
name: "2 columns gt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Value: 20,
//Exp: Gt,
Exp: ">",
},
},
},
want: "gender = ? AND age > ?",
want1: []interface{}{"male", 20},
wantErr: false,
},
{
name: "2 columns gte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Value: 20,
//Exp: Gte,
Exp: ">=",
},
},
},
want: "gender = ? AND age >= ?",
want1: []interface{}{"male", 20},
wantErr: false,
},
{
name: "2 columns lt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Value: 20,
//Exp: Lt,
Exp: "<",
},
},
},
want: "gender = ? AND age < ?",
want1: []interface{}{"female", 20},
wantErr: false,
},
{
name: "2 columns lte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "gender = ? AND age <= ?",
want1: []interface{}{"female", 20},
wantErr: false,
},
{
name: "2 columns range and",
args: args{
columns: []Column{
{
Name: "age",
Value: 10,
//Exp: Gte,
Exp: ">=",
},
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "age >= ? AND age <= ?",
want1: []interface{}{10, 20},
wantErr: false,
},
{
name: "2 columns eq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
//Logic: OR,
Logic: "||",
},
{
Name: "gender",
Value: "female",
},
},
},
want: "name = ? OR gender = ?",
want1: []interface{}{"LiSi", "female"},
wantErr: false,
},
{
name: "2 columns neq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
//Logic: OR,
Logic: "||",
},
{
Name: "gender",
Value: "male",
//Exp: Neq,
Exp: "!=",
},
},
},
want: "name = ? OR gender <> ?",
want1: []interface{}{"LiSi", "male"},
wantErr: false,
},
{
name: "2 columns eq and in",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
//Logic: "&",
},
{
Name: "name",
Value: "LiSi,ZhangSan,WangWu",
Exp: In,
},
},
},
want: "gender = ? AND name IN (?)",
want1: []interface{}{"male", []interface{}{"LiSi", "ZhangSan", "WangWu"}},
wantErr: false,
},
// ------------------------------ IN -------------------------------------------------
{
name: "3 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
},
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "name",
Value: "WangWu",
},
},
},
want: "name IN (?)",
want1: []interface{}{[]interface{}{"LiSi", "ZhangSan", "WangWu"}},
wantErr: false,
},
// ---------------------------- error ----------------------------------------------
{
name: "exp type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Exp: "xxxxxx",
},
},
},
want: "",
want1: nil,
wantErr: true,
},
{
name: "logic type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Logic: "xxxxxx",
},
},
},
want: "",
want1: nil,
wantErr: true,
},
{
name: "empty",
args: args{
columns: nil,
},
want: "",
want1: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := &Params{
Columns: tt.args.columns,
}
got, got1, err := params.ConvertToGormConditions()
if (err != nil) != tt.wantErr {
t.Errorf("ConvertToGormConditions() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ConvertToGormConditions() got = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("ConvertToGormConditions() got1 = %v, want %v", got1, tt.want1)
}
got = strings.Replace(got, "?", "%v", -1)
t.Logf(got, got1...)
})
}
}
func TestConditions_ConvertToGorm(t *testing.T) {
c := Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
str, values, err := c.ConvertToGorm()
if err != nil {
t.Error(err)
}
assert.Equal(t, "name = ? AND gender = ?", str)
assert.Equal(t, len(values), 2)
}
func TestConditions_checkValid(t *testing.T) {
// empty error
c := Conditions{}
err := c.CheckValid()
assert.Error(t, err)
// value is empty error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: nil,
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// exp error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Exp: "unknown-exp",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// logic error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Logic: "unknown-logic",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// success
c = Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
err = c.CheckValid()
assert.NoError(t, err)
}
package sqlite
import (
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Option set the mysql options.
type Option func(*options)
type options struct {
isLog bool
slowThreshold time.Duration
maxIdleConns int
maxOpenConns int
connMaxLifetime time.Duration
disableForeignKey bool
enableTrace bool
requestIDKey string
gLog *zap.Logger
logLevel logger.LogLevel
plugins []gorm.Plugin
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// default settings
func defaultOptions() *options {
return &options{
isLog: false, // whether to output logs, default off
slowThreshold: time.Duration(0), // if greater than 0, only print logs that are longer than the threshold, higher priority than isLog
maxIdleConns: 3, // set the maximum number of connections in the idle connection pool
maxOpenConns: 50, // set the maximum number of open database connections
connMaxLifetime: 30 * time.Minute, // sets the maximum amount of time a connection can be reused
disableForeignKey: true, // disables the use of foreign keys, true is recommended for production environments, enabled by default
enableTrace: false, // whether to enable link tracing, default is off
requestIDKey: "", // request id key
gLog: nil, // custom logger
logLevel: logger.Info, // default logLevel
}
}
// WithLogging set log sql, If l=nil, the gorm log library will be used
func WithLogging(l *zap.Logger, level ...logger.LogLevel) Option {
return func(o *options) {
o.isLog = true
o.gLog = l
if len(level) > 0 {
o.logLevel = level[0]
}
o.logLevel = logger.Info
}
}
// WithSlowThreshold Set sql values greater than the threshold
func WithSlowThreshold(d time.Duration) Option {
return func(o *options) {
o.slowThreshold = d
}
}
// WithMaxIdleConns set max idle conns
func WithMaxIdleConns(size int) Option {
return func(o *options) {
o.maxIdleConns = size
}
}
// WithMaxOpenConns set max open conns
func WithMaxOpenConns(size int) Option {
return func(o *options) {
o.maxOpenConns = size
}
}
// WithConnMaxLifetime set conn max lifetime
func WithConnMaxLifetime(t time.Duration) Option {
return func(o *options) {
o.connMaxLifetime = t
}
}
// WithEnableForeignKey use foreign keys
func WithEnableForeignKey() Option {
return func(o *options) {
o.disableForeignKey = false
}
}
// WithEnableTrace use trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithLogRequestIDKey log request id
func WithLogRequestIDKey(key string) Option {
return func(o *options) {
if key == "" {
key = "request_id"
}
o.requestIDKey = key
}
}
// WithGormPlugin setting gorm plugin
func WithGormPlugin(plugins ...gorm.Plugin) Option {
return func(o *options) {
o.plugins = plugins
}
}
// Package sqlite provides a gorm driver for sqlite.
package sqlite
import (
"fmt"
"log"
"os"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/dbclose"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/glog"
)
// Init sqlite
func Init(dbFile string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
dsn := fmt.Sprintf("%s?_journal=WAL&_vacuum=incremental", dbFile)
db, err := gorm.Open(sqlite.Open(dsn), gormConfig(o))
if err != nil {
return nil, err
}
db.Set("gorm:auto_increment", true)
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// gorm setting
func gormConfig(o *options) *gorm.Config {
config := &gorm.Config{
// disable foreign key constraints, not recommended for production environments
DisableForeignKeyConstraintWhenMigrating: o.disableForeignKey,
// removing the plural of an epithet
NamingStrategy: schema.NamingStrategy{SingularTable: true},
}
// print SQL
if o.isLog {
if o.gLog == nil {
config.Logger = logger.Default.LogMode(o.logLevel)
} else {
config.Logger = glog.NewCustomGormLogger(o.gLog, o.requestIDKey, o.logLevel)
}
} else {
config.Logger = logger.Default.LogMode(logger.Silent)
}
// print only slow queries
if o.slowThreshold > 0 {
config.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // use the standard output asWriter
logger.Config{
SlowThreshold: o.slowThreshold,
Colorful: true,
LogLevel: logger.Warn, // set the logging level, only above the specified level will output the slow query log
},
)
}
return config
}
// Close close gorm db
func Close(db *gorm.DB) error {
return dbclose.Close(db)
}
package sqlite
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
dbFile := "test_sqlite.db"
db, err := Init(dbFile)
if err != nil {
// ignore test error about not being able to connect to real sqlite
t.Logf(fmt.Sprintf("connect to sqlite failed, err=%v, dbFile=%s", err, dbFile))
return
}
defer Close(db)
t.Logf("%+v", db.Name())
}
func Test_gormConfig(t *testing.T) {
o := defaultOptions()
o.apply(
WithLogging(nil),
WithLogging(nil, 4),
WithSlowThreshold(time.Millisecond*100),
WithEnableTrace(),
WithMaxIdleConns(5),
WithMaxOpenConns(50),
WithConnMaxLifetime(time.Minute*3),
WithEnableForeignKey(),
WithLogRequestIDKey("request_id"),
WithGormPlugin(nil),
)
c := gormConfig(o)
assert.NotNil(t, c)
}
package xcode
// Code 通用错误代码接口定义。
type Code interface {
// Code 错误码。
Code() int
// Message 错误码简短信息。
Message() string
// Detail 错误码详细信息。
Detail() any
}
// ================================================================================================================
// 公共错误码定义。
// 保留内部错误码: code < 1000。
// ================================================================================================================
var (
CodeDefault = 0
CodeOK = 200
CodeValidationFailed = 501
CodeInvalidParameter = 503
CodeMissingConfiguration = 507
CodeNotAuthorized = 401
CodeUnknown = 604
)
package xcode
import "fmt"
// localCode 内部错误码实现。
type localCode struct {
code int // Error code, usually an integer.
message string // Brief message for this error code.
detail any // As type of interface, it is mainly designed as an extension field for error code.
}
// Code 错误码。
func (c localCode) Code() int {
return c.code
}
// Message 错误码简短信息。
func (c localCode) Message() string {
return c.message
}
// Detail 错误码详细信息。
func (c localCode) Detail() any {
return c.detail
}
// String 错误码字符串。
func (c localCode) String() string {
if c.detail != nil {
return fmt.Sprintf(`%d:%s %v`, c.code, c.message, c.detail)
}
if c.message != "" {
return fmt.Sprintf(`%d:%s`, c.code, c.message)
}
return fmt.Sprintf(`%d`, c.code)
}
package xerror
// IIs Is 接口。
type IIs interface {
Error() string
Is(target error) bool
}
// IEqual Equal 接口。
type IEqual interface {
Error() string
Equal(target error) bool
}
// ICode Code 接口。
type ICode interface {
Error() string
Code() int
}
// IStack Stack 接口。
type IStack interface {
Error() string
Stack() string
}
// ICause Cause 接口。
type ICause interface {
Error() string
Cause() error
}
// ICurrent Current 接口。
type ICurrent interface {
Error() string
Current() error
}
// IUnwrap Unwrap 接口。
type IUnwrap interface {
Error() string
Unwrap() error
}
const (
// stackFilterKeyForX 过滤 G 模块路径堆栈。
stackFilterKeyForX = "xmall/common/intern/"
// separatorSpace 空间分隔符。
separatorSpace = ", "
)
var (
// IsUsingBriefStack 简短错误堆栈的开关。(默认为true)
IsUsingBriefStack bool
)
func init() {
IsUsingBriefStack = true
}
package xerror
import (
"fmt"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
)
// New 用于创建一个自定义文本错误信息的 error 对象,并包含堆栈信息。
func New(text string) error {
return &Error{
stack: callers(),
text: text,
code: xcode.CodeDefault,
}
}
// NewC add code
func NewC(code int, text string) error {
return &Error{
stack: callers(),
text: text,
code: code,
}
}
func Join(errs ...error) error {
if len(errs) == 0 {
return nil
}
var es []error
for _, e := range errs {
if e != nil {
es = append(es, e)
}
}
if len(es) == 0 {
return nil
}
if len(es) == 1 {
return es[0]
}
err := es[0]
for i := 1; i < len(es); i++ {
err = Wrap(err, es[i].Error())
}
return err
}
// Newf 用于创建一个自定义文本错误带参数信息的 error 对象,并包含堆栈信息。
func Newf(format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
code: xcode.CodeDefault,
}
}
func Errorf(format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
code: xcode.CodeDefault,
}
}
func NewCf(code int, format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// NewSkip 用于创建一个自定义错误信息的 error 对象,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。高级功能,一般开发者很少用得到。
// 参数 `skip` 指定堆栈跳过的层数。
func NewSkip(skip int, text string) error {
return &Error{
stack: callers(skip),
text: text,
code: xcode.CodeDefault,
}
}
func NewSkipC(code, skip int, text string) error {
return &Error{
stack: callers(skip),
text: text,
code: code,
}
}
// NewSkipf 用于创建一个自定义错误信息的error对象,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
// 参数 `skip` 指定堆栈跳过的层数。
func NewSkipf(skip int, format string, args ...any) error {
return &Error{
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: xcode.CodeDefault,
}
}
func NewSkipCf(code, skip int, format string, args ...any) error {
return &Error{
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// Wrap 用于包裹其他错误 error 对象,构造成多级的错误信息,包含堆栈信息。
// 注:它不会丢失包装错误的错误码,因为它从中继承了错误码。
func Wrap(err error, text string) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(),
text: text,
code: Code(err),
}
}
// Wrapf 用于包裹其他错误 error 对象,构造成多级的错误信息,包含堆栈信息。
func Wrapf(err error, format string, args ...any) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(),
text: fmt.Sprintf(format, args...),
code: Code(err),
}
}
// WrapSkip 用于包裹其他错误 error 对象,构造成多级的错误信息,包含堆栈信息,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func WrapSkip(skip int, err error, text string) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(skip),
text: text,
code: Code(err),
}
}
// WrapSkipf 用于包裹其他错误 error 对象,构造成多级的错误信息,包含堆栈信息,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func WrapSkipf(skip int, err error, format string, args ...any) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: Code(err),
}
}
type CodeI interface {
Code() int
}
func GetCodeOrDefault(err error, defaultCode int) int {
if err == nil {
return defaultCode
}
if code, ok := err.(CodeI); ok {
return code.Code()
}
return defaultCode
}
package xerror
import (
"fmt"
"strings"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
)
// NewCode 用于创建一个自定义错误信息的 error 对象,并包含堆栈信息,并增加错误码对象的输入。
func NewCode(code int, text ...string) error {
return &Error{
stack: callers(),
text: strings.Join(text, separatorSpace),
code: code,
}
}
// NewCodef 用于创建一个自定义错误信息的 error 对象,并包含堆栈信息,并增加错误码对象的输入。
func NewCodef(code int, format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// NewCodeSkip 用于创建一个自定义错误信息的 error 对象,并包含堆栈信息,并增加错误码对象的输入。并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func NewCodeSkip(code int, skip int, text ...string) error {
return &Error{
stack: callers(skip),
text: strings.Join(text, separatorSpace),
code: code,
}
}
// NewCodeSkipf 用于创建一个自定义错误信息的 error 对象,并包含堆栈信息,并增加错误码对象的输入。并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func NewCodeSkipf(code int, skip int, format string, args ...any) error {
return &Error{
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// WrapCode 用 error 和文本包裹错误。
func WrapCode(code int, err error, text ...string) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(),
text: strings.Join(text, separatorSpace),
code: code,
}
}
// WrapCodef 用 error 和格式指定符包裹错误。
func WrapCodef(code int, err error, format string, args ...any) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// WrapCodeSkip 用 error 和文本包裹错误,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func WrapCodeSkip(code int, skip int, err error, text ...string) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(skip),
text: strings.Join(text, separatorSpace),
code: code,
}
}
// WrapCodeSkipf 用 error 和格式指定符包裹错误,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。
func WrapCodeSkipf(code int, skip int, err error, format string, args ...any) error {
if err == nil {
return nil
}
return &Error{
error: err,
stack: callers(skip),
text: fmt.Sprintf(format, args...),
code: code,
}
}
// Code 获取 error 中的错误码接口。
func Code(err error) int {
if err == nil {
return xcode.CodeDefault
}
if e, ok := err.(ICode); ok {
return e.Code()
}
if e, ok := err.(IUnwrap); ok {
return Code(e.Unwrap())
}
return xcode.CodeDefault
}
// HasCode 检查并报告 `err` 在其链接错误中是否具有 `code`。
func HasCode(err error, code int) bool {
if err == nil {
return false
}
if e, ok := err.(ICode); ok {
return code == e.Code()
}
if e, ok := err.(IUnwrap); ok {
return HasCode(e.Unwrap(), code)
}
return false
}
package xerror
// Option 自定义的错误创建。
type Option struct {
Error error // 包装错误(如果有)。
Stack bool // 是否将堆栈信息记录到错误中。
Text string // 错误文本,由 New* 函数创建
Code int // 如有必要,错误码。
}
// NewOption 用于自定义配置的错误对象创建。
func NewOption(option Option) error {
err := &Error{
error: option.Error,
text: option.Text,
code: option.Code,
}
if option.Stack {
err.stack = callers()
}
return err
}
package xerror
import (
"runtime"
"github.com/pkg/errors"
)
// stack 代表堆栈程序计数器。
type stack []uintptr
const (
// maxStackDepth 标记最大堆栈深度的错误后轨迹。
maxStackDepth = 64
// maxStackDepth = 10
)
// Cause 获取根错误 error。
// 如果 `err` 是 nil, 则返回 nil。
func Cause(err error) error {
if err == nil {
return nil
}
if e, ok := err.(ICause); ok {
return e.Cause()
}
if e, ok := err.(IUnwrap); ok {
return Cause(e.Unwrap())
}
return err
}
// Stack 获取堆栈信息。
// 如果 `err` 是 nil, 则返回空字符串。
// 如果 `err` 不支持堆栈,它将直接返回错误字符串。
func Stack(err error) string {
if err == nil {
return ""
}
if e, ok := err.(IStack); ok {
return e.Stack()
}
return err.Error()
}
// Current 获取当前 error。
// 如果当前错误是 nil, 则返回 nil。
func Current(err error) error {
if err == nil {
return nil
}
if e, ok := err.(ICurrent); ok {
return e.Current()
}
return err
}
// Unwrap 获取下一层 error。
// 如果当前级别错误或下一个级别错误为 nil,则返回 nil。
func Unwrap(err error) error {
if err == nil {
return nil
}
if e, ok := err.(IUnwrap); ok {
return e.Unwrap()
}
return nil
}
// HasStack 判断错误是否带堆栈,实现 `xerror.IStack` 接口。
func HasStack(err error) bool {
_, ok := err.(IStack)
return ok
}
func Equal(err, target error) bool {
if err == target {
return true
}
if err.Error() == target.Error() {
return true
}
if e, ok := err.(IEqual); ok {
return e.Equal(target)
}
if e, ok := target.(IEqual); ok {
return e.Equal(err)
}
return Is(err, target)
}
func IsNot(err, target error) bool {
return !Is(err, target)
}
func Is(err, target error) bool {
if err == nil && target == nil {
return true
}
if err != nil && target == nil {
return false
}
if err == nil && target != nil {
return false
}
if err.Error() == target.Error() {
return true
}
if errors.Is(err, target) {
return true
}
if e, ok := err.(IIs); ok {
return e.Is(target)
}
return false
}
func HasError(err, target error) bool {
return Is(err, target)
}
func callers(skip ...int) stack {
var (
pcs [maxStackDepth]uintptr
n = 3
)
if len(skip) > 0 {
n += skip[0]
}
return pcs[:runtime.Callers(n, pcs[:])]
}
package xerror
import (
"fmt"
"runtime"
"strings"
"github.com/pkg/errors"
)
// Error 自定义错误对象。
type Error struct {
error error // 包装错误。
stack stack // 堆栈数组,当创建或包装此错误时记录堆栈信息。
text string // 创建错误时自定义错误文本。
code int // 如有必要,错误码。
}
const (
// stackFilterKeyLocal 过滤当前错误模块路径的键。
stackFilterKeyLocal = "/xerrors/xerror"
)
var (
// goRootForFilter 用于开发环境中的堆栈过滤。
goRootForFilter = runtime.GOROOT()
)
func init() {
if goRootForFilter != "" {
goRootForFilter = strings.ReplaceAll(goRootForFilter, "\\", "/")
}
}
// Error 实现错误的接口,它将所有错误返回为字符串。
func (err *Error) Error() string {
if err == nil {
return ""
}
errStr := err.text
if err.error != nil {
if errStr != "" {
errStr += ": "
}
errStr += err.error.Error()
}
return errStr
}
// Cause 获取根错误 error。
func (err *Error) Cause() error {
if err == nil {
return nil
}
loop := err
for loop != nil {
if loop.error != nil {
if e, ok := loop.error.(*Error); ok {
// 内部自定义错误。
loop = e
} else if e, ok := loop.error.(ICause); ok {
// 实现 ApiCause 接口的其他错误。
return e.Cause()
} else {
return loop.error
}
} else {
// return loop
//
// 参考案例 https://github.com/pkg/errors。
return errors.New(loop.text)
}
}
return nil
}
// Current 获取当前 error。
// 如果当前错误是 nil, 则返回 nil。
func (err *Error) Current() error {
if err == nil {
return nil
}
return &Error{
error: nil,
stack: err.stack,
text: err.text,
code: err.code,
}
}
// Unwrap 获取下一层 error。
func (err *Error) Unwrap() error {
if err == nil {
return nil
}
return err.error
}
// Equal 错误对象比较。
// 如果它们的 `code` 和 `text` 都相同,则认为错误相同。
func (err *Error) Equal(target error) bool {
if err.Error() == target.Error() {
return true
}
if errors.Is(target, err) {
return true
}
if err.text != fmt.Sprintf(`%-s`, target) {
return false
}
return true
}
// Is 当前错误 `err` 的链接错误中是否包含错误 `target`。
func (err *Error) Is(target error) bool {
if Equal(err, target) {
return true
}
nextErr := err.Unwrap()
if nextErr == nil {
return false
}
if Equal(nextErr, target) {
return true
}
if e, ok := nextErr.(IIs); ok {
return e.Is(target)
}
return false
}
package xerror
import "gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
// Code 获取错误码。
// 如果没有错误代码,则返回 `xcode.CodeDefault`。
func (err *Error) Code() int {
if err == nil {
return xcode.CodeDefault
}
if err.code == xcode.CodeDefault {
return Code(err.Unwrap())
}
return err.code
}
// SetCode 使用指定 `code` 更新内部 `code` 。
func (err *Error) SetCode(code int) {
if err == nil {
return
}
err.code = code
}
package xerror
import (
"fmt"
"io"
)
// Format 根据 `fmt.Formatter` 接口格式化。
//
// %v, %s : 打印所有错误字符串;
// %-v, %-s : 打印当前级别错误字符串;
// %+s : 打印完整堆栈错误列表;
// %+v : 打印错误字符串和完整堆栈错误列表
func (err *Error) Format(s fmt.State, verb rune) {
switch verb {
case 's', 'v':
switch {
case s.Flag('-'):
if err.text != "" {
_, _ = io.WriteString(s, err.text)
} else {
_, _ = io.WriteString(s, err.Error())
}
case s.Flag('+'):
if verb == 's' {
_, _ = io.WriteString(s, err.Stack())
} else {
_, _ = io.WriteString(s, err.Error()+"\n"+err.Stack())
}
default:
_, _ = io.WriteString(s, err.Error())
}
}
}
package xerror
// MarshalJSON 实现 json.Marshal 接口。
// 注:这里不要使用指针作为其接收器。
func (err Error) MarshalJSON() ([]byte, error) {
return []byte(`"` + err.Error() + `"`), nil
}
package xerror
import (
"bytes"
"container/list"
"fmt"
"runtime"
"strings"
)
// stackInfo 管理特定错误的堆栈信息。
type stackInfo struct {
Index int // Index 整个错误堆栈中当前错误的索引。
Message string // Error 信息字符串。
Lines *list.List // Lines 按顺序包含当前错误堆栈的所有错误堆栈行。
}
// stackLine 管理堆栈的每一行信息。
type stackLine struct {
Function string // Function 包含其完整包路径。
FileLine string // FileLine 函数的源文件名及其行号。
}
// Stack 以字符串形式返回错误堆栈信息。
func (err *Error) Stack() string {
if err == nil {
return ""
}
var (
loop = err
index = 1
infos []*stackInfo
)
for loop != nil {
info := &stackInfo{
Index: index,
Message: fmt.Sprintf("%-v", loop),
}
index++
infos = append(infos, info)
loopLinesOfStackInfo(loop.stack, info)
if loop.error != nil {
if e, ok := loop.error.(*Error); ok {
loop = e
} else {
infos = append(infos, &stackInfo{
Index: index,
Message: loop.error.Error(),
})
index++
break
}
} else {
break
}
}
filterLinesOfStackInfos(infos)
return formatStackInfos(infos)
}
// filterLinesOfStackInfos 从顶部错误中删除存在于后续堆栈中的重复行。
func filterLinesOfStackInfos(infos []*stackInfo) {
var (
ok bool
set = make(map[string]struct{})
info *stackInfo
line *stackLine
removes []*list.Element
)
for i := len(infos) - 1; i >= 0; i-- {
info = infos[i]
if info.Lines == nil {
continue
}
for n, e := 0, info.Lines.Front(); n < info.Lines.Len(); n, e = n+1, e.Next() {
line = e.Value.(*stackLine)
if _, ok = set[line.FileLine]; ok {
removes = append(removes, e)
} else {
set[line.FileLine] = struct{}{}
}
}
if len(removes) > 0 {
for _, e := range removes {
info.Lines.Remove(e)
}
}
removes = removes[:0]
}
}
// formatStackInfos 格式化并以字符串形式返回错误堆栈信息。
func formatStackInfos(infos []*stackInfo) string {
var buffer = bytes.NewBuffer(nil)
for i, info := range infos {
buffer.WriteString(fmt.Sprintf("%d. %s\n", i+1, info.Message))
if info.Lines != nil && info.Lines.Len() > 0 {
formatStackLines(buffer, info.Lines)
}
}
return buffer.String()
}
// formatStackLines 格式化并将错误堆栈行作为字符串返回。
func formatStackLines(buffer *bytes.Buffer, lines *list.List) string {
var (
line *stackLine
space = " "
length = lines.Len()
)
for i, e := 0, lines.Front(); i < length; i, e = i+1, e.Next() {
line = e.Value.(*stackLine)
// Graceful indent.
if i >= 9 {
space = " "
}
buffer.WriteString(fmt.Sprintf(
" %d. %s%s\n %s\n",
i+1, space, line.Function, line.FileLine,
))
}
return buffer.String()
}
// loopLinesOfStackInfo 迭代堆栈信息行并生成堆栈行信息。
func loopLinesOfStackInfo(st stack, info *stackInfo) {
if st == nil {
return
}
for _, p := range st {
if fn := runtime.FuncForPC(p - 1); fn != nil {
file, line := fn.FileLine(p - 1)
if IsUsingBriefStack {
// 过滤整个 X 包堆栈路径。
if strings.Contains(file, stackFilterKeyForX) {
continue
}
} else {
// 包路径堆栈过滤。
if strings.Contains(file, stackFilterKeyLocal) {
continue
}
}
// 避免堆栈字符串如 `autogenerated`
if strings.Contains(file, "<") {
continue
}
// 忽略 GO ROOT 路径。
if goRootForFilter != "" &&
len(file) >= len(goRootForFilter) &&
file[0:len(goRootForFilter)] == goRootForFilter {
continue
}
if info.Lines == nil {
info.Lines = list.New()
}
info.Lines.PushBack(&stackLine{
Function: fn.Name(),
FileLine: fmt.Sprintf(`%s:%d`, file, line),
})
}
}
}
package xerror_test
import (
"testing"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"github.com/pkg/errors"
)
var (
// Wrap* 函数基准测试的基本错误。
baseError = errors.New("test")
)
func Benchmark_New(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.New("test")
}
}
func Benchmark_Newf(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.Newf("%s", "test")
}
}
func Benchmark_Wrap(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.Wrap(baseError, "test")
}
}
func Benchmark_Wrapf(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.Wrapf(baseError, "%s", "test")
}
}
func Benchmark_NewSkip(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewSkip(1, "test")
}
}
func Benchmark_NewSkipf(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewSkipf(1, "%s", "test")
}
}
func Benchmark_NewCode(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewCode(500, "test")
}
}
func Benchmark_NewCodef(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewCodef(500, "%s", "test")
}
}
func Benchmark_NewCodeSkip(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewCodeSkip(1, 500, "test")
}
}
func Benchmark_NewCodeSkipf(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.NewCodeSkipf(1, 500, "%s", "test")
}
}
func Benchmark_WrapCode(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.WrapCode(500, baseError, "test")
}
}
func Benchmark_WrapCodef(b *testing.B) {
for i := 0; i < b.N; i++ {
xerror.WrapCodef(500, baseError, "test")
}
}
package xerror_test
import (
"fmt"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"github.com/pkg/errors"
)
func ExampleNewCode() {
err := xerror.NewCode(10000, "My Error")
fmt.Println(err.Error())
fmt.Println(xerror.Code(err))
// Output:
// My Error
// 10000
}
func ExampleNewCodef() {
err := xerror.NewCodef(10000, "It's %s", "My Error")
fmt.Println(err.Error())
fmt.Println(xerror.Code(err))
// Output:
// It's My Error
// 10000
}
func ExampleWrapCode() {
err1 := errors.New("permission denied")
err2 := xerror.WrapCode(10000, err1, "Custom Error")
fmt.Println(err2.Error())
fmt.Println(xerror.Code(err2))
// Output:
// Custom Error: permission denied
// 10000
}
func ExampleWrapCodef() {
err1 := errors.New("permission denied")
err2 := xerror.WrapCodef(10000, err1, "It's %s", "Custom Error")
fmt.Println(err2.Error())
fmt.Println(xerror.Code(err2))
// Output:
// It's Custom Error: permission denied
// 10000
}
func ExampleEqual() {
err1 := errors.New("permission denied")
err2 := xerror.New("permission denied")
err3 := xerror.NewCode(xcode.CodeNotAuthorized, "permission denied")
fmt.Println(xerror.Equal(err1, err2))
fmt.Println(xerror.Equal(err2, err3))
// Output:
// true
// false
}
func ExampleIs() {
err1 := errors.New("permission denied")
err2 := xerror.Wrap(err1, "operation failed")
fmt.Println(xerror.Is(err1, err1))
fmt.Println(xerror.Is(err2, err2))
fmt.Println(xerror.Is(err2, err1))
fmt.Println(xerror.Is(err1, err2))
// Output:
// false
// true
// true
// false
}
package xerror_test
import (
"encoding/json"
"fmt"
"testing"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xcode"
"github.com/pkg/errors"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/xerrors/xerror"
"gorm.io/gorm"
"github.com/stretchr/testify/assert"
)
func nilError() error {
return nil
}
func Test_Nil(t *testing.T) {
assert.NotNil(t, xerror.New(""))
assert.Nil(t, xerror.Wrap(nilError(), "test"))
}
func Test_New(t *testing.T) {
err1 := xerror.New("1")
assert.NotNil(t, err1)
assert.Equal(t, err1.Error(), "1")
err2 := xerror.Newf("%d", 1)
assert.NotNil(t, err2)
assert.Equal(t, err2.Error(), "1")
err3 := xerror.NewSkipf(1, "%d", 1)
assert.NotNil(t, err3)
assert.Equal(t, err3.Error(), "1")
}
func Test_Wrap(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.Wrap(err1, "2")
err1 = xerror.Wrap(err1, "3")
assert.NotNil(t, err1)
assert.Equal(t, err1.Error(), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.Wrap(err2, "")
assert.NotNil(t, err2)
assert.Equal(t, err2.Error(), "1")
}
func Test_Wrapf(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.Wrapf(err1, "%d", 2)
err1 = xerror.Wrapf(err1, "%d", 3)
assert.NotNil(t, err1)
assert.Equal(t, err1.Error(), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.Wrapf(err2, "")
assert.NotNil(t, err2, nil)
assert.Equal(t, err2.Error(), "1")
}
func Test_WrapSkip(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.WrapSkip(1, err1, "2")
err1 = xerror.WrapSkip(1, err1, "3")
assert.NotNil(t, err1, nil)
assert.Equal(t, err1.Error(), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.WrapSkip(1, err2, "")
assert.NotNil(t, err2, nil)
assert.Equal(t, err2.Error(), "1")
}
func Test_WrapSkipf(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.WrapSkipf(1, err1, "2")
err1 = xerror.WrapSkipf(1, err1, "3")
assert.NotNil(t, err1, nil)
assert.Equal(t, err1.Error(), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.WrapSkipf(1, err2, "")
assert.NotNil(t, err2, nil)
assert.Equal(t, err2.Error(), "1")
}
func Test_Cause(t *testing.T) {
err := errors.New("1")
assert.Equal(t, xerror.Cause(err), err)
err1 := errors.New("1")
err1 = xerror.Wrap(err1, "2")
err1 = xerror.Wrap(err1, "3")
assert.Equal(t, xerror.Cause(err1).Error(), "1")
err2 := xerror.New("1")
assert.Equal(t, xerror.Cause(err2).Error(), "1")
err3 := xerror.New("1")
err3 = xerror.Wrap(err3, "2")
err3 = xerror.Wrap(err3, "3")
assert.Equal(t, xerror.Cause(err3).Error(), "1")
}
func Test_Format(t *testing.T) {
err1 := errors.New("1")
err1 = xerror.Wrap(err1, "2")
err1 = xerror.Wrap(err1, "3")
assert.NotNil(t, err1)
assert.Equal(t, fmt.Sprintf("%s", err1), "3: 2: 1")
assert.Equal(t, fmt.Sprintf("%v", err1), "3: 2: 1")
err2 := xerror.New("1")
err2 = xerror.Wrap(err2, "2")
err2 = xerror.Wrap(err2, "3")
assert.NotNil(t, err2, nil)
assert.Equal(t, fmt.Sprintf("%-s", err2), "3")
assert.Equal(t, fmt.Sprintf("%-v", err2), "3")
}
func Test_Stack(t *testing.T) {
err := errors.New("1")
assert.Equal(t, fmt.Sprintf("%+v", err), "1")
err1 := errors.New("1")
err1 = xerror.Wrap(err1, "2")
err1 = xerror.Wrap(err1, "3")
assert.NotNil(t, err1, nil)
// fmt.Printf("%+v", err1)
err2 := xerror.New("1")
assert.NotNil(t, fmt.Sprintf("%+v", err2), "1")
// fmt.Printf("%+v", err2)
err3 := xerror.New("1")
err3 = xerror.Wrap(err3, "2")
err3 = xerror.Wrap(err3, "3")
assert.NotNil(t, err3, nil)
// fmt.Printf("%+v", err3)
}
func Test_Current(t *testing.T) {
err := errors.New("1")
err = xerror.Wrap(err, "2")
err = xerror.Wrap(err, "3")
assert.Equal(t, err.Error(), "3: 2: 1")
assert.Equal(t, xerror.Current(err).Error(), "3")
}
func Test_Unwrap(t *testing.T) {
err := errors.New("1")
err = xerror.Wrap(err, "2")
err = xerror.Wrap(err, "3")
assert.Equal(t, err.Error(), "3: 2: 1")
err = xerror.Unwrap(err)
assert.Equal(t, err.Error(), "2: 1")
err = xerror.Unwrap(err)
assert.Equal(t, err.Error(), "1")
err = xerror.Unwrap(err)
assert.Nil(t, err)
}
func Test_Code(t *testing.T) {
err1 := errors.New("123")
assert.Equal(t, xerror.Code(err1), -1)
assert.Equal(t, err1.Error(), "123")
err2 := xerror.NewCode(xcode.CodeUnknown, "123")
assert.Equal(t, xerror.Code(err2), xcode.CodeUnknown)
assert.Equal(t, err2.Error(), "123")
err3 := xerror.NewCodef(1, "%s", "123")
assert.Equal(t, xerror.Code(err3), 1)
assert.Equal(t, err3.Error(), "123")
err4 := xerror.NewCodeSkip(1, 0, "123")
assert.Equal(t, xerror.Code(err4), 1)
assert.Equal(t, err4.Error(), "123")
err5 := xerror.NewCodeSkipf(1, 0, "%s", "123")
assert.Equal(t, xerror.Code(err5), 1)
assert.Equal(t, err5.Error(), "123")
err6 := errors.New("1")
err6 = xerror.Wrap(err6, "2")
err6 = xerror.WrapCode(1, err6, "3")
assert.Equal(t, xerror.Code(err6), 1)
assert.Equal(t, err6.Error(), "3: 2: 1")
err7 := errors.New("1")
err7 = xerror.Wrap(err7, "2")
err7 = xerror.WrapCodef(1, err7, "%s", "3")
assert.Equal(t, xerror.Code(err7), 1)
assert.Equal(t, err7.Error(), "3: 2: 1")
err8 := errors.New("1")
err8 = xerror.Wrap(err8, "2")
err8 = xerror.WrapCodeSkip(1, 100, err8, "3")
assert.Equal(t, xerror.Code(err8), 1)
assert.Equal(t, err8.Error(), "3: 2: 1")
err9 := errors.New("1")
err9 = xerror.Wrap(err9, "2")
err9 = xerror.WrapCodeSkipf(1, 100, err9, "%s", "3")
assert.Equal(t, xerror.Code(err9), 1)
assert.Equal(t, err9.Error(), "3: 2: 1")
}
func Test_SetCode(t *testing.T) {
err := xerror.New("123")
assert.Equal(t, xerror.Code(err), -1)
assert.Equal(t, err.Error(), "123")
err.(*xerror.Error).SetCode(xcode.CodeValidationFailed)
assert.Equal(t, xerror.Code(err), xcode.CodeValidationFailed)
assert.Equal(t, err.Error(), "123")
}
func Test_Json(t *testing.T) {
err := xerror.Wrap(xerror.New("1"), "2")
b, e := json.Marshal(err)
assert.Equal(t, e, nil)
assert.Equal(t, string(b), `"2: 1"`)
}
func Test_HasStack(t *testing.T) {
err1 := errors.New("1")
err2 := xerror.New("1")
assert.Equal(t, xerror.HasStack(err1), false)
assert.Equal(t, xerror.HasStack(err2), true)
}
func Test_Equal(t *testing.T) {
err1 := errors.New("1")
err2 := errors.New("1")
err3 := xerror.New("1")
err4 := xerror.New("4")
assert.Equal(t, xerror.Equal(err1, err2), false)
assert.Equal(t, xerror.Equal(err1, err3), true)
assert.Equal(t, xerror.Equal(err2, err3), true)
assert.Equal(t, xerror.Equal(err3, err4), false)
assert.Equal(t, xerror.Equal(err1, err4), false)
}
func Test_Is(t *testing.T) {
err1 := errors.New("1")
err2 := xerror.Wrap(err1, "2")
err2 = xerror.Wrap(err2, "3")
assert.Equal(t, xerror.Is(err2, err1), true)
err3 := xerror.Wrap(gorm.ErrRecordNotFound, "3")
assert.Equal(t, xerror.Is(err3, gorm.ErrRecordNotFound), true)
err4 := xerror.New(gorm.ErrRecordNotFound.Error())
assert.Equal(t, xerror.Is(err4, gorm.ErrRecordNotFound), true)
err5 := xerror.WrapCode(1, gorm.ErrRecordNotFound, "exes")
assert.Equal(t, xerror.Is(err5, gorm.ErrRecordNotFound), true)
t.Log((err5).(xerror.ICode))
}
func Test_HashError(t *testing.T) {
err1 := errors.New("1")
err2 := xerror.Wrap(err1, "2")
err2 = xerror.Wrap(err2, "3")
assert.Equal(t, xerror.HasError(err2, err1), true)
}
func Test_HashCode(t *testing.T) {
err1 := errors.New("1")
err2 := xerror.WrapCode(2, err1, "2")
err3 := xerror.Wrap(err2, "3")
err4 := xerror.Wrap(err3, "4")
assert.Equal(t, xerror.HasCode(err1, xcode.CodeDefault), false)
assert.Equal(t, xerror.HasCode(err2, xcode.CodeNotAuthorized), true)
assert.Equal(t, xerror.HasCode(err3, xcode.CodeNotAuthorized), true)
assert.Equal(t, xerror.HasCode(err4, xcode.CodeNotAuthorized), true)
}
package ctxutils
import (
"context"
"errors"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/sgorm/query"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
const (
GinContextKey = "ginContext"
HeaderXTimestampKey = "Timestamp"
KeyReqBody = "reqBody"
KeyRspBody = "rspBody"
KeyRspCode = "rspCode"
KeyOApiReqBody = "oApiReqBody%s"
KeyOApiRspBody = "oApiRspBody%s"
KeyAppName = "appName"
KeyUser = "user"
KeyApiStartTime = "apiStartTime"
KeyClientIP = "clientIP"
KeyUID = "uid"
KeyUType = "uType"
KeyCompanyID = "companyID"
KeyCompanyName = "companyName"
KeyShopID = "shopID"
KeyUName = "uname"
KeyToken = "token"
KeyCost = "X-Request-Cost"
KeyRspBodyMax = "rsp-body-max"
KeyApiType = "apiType" // enums: page
KeyPagination = "pagination"
)
var (
HeaderXRequestIDKey = "X-Request-ID"
ContextTraceIDKey = "X-Request-ID"
)
var (
ErrorGinContextNotFound = errors.New("gin context not found")
)
func Set(c *gin.Context, key string, val any) {
c.Set(key, val)
}
func GinTraceID(c *gin.Context) string {
if v, isExist := c.Get(ContextTraceIDKey); isExist {
if requestID, ok := v.(string); ok {
return requestID
}
}
return ""
}
func CtxTraceID(ctx context.Context) string {
if v := ctx.Value(ContextTraceIDKey); v != nil {
if traceID, ok := v.(string); ok {
return traceID
}
}
return ""
}
func GinTraceIDField(c *gin.Context) zap.Field {
return zap.String(ContextTraceIDKey, GinTraceID(c))
}
func CtxTraceIDField(ctx context.Context) zap.Field {
return zap.String(ContextTraceIDKey, CtxTraceID(ctx))
}
// func NewEmptyCtx() context.Context {
// return context.WithValue(context.Background(), ContextTraceIDKey, GenerateTid())
// }
func NewCtx(ctx context.Context) context.Context {
return context.WithValue(context.Background(), ContextTraceIDKey, ctx.Value(ContextTraceIDKey))
}
func GetClientIP(ctx context.Context) string {
ip := ""
ipVal := ctx.Value(KeyClientIP)
if ipVal != nil {
if str, ok := ipVal.(string); ok {
ip = str
}
}
return ip
}
func WrapCtx(c *gin.Context) context.Context {
ctx := context.WithValue(c.Request.Context(), ContextTraceIDKey, c.GetString(ContextTraceIDKey))
for k, v := range c.Keys {
ctx = context.WithValue(ctx, k, v)
}
ctx = context.WithValue(ctx, GinContextKey, c) //nolint
return ctx
}
func CtxGin(c context.Context) *gin.Context {
if str, ok := c.Value(GinContextKey).(*gin.Context); ok {
return str
}
return nil
}
type IPagination interface {
GetPageIndex() int
GetPageSize() int
}
func SetPage(ctx context.Context, in IPagination) {
if c := CtxGin(ctx); c != nil {
c.Set(KeyApiType, "page")
pagination := query.Pagination{
PageIndex: in.GetPageIndex(),
PageSize: in.GetPageSize(),
}
c.Set(KeyPagination, pagination)
}
}
package idutils
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/httpcli"
"gitlab.wanzhuangkj.com/tush/opkg/pkg/logger"
"gitlab.wanzhuangkj.com/tush/opkg/xutils/sf"
)
type IDGeneration struct {
Type string `yaml:"type" json:"type" mapstructure:"type"` // snowflake or leaf
Leaf Leaf `yaml:"leaf" json:"leaf" mapstructure:"leaf"`
Snowflake Snowflake `yaml:"snowflake" json:"snowflake" mapstructure:"snowflake"`
}
type Leaf struct {
Addr string `yaml:"addr" json:"addr" mapstructure:"addr"` // eg http://leaf.qitu
ShowLog bool `yaml:"showLog" json:"showLog" mapstructure:"showLog"`
RetryCount int `yaml:"retryCount" json:"retryCount" mapstructure:"retryCount"`
}
type Snowflake struct {
Node int `yaml:"node" json:"node" mapstructure:"node"`
}
var (
idGenerateType = "snowflake" // 默认snowflake方式生成
leafRetryCount = 3
leafShowLog = false
leafAddr string = "http://leaf.qitu"
leafApi = &client{
cli: httpcli.NewClient(
&http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: 4,
MaxIdleConns: 4,
IdleConnTimeout: 90 * time.Second,
},
}),
}
)
type client struct {
cli *httpcli.Client
}
func Init(cfg IDGeneration) error {
if cfg.Type != "" {
if cfg.Type == "leaf" {
if cfg.Leaf.Addr != "" {
leafAddr = cfg.Leaf.Addr
}
if cfg.Leaf.ShowLog {
leafShowLog = true
}
if cfg.Leaf.RetryCount > 0 {
leafRetryCount = cfg.Leaf.RetryCount
}
}
}
logger.Infof("[dxsf] type: %s", cfg.Type)
return nil
}
func GenerateID(ctx context.Context, bizTag string) (sf.ID, error) {
if idGenerateType == "snowflake" {
return generateSFID(ctx, bizTag)
} else {
return generateApiID(ctx, bizTag)
}
}
func generateSFID(_ context.Context, _ string) (sf.ID, error) {
return sf.GenerateID(), nil
}
func GenerateSFID() sf.ID {
return sf.GenerateID()
}
func generateApiID(ctx context.Context, bizTag string) (sf.ID, error) {
url := fmt.Sprintf(`%s/api/leaf?biz_tag=%s`, leafAddr, bizTag)
var id sf.ID
reply := &IDReply{
Data: &id,
}
req := leafApi.cli.NewRequest()
if !leafShowLog {
req = req.OmitLog()
}
if err := req.SetRetry(leafRetryCount).SetContentType("application/json").SetURL(url).GET(ctx).BindJSON(reply).Err(); err != nil {
logger.Error("调用leaf失败", logger.String("bizTag", bizTag), logger.Err(err))
return sf.ID(0), fmt.Errorf("调用leaf失败, %w", err)
}
if reply.Code == 1 {
return id, nil
}
return sf.ID(0), errors.New(reply.Msg)
}
type IDReply struct {
Code int `json:"code"`
Data any `json:"data"`
Msg string `json:"msg"`
}
package retryutils
import (
"context"
"errors"
"fmt"
"math"
"math/rand"
"time"
)
const (
DefaultRetryTimes = 5
DefaultRetryLinearInterval = time.Second * 3
)
type RetryConfig struct {
context context.Context
retryTimes uint
backoffStrategy BackoffStrategy
}
type RetryFunc func() error
type Option func(*RetryConfig)
func RetryTimes(n uint) Option {
return func(rc *RetryConfig) {
rc.retryTimes = n
}
}
func RetryWithCustomBackoff(backoffStrategy BackoffStrategy) Option {
if backoffStrategy == nil {
panic("backoffStrategy不能为空")
}
return func(rc *RetryConfig) {
rc.backoffStrategy = backoffStrategy
}
}
func RetryWithLinearBackoff(interval time.Duration) Option {
if interval <= 0 {
panic("retry间隔必须大于0")
}
return func(rc *RetryConfig) {
rc.backoffStrategy = &linear{
interval: interval,
}
}
}
func RetryWithExponentialWithJitterBackoff(interval time.Duration, base uint64, maxJitter time.Duration) Option {
if interval <= 0 {
panic("retry间隔必须大于0")
}
if maxJitter < 0 {
panic("maxJitter必须大于0")
}
if base%2 == 0 {
return func(rc *RetryConfig) {
rc.backoffStrategy = &shiftExponentialWithJitter{
interval: interval,
maxJitter: maxJitter,
shifter: uint64(math.Log2(float64(base))),
}
}
}
return func(rc *RetryConfig) {
rc.backoffStrategy = &exponentialWithJitter{
interval: interval,
base: time.Duration(base),
maxJitter: maxJitter,
}
}
}
func Context(ctx context.Context) Option {
return func(rc *RetryConfig) {
rc.context = ctx
}
}
func Retry(retryFunc RetryFunc, opts ...Option) error {
config := &RetryConfig{
retryTimes: DefaultRetryTimes,
context: context.TODO(),
}
for _, opt := range opts {
opt(config)
}
if config.backoffStrategy == nil {
config.backoffStrategy = &linear{
interval: DefaultRetryLinearInterval,
}
}
var (
i uint
err error
)
for i < config.retryTimes {
if err = retryFunc(); err != nil {
after := time.After(config.backoffStrategy.CalculateInterval())
select {
case <-after:
case <-config.context.Done():
return errors.New("retry is cancelled")
}
} else {
return nil
}
i++
}
// funcPath := runtime.FuncForPC(reflect.ValueOf(retryFunc).Pointer()).Name()
// lastSlash := strings.LastIndex(funcPath, "/")
// funcName := funcPath[lastSlash+1:]
return fmt.Errorf("retry[%d] faild, %s", config.retryTimes, err.Error())
}
type BackoffStrategy interface {
CalculateInterval() time.Duration
}
type linear struct {
interval time.Duration
}
func (l linear) CalculateInterval() time.Duration {
return l.interval
}
type exponentialWithJitter struct {
base time.Duration
interval time.Duration
maxJitter time.Duration
}
func (e *exponentialWithJitter) CalculateInterval() time.Duration {
current := e.interval
e.interval = e.interval * e.base
return current + jitter(e.maxJitter)
}
type shiftExponentialWithJitter struct {
interval time.Duration
maxJitter time.Duration
shifter uint64
}
func (e shiftExponentialWithJitter) CalculateInterval() time.Duration {
current := e.interval
e.interval = e.interval << e.shifter
return current + jitter(e.maxJitter)
}
func jitter(maxJitter time.Duration) time.Duration {
if maxJitter == 0 {
return 0
}
return time.Duration(rand.Int63n(int64(maxJitter)) + 1)
}
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论