From 2208656bc72b787cfdbd00038f33eb4ff966c8d2 Mon Sep 17 00:00:00 2001 From: Leon Date: Fri, 16 Feb 2024 12:35:42 +0800 Subject: [PATCH] feat: add go-cache and cron basic configuration (#8) * Add logging and colorize output * Update config file name and instructions * Add server start output and refactor print function * Add local server URL to server startup message * Add go-cache and cron basic configuration --- README.md | 8 ++-- cmd/main.go | 49 ++++++++++++++++++++--- cmd/srv/controller/controller.go | 2 +- config.yaml.example | 3 +- go.mod | 2 + go.sum | 4 ++ internal/gconfig/config.go | 1 + {pkg/mygin => internal/gogin}/template.go | 11 ++--- pkg/utils/utils.go | 27 ++++++++----- service/singleton/crontask.go | 22 ++++++++++ service/singleton/singleton.go | 31 ++++++++++---- 11 files changed, 126 insertions(+), 34 deletions(-) rename {pkg/mygin => internal/gogin}/template.go (89%) create mode 100644 service/singleton/crontask.go diff --git a/README.md b/README.md index 4d86a00..f02f96e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Go Gin -Gin gonic starter with zerolog, viper, gorm, jwt basic setup. +Gin gonic starter with zerolog, viper, gorm, jwt basic, go-cache, cron basic configuration. [![Build Status][build-status-image]][build-status] [![license][license-image]][repository-url] @@ -19,10 +19,10 @@ If you want to develop with this project, you can follow the steps below. git clone git@github.com:funnyzak/go-gin.git && cd go-gin ``` -2. Copy the `config.example.json` file to `config.json` and update the values. +2. Copy the `config.yaml.example` file to `config.yaml` and update the values. ```bash - cp config.example.json config.json + cp config.yaml.example config.yaml ``` 3. Run the application. @@ -38,7 +38,7 @@ If you want to develop with this project, you can follow the steps below. ### CI/CD -You can click `Use this template` to create a new repository based on this project. and add Secrets Keys: `DOCKER_USERNAME` and `DOCKER_PASSWORD` in the repository settings. And when you push the code, it will automatically build binary and docker image and push to the Docker Hub. +You can fork this repository and add Secrets Keys: `DOCKER_USERNAME` and `DOCKER_PASSWORD` in the repository settings. And when you push the code, it will automatically build binary and docker image and push to the Docker Hub. ## Structure diff --git a/cmd/main.go b/cmd/main.go index 5b86cc6..25d4954 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,7 +2,9 @@ package main import ( "context" + "fmt" "go-gin/cmd/srv/controller" + "go-gin/pkg/utils" "go-gin/service/singleton" "github.com/ory/graceful" @@ -10,6 +12,7 @@ import ( ) type CliParam struct { + Version bool // Show version ConfigName string // Config file name Port uint // Server port } @@ -18,17 +21,25 @@ var ( cliParam CliParam ) -func main() { +func init() { flag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true + flag.BoolVarP(&cliParam.Version, "version", "v", false, "show version") flag.StringVarP(&cliParam.ConfigName, "config", "c", "config", "config file name") flag.UintVarP(&cliParam.Port, "port", "p", 0, "server port") flag.Parse() flag.Lookup("config").NoOptDefVal = "config" - singleton.InitConfig(cliParam.ConfigName) singleton.InitLog(singleton.Conf) + singleton.InitTimezoneAndCache() singleton.InitDBFromPath(singleton.Conf.DBPath) initService() +} + +func main() { + if cliParam.Version { + fmt.Println(singleton.Version) + return + } port := singleton.Conf.Server.Port if cliParam.Port != 0 { @@ -37,18 +48,44 @@ func main() { srv := controller.ServerWeb(port) + startOutput := func() { + fmt.Println() + fmt.Println("Server is running with config:") + utils.PrintStructFieldsAndValues(singleton.Conf, "") + + fmt.Println() + fmt.Println("Server is running at:") + fmt.Printf(" - %-7s: %s\n", "Local", utils.Colorize(utils.ColorGreen, fmt.Sprintf("http://127.0.0.1:%d", port))) + ipv4s, err := utils.GetIPv4NetworkIPs() + if ipv4s != nil && err == nil { + for _, ip := range ipv4s { + fmt.Printf(" - %-7s: %s\n", "Network", utils.Colorize(utils.ColorGreen, fmt.Sprintf("http://%s:%d", ip, port))) + } + } + fmt.Println() + } + if err := graceful.Graceful(func() error { + startOutput() return srv.ListenAndServe() }, func(c context.Context) error { - singleton.Log.Info().Msg("Graceful::START") + fmt.Print(utils.Colorize("Server is shutting down", utils.ColorRed)) srv.Shutdown(c) return nil }); err != nil { - singleton.Log.Err(err).Msg("Graceful::Error") + fmt.Println(utils.Colorize("Server is shutting down with error: %s", utils.ColorRed), err) } - } func initService() { - singleton.InitSingleton() + // Load all services in the singleton package + singleton.LoadSingleton() + + if _, err := singleton.Cron.AddFunc("0 * * * * *", writeHello); err != nil { + panic(err) + } +} + +func writeHello() { + singleton.Log.Info().Msg("Hello world, I am a cron task") } diff --git a/cmd/srv/controller/controller.go b/cmd/srv/controller/controller.go index 75e7433..48ac69f 100644 --- a/cmd/srv/controller/controller.go +++ b/cmd/srv/controller/controller.go @@ -57,7 +57,7 @@ func serveStatic(r *gin.Engine) { // Load templates func loadTemplates(r *gin.Engine) { - new_tmpl := template.New("").Funcs(mygin.FuncMap) + new_tmpl := template.New("").Funcs(gogin.FuncMap) var err error new_tmpl, err = new_tmpl.ParseFS(resource.TemplateFS, "template/**/*.html", "template/*.html") if err != nil { diff --git a/config.yaml.example b/config.yaml.example index c66ae63..314f280 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -6,7 +6,7 @@ site: description: A simple web application using Go and Gin base_url: http://localhost:8080 -debug: true +debug: false log: level: debug @@ -28,3 +28,4 @@ jwt: refresh_token_expiration: 720 # minutes access_token_cookie_name: go-gin-access refresh_token_cookie_name: go-gin-refresh +location: Asia/Chongqing # Timezone diff --git a/go.mod b/go.mod index b52aa6f..3118dab 100644 --- a/go.mod +++ b/go.mod @@ -42,8 +42,10 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/myesui/uuid v1.0.0 // indirect github.com/ory/graceful v0.1.3 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/robfig/cron/v3 v3.0.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/go.sum b/go.sum index dd344bd..0efe74b 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,8 @@ github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc= github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= @@ -111,6 +113,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= +github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= diff --git a/internal/gconfig/config.go b/internal/gconfig/config.go index f0e533c..6f367fa 100644 --- a/internal/gconfig/config.go +++ b/internal/gconfig/config.go @@ -35,4 +35,5 @@ type Config struct { AccessTokenCookieName string `mapstructure:"access_token_cookie_name"` RefreshTokenCookieName string `mapstructure:"refresh_token_cookie_name"` } `mapstructure:"jwt"` + Location string `mapstructure:"location"` } diff --git a/pkg/mygin/template.go b/internal/gogin/template.go similarity index 89% rename from pkg/mygin/template.go rename to internal/gogin/template.go index 92526d1..7f7d8c5 100644 --- a/pkg/mygin/template.go +++ b/internal/gogin/template.go @@ -1,7 +1,8 @@ -package mygin +package gogin import ( "fmt" + "go-gin/service/singleton" "html/template" "strconv" "strings" @@ -17,10 +18,10 @@ var FuncMap = template.FuncMap{ } }, "tf": func(t time.Time) string { - return t.Format("01/02/2006 15:04:05") + return t.In(singleton.Loc).Format("01/02/2006 15:04:05") }, "tsf": func(ts int64) string { - return time.Unix(int64(ts/1000), 0).Format("01/02/2006 15:04:05") + return time.Unix(int64(ts/1000), 0).In(singleton.Loc).Format("01/02/2006 15:04:05") }, "text2html": func(text string) template.HTML { text = strings.Replace(text, "\n", "

", -1) @@ -36,7 +37,7 @@ var FuncMap = template.FuncMap{ return template.HTML(`<` + s + `>`) // #nosec }, "stf": func(s uint64) string { - return time.Unix(int64(s), 0).Format("01/02/2006 15:04") + return time.Unix(int64(s), 0).In(singleton.Loc).Format("01/02/2006 15:04") }, "sf": func(duration uint64) string { return time.Duration(time.Duration(duration) * time.Second).String() @@ -119,7 +120,7 @@ var FuncMap = template.FuncMap{ }, "dayBefore": func(i int) string { year, month, day := time.Now().Date() - today := time.Date(year, month, day, 0, 0, 0, 0, time.Local) + today := time.Date(year, month, day, 0, 0, 0, 0, singleton.Loc) return today.AddDate(0, 0, i-29).Format("01/02") }, "className": func(percent float32) string { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index afe63d0..4618cc0 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -133,7 +133,7 @@ func GetNetworkIPs() ([]string, error) { return ips, nil } -func PrintStructFieldsAndValues(s interface{}, title string) error { +func PrintStructFieldsAndValues(s interface{}, indent string) { v := reflect.ValueOf(s) if v.Kind() == reflect.Ptr { @@ -141,23 +141,30 @@ func PrintStructFieldsAndValues(s interface{}, title string) error { } if v.Kind() != reflect.Struct { - return fmt.Errorf("PrintStructFieldsAndValues: %v is not a struct", v.Type()) + fmt.Printf("%s%v is not a struct\n", indent, v.Type()) + return } typeOfS := v.Type() - fmt.Println() - if title != "" { - fmt.Printf("%s\n", title) - } for i := 0; i < v.NumField(); i++ { field := v.Field(i) - if field.CanInterface() { - fmt.Printf(" - %-20s: %v\n", typeOfS.Field(i).Name, Colorize(ColorGreen, fmt.Sprint(field.Interface()))) + fmt.Printf("%s - %s: ", indent, typeOfS.Field(i).Name) + + if field.Kind() == reflect.Struct { + fmt.Println() + PrintStructFieldsAndValues(field.Interface(), indent+" ") + } else if field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct { + fmt.Println() + PrintStructFieldsAndValues(field.Interface(), indent+" ") + } else { + if field.CanInterface() { + fmt.Println(Colorize(ColorGreen, fmt.Sprint(field.Interface()))) + } else { + fmt.Println() + } } } - fmt.Println() - return nil } func ParseBool(val string, defVal bool) bool { diff --git a/service/singleton/crontask.go b/service/singleton/crontask.go new file mode 100644 index 0000000..68434ab --- /dev/null +++ b/service/singleton/crontask.go @@ -0,0 +1,22 @@ +package singleton + +import ( + "sync" + + "github.com/robfig/cron/v3" +) + +var ( + Cron *cron.Cron + CronLock sync.RWMutex +) + +func InitCronTask() { + Cron = cron.New(cron.WithSeconds(), cron.WithLocation(Loc)) +} + +func LoadCronTasks() { + InitCronTask() + + Cron.Start() +} diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index ff06b58..4f13b43 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -3,8 +3,11 @@ package singleton import ( "fmt" "os" + "time" + "github.com/patrickmn/go-cache" "github.com/rs/zerolog" + "github.com/spf13/viper" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -17,22 +20,36 @@ import ( var Version = "0.0.1" var ( - Conf *gconfig.Config - Log *zerolog.Logger - DB *gorm.DB + ViperConf *viper.Viper // Viper config for the application + Conf *gconfig.Config // Global config for the application + Log *zerolog.Logger // Global logger for the application + DB *gorm.DB // Global db for the application + Cache *cache.Cache // Global cache for the application + Loc *time.Location // Global location for the application ) -func InitSingleton() { - // TOO: init db +func LoadSingleton() { + LoadCronTasks() + // TODO: Add your initialization code here, eg Service, Task, etc. +} + +func InitTimezoneAndCache() { + var err error + Loc, err = time.LoadLocation(Conf.Location) + if err != nil { + panic(err) + } + + Cache = cache.New(5*time.Minute, 10*time.Minute) } func InitConfig(name string) { - _config, err := utils.ReadViperConfig(name, "yaml", []string{".", "./config", "../"}) + ViperConf, err := utils.ReadViperConfig(name, "yaml", []string{".", "./config", "../"}) if err != nil { panic(fmt.Errorf("unable to read config: %s", err)) } - if err := _config.Unmarshal(&Conf); err != nil { + if err := ViperConf.Unmarshal(&Conf); err != nil { panic(fmt.Errorf("unable to unmarshal config: %s", err)) } }