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))
}
}