├── .dockerignore ├── .env.template ├── .gitignore ├── Makefile ├── README.md ├── base.env ├── command ├── root.go ├── seed.go └── server.go ├── controller ├── responses.go └── user_controller.go ├── db ├── dbconf.yml ├── migrations │ ├── 20190516163301_Init.go │ └── 20250304073807_MigrationName.sql └── seeds │ ├── seed_users.go │ └── seeds.go ├── dic ├── app.go ├── config.go └── migrations.go ├── doc └── .gitignore ├── docker-compose.yml ├── docker ├── go │ ├── Dockerfile │ ├── dbconf.yml │ └── entrypoint.sh └── wait-for-it.sh ├── go.mod ├── go.sum ├── install └── install.sh ├── logger └── logger.go ├── main.go ├── model ├── db │ └── db.go ├── entity │ └── user.go └── service │ └── user_service.go └── route ├── api.go └── description ├── base.go └── user.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | vendor 3 | .env 4 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | SENTRY_DSN=https://your_public_key@sentry.io/project_id 2 | # Seconds 3 | SENTRY_TIMEOUT=30 4 | 5 | DB_URL=host=localhost user=postgres dbname=go-api-boilerplate sslmode=disable password=postgres 6 | 7 | SERVER_PORT=8081 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | bin 5 | vendor 6 | gin-bin 7 | .env 8 | .env.prod 9 | .env.local 10 | docker-compose.override.yml 11 | 12 | coverage.out 13 | dev.txt 14 | log.txt 15 | tmp* 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PATH_THIS:=$(realpath $(dir $(lastword ${MAKEFILE_LIST}))) 2 | DIR:=$(PATH_THIS) 3 | 4 | include base.env 5 | VARS:=$(shell sed -ne 's/ *\#.*$$//; /./ s/=.*$$// p' base.env ) 6 | $(foreach v,$(VARS),$(eval $(shell echo export $(v)="$($(v))"))) 7 | 8 | include .env 9 | VARS:=$(shell sed -ne 's/ *\#.*$$//; /./ s/=.*$$// p' .env ) 10 | $(foreach v,$(VARS),$(eval $(shell echo export $(v)="$($(v))"))) 11 | 12 | CODE=\033[0;33m 13 | NAME=\033[0;32m 14 | NC=\033[0m # No Color 15 | 16 | # |\__/,| (`\ 17 | # _.|o o |_ ) ) 18 | #-(((---(((-------- 19 | 20 | help: 21 | @echo "${NAME} /') |,\__/|${NC}" 22 | @echo "${NAME} ( ( _| o o|._ go-api-boilerplate${NC}" 23 | @echo "${NAME}--------)))---)))-----------------------${NC}" 24 | @echo "" 25 | @echo "${NAME}server${NC}" 26 | @echo " Run server" 27 | @echo "${NAME}new_migration${NC}" 28 | @echo " Create a new migration." 29 | @echo " Example: ${CODE}make new_migration type=sql name=MigrationName${NC}" 30 | @echo "${NAME}migrate${NC}" 31 | @echo " Run migrations" 32 | @echo "${NAME}rollback${NC}" 33 | @echo " Rollback last migration" 34 | @echo "${NAME}seed${NC}" 35 | @echo " Run seeders" 36 | @echo "${NAME}test${NC}" 37 | @echo " Run tests" 38 | @echo "${NAME}swagger${NC}" 39 | @echo " Generate Swagger documentation" 40 | @echo "" 41 | 42 | 43 | .PHONY: server 44 | server: 45 | @cd $(DIR) \ 46 | && air server 47 | 48 | .PHONY: new_migration 49 | new_migration: 50 | @cd $(DIR) \ 51 | && goose create -type $(type) $(name) 52 | 53 | .PHONY: migrate 54 | migrate: 55 | @cd $(DIR) \ 56 | && goose up 57 | 58 | .PHONY: rollback 59 | rollback: 60 | @cd $(DIR) \ 61 | && goose down 62 | 63 | .PHONY: seed 64 | seed: 65 | @cd $(DIR) \ 66 | && go run main.go seed 67 | 68 | .PHONY: test 69 | test: 70 | @cd $(DIR) \ 71 | && go test ./... 72 | 73 | .PHONY: swagger 74 | swagger: 75 | @cd $(DIR) \ 76 | && swagger generate spec -o doc/swagger.json 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go API Boilerplate 2 | 3 | ``` 4 | /') |,\__/| 5 | ( ( _| o o|._ go-api-boilerplate 6 | --------)))---)))----------------------- 7 | ``` 8 | 9 | ## Features 10 | 11 | - [x] Framework for API: Gin 12 | - [x] Package manager: go mod 13 | - [x] DI: Based on service container 14 | - [x] Layers: Controller->Service->Entity 15 | - [x] Routes: Gin 16 | - [x] Process controller results and convert them into JSON/XML according to request headers 17 | - [x] Logger: logrus 18 | - [x] Environment variables, config: Viper 19 | - [x] ORM: GORM 20 | - [x] Migrations: goose 21 | - [x] Data seeders 22 | - [x] Console commands: Cobra 23 | - [x] Unit tests with overriding of services in DI (`go test`) 24 | - [x] Code coverage by tests (`go tool cover`) 25 | - [x] Logger integration with Sentry 26 | - [x] Setup alerting for unhandled errors 27 | - [x] Swagger 28 | - [x] Docker compose 29 | - [x] Makefile 30 | - [x] Development: hot reload code 31 | 32 | 33 | ## Folders structure 34 | 35 | - `command/`: Console commands. 36 | - `controller/`: Controllers for web requests processing. 37 | - `db/`: Migrations and seeders. 38 | - `dic/`: Dependency Injection Container. 39 | - `doc/`: Swagger documentation. 40 | - `docker/`: Docker containers description. 41 | - `install/`: Scripts for environment preparing. 42 | - `logger/`: Logger and client for Sentry. 43 | - `model/`: Business logic. 44 | - `model/db/`: DB connection. 45 | - `model/entity/`: GORM entities. 46 | - `model/service/`: Business logic. 47 | - `route/`: Web requests routes. 48 | - `vendor/`: Packages using in application. 49 | - `base.env`: Base environment variables. 50 | - `.env`: Environment variables for current environment. 51 | 52 | 53 | ## How to use (Docker) 54 | 55 | 56 | ```bash 57 | docker-compose up --build 58 | ``` 59 | 60 | Check 61 | - http://localhost:8080/users 62 | - http://localhost:8080/doc/swagger/index.html 63 | 64 | 65 | ## How to use (without Docker) 66 | 67 | ### Install necessary packages 68 | 69 | ```bash 70 | ./install/install.sh 71 | ``` 72 | 73 | 74 | ### Create and edit config 75 | 76 | 77 | ```bash 78 | cp .env.template .env 79 | mcedit .env 80 | ``` 81 | 82 | 83 | ### Get vendor packages 84 | 85 | ```bash 86 | go mod vendor 87 | ``` 88 | 89 | 90 | ### Run migrations 91 | 92 | Create database `go-api-boilerplate`. 93 | 94 | And run migrations: 95 | 96 | ```bash 97 | make migrate 98 | ``` 99 | 100 | 101 | ### Run application 102 | 103 | Check available commands 104 | 105 | ```bash 106 | make 107 | ``` 108 | 109 | Run http server 110 | 111 | ```bash 112 | make server 113 | ``` 114 | 115 | Or: 116 | 117 | ```bash 118 | go run main.go server --port=8081 119 | ``` 120 | 121 | Check http://localhost:8081 122 | 123 | 124 | ### Run tests 125 | 126 | Run all tests: 127 | 128 | ```bash 129 | go test ./... -v -coverpkg=./... -coverprofile=coverage.out 130 | go tool cover -html=coverage.out 131 | ``` 132 | 133 | Run test for one package: 134 | 135 | ```bash 136 | go test go-api-boilerplate/test/unit -v -coverpkg=./... -coverprofile=coverage.out 137 | ``` 138 | 139 | Run one test: 140 | 141 | ```bash 142 | go test test/unit/user_service_test.go -v -coverpkg=./... -coverprofile=coverage.out 143 | ``` 144 | 145 | Using make: 146 | 147 | ```bash 148 | make test 149 | ``` 150 | 151 | 152 | ### Generate Swagger documentation 153 | 154 | Generate swagger.json: 155 | 156 | ```bash 157 | make swagger 158 | ``` 159 | 160 | Documentation must be available at url http://localhost:8081/doc/swagger/index.html 161 | 162 | 163 | ## Requirements 164 | - Go 1.23+ 165 | -------------------------------------------------------------------------------- /base.env: -------------------------------------------------------------------------------- 1 | APP_COMMAND=go-api-boilerplate 2 | APP_NAME="Go API Boilerplate" 3 | 4 | GIN_MODE=release 5 | LOG_LEVEL=info 6 | 7 | # Seconds 8 | SENTRY_TIMEOUT=15 9 | 10 | SERVER_PORT=80 11 | 12 | DB_URL=host=localhost user=postgres dbname=go-api-boilerplate sslmode=disable password=postgres 13 | DB_MAX_CONNECTIONS=20 14 | DB_LOG_MODE=false 15 | -------------------------------------------------------------------------------- /command/root.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/spf13/viper" 7 | "os" 8 | ) 9 | 10 | var rootCmd = &cobra.Command{ 11 | Short: "Application description", 12 | Long: `Long 13 | application 14 | description`, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | // Do Stuff Here 17 | }, 18 | } 19 | 20 | func Execute() { 21 | rootCmd.Use = viper.GetString("APP_COMMAND") 22 | if err := rootCmd.Execute(); err != nil { 23 | fmt.Println(err) 24 | os.Exit(1) 25 | } 26 | } -------------------------------------------------------------------------------- /command/seed.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/zubroide/go-api-boilerplate/db/seeds" 7 | "github.com/zubroide/go-api-boilerplate/dic" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | var seederName string 12 | 13 | func init() { 14 | seedCmd.PersistentFlags().StringVar(&seederName, "seeder", "", "Seeder name") 15 | rootCmd.AddCommand(seedCmd) 16 | } 17 | 18 | var seedCmd = &cobra.Command{ 19 | Use: "seed", 20 | Short: "Run seeder", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | 23 | // get gormDB from dic 24 | gormDB, ok := dic.Container.Get(dic.Db).(*gorm.DB) 25 | if !ok || gormDB == nil { 26 | fmt.Println("DB instance does not resolved") 27 | } 28 | 29 | // prepare auto seeders collection 30 | auto := seeds.NewSeeds(gormDB) 31 | auto.AppendSeeder(&seeds.SeedUsers{SeederBase: seeds.SeederBase{"users"}}) 32 | 33 | // prepare manual seeder collection 34 | manual := seeds.NewSeeds(gormDB) 35 | manual.AppendSeeder(&seeds.SeedUsers{SeederBase: seeds.SeederBase{"users"}}) 36 | 37 | // run target seeders 38 | if len(seederName) > 0 { 39 | if err := manual.RunSeederByName(seederName); nil != err { 40 | panic(err) 41 | } 42 | } else { 43 | if err := auto.RunSeeds(); nil != err { 44 | panic(err) 45 | } 46 | } 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /command/server.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | "github.com/zubroide/go-api-boilerplate/dic" 7 | "github.com/zubroide/go-api-boilerplate/route" 8 | ) 9 | 10 | func init() { 11 | var serverPort string 12 | defaultServerPort := viper.GetString("SERVER_PORT") 13 | serverCmd.PersistentFlags().StringVar(&serverPort, "port", defaultServerPort, "Server port") 14 | viper.BindPFlag("SERVER_PORT", serverCmd.PersistentFlags().Lookup("port")) 15 | 16 | rootCmd.AddCommand(serverCmd) 17 | } 18 | 19 | var serverCmd = &cobra.Command{ 20 | Use: "server", 21 | Short: "Run server", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | router := route.Setup(dic.Builder) 24 | router.Run(":" + viper.GetString("SERVER_PORT")) 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /controller/responses.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func SuccessJSON(ctx *gin.Context, data interface{}) { 9 | ctx.JSON(http.StatusOK, gin.H{"data": data}) 10 | } 11 | 12 | func BadRequestJSON(ctx *gin.Context, message string) { 13 | ctx.JSON(http.StatusBadRequest, gin.H{"error": message}) 14 | } 15 | 16 | func ServerErrorJSON(ctx *gin.Context, message string) { 17 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": message}) 18 | } 19 | -------------------------------------------------------------------------------- /controller/user_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/zubroide/go-api-boilerplate/logger" 6 | "github.com/zubroide/go-api-boilerplate/model/service" 7 | ) 8 | 9 | type UserController struct { 10 | service service.UserServiceInterface 11 | logger logger.LoggerInterface 12 | } 13 | 14 | type UserListParameters struct { 15 | Name string `json:"name" form:"name"` 16 | } 17 | 18 | func NewUserController(service service.UserServiceInterface, logger logger.LoggerInterface) *UserController { 19 | return &UserController{service, logger} 20 | } 21 | 22 | func (c *UserController) List(ctx *gin.Context) { 23 | var params UserListParameters 24 | if err := ctx.ShouldBind(¶ms); err != nil { 25 | BadRequestJSON(ctx, err.Error()) 26 | return 27 | } 28 | list, err := c.service.GetUsers(params.Name) 29 | if err != nil { 30 | ServerErrorJSON(ctx, "Something went wrong") 31 | return 32 | } 33 | SuccessJSON(ctx, list) 34 | } 35 | -------------------------------------------------------------------------------- /db/dbconf.yml: -------------------------------------------------------------------------------- 1 | development: 2 | driver: postgres 3 | # Must be specified but is not used in the code 4 | open: $DB_URL 5 | # Must be specified but is not used in the code 6 | dsn: $DB_URL 7 | -------------------------------------------------------------------------------- /db/migrations/20190516163301_Init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/zubroide/go-api-boilerplate/dic" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type User struct { 10 | gorm.Model 11 | Name string `binding:"required"` 12 | } 13 | 14 | // Up is executed when this migration is applied 15 | func Up_20190516163301(tx *sql.Tx) { 16 | dic.DB.Migrator().CreateTable(&User{}) 17 | } 18 | 19 | // Down is executed when this migration is rolled back 20 | func Down_20190516163301(tx *sql.Tx) { 21 | dic.DB.Migrator().DropTable(&User{}) 22 | } 23 | -------------------------------------------------------------------------------- /db/migrations/20250304073807_MigrationName.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- SQL in section 'Up' is executed when this migration is applied 3 | 4 | select 1; 5 | 6 | -- +goose Down 7 | -- SQL section 'Down' is executed when this migration is rolled back 8 | 9 | select 1; 10 | -------------------------------------------------------------------------------- /db/seeds/seed_users.go: -------------------------------------------------------------------------------- 1 | package seeds 2 | 3 | import ( 4 | "github.com/zubroide/go-api-boilerplate/model/entity" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type SeedUsers struct { 9 | SeederBase 10 | } 11 | 12 | func (s *SeedUsers) Seed(db *gorm.DB) error { 13 | values := []*entity.User{ 14 | {Name: "Test user"}, 15 | } 16 | 17 | for _, value := range values { 18 | db.Where(entity.User{Name: value.Name}). 19 | Assign(entity.User{ 20 | Name: value.Name, 21 | }). 22 | FirstOrCreate(value) 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /db/seeds/seeds.go: -------------------------------------------------------------------------------- 1 | package seeds 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // Seeds manager seeders 10 | type Seeds struct { 11 | db *gorm.DB 12 | items []Seeder 13 | } 14 | 15 | // Seeder seeder interface 16 | type Seeder interface { 17 | GetName() string 18 | Seed(db *gorm.DB) error 19 | } 20 | 21 | type SeederBase struct { 22 | Name string 23 | } 24 | 25 | func (s *SeederBase) GetName() string { 26 | return s.Name 27 | } 28 | 29 | // NewSeeds factory new seeders manager 30 | func NewSeeds(db *gorm.DB) *Seeds { 31 | seeds := new(Seeds) 32 | seeds.db = db 33 | return seeds 34 | } 35 | 36 | // AppendSeeder register seeder 37 | func (s *Seeds) AppendSeeder(seeder Seeder) error { 38 | if nil != s.FindSeeder(seeder.GetName()) { 39 | err := fmt.Sprintf("Seeder with name (%s) already exists", seeder.GetName()) 40 | return errors.New(err) 41 | } 42 | s.items = append(s.items, seeder) 43 | return nil 44 | } 45 | 46 | func (s *Seeds) FindSeeder(name string) Seeder { 47 | for _, seeder := range s.items { 48 | if seeder.GetName() == name { 49 | return seeder 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | // RunSeeder run target seeder 56 | func (s *Seeds) RunSeeder(seeder Seeder) error { 57 | fmt.Printf("Seeder (%s) start\n", seeder.GetName()) 58 | // run seeder 59 | if err := seeder.Seed(s.db); nil != err { 60 | return err 61 | } 62 | fmt.Printf("Seeder (%s) complete\n", seeder.GetName()) 63 | 64 | return nil 65 | } 66 | 67 | // RunSeederByName run target seeder by name 68 | func (s *Seeds) RunSeederByName(name string) error { 69 | seeder := s.FindSeeder(name) 70 | if nil == seeder { 71 | err := fmt.Sprintf("Seeder with name (%s) does not exists", name) 72 | return errors.New(err) 73 | } 74 | return s.RunSeeder(seeder) 75 | } 76 | 77 | // RunSeeds run all seeders 78 | func (s *Seeds) RunSeeds() error { 79 | for _, seeder := range s.items { 80 | if err := s.RunSeeder(seeder); nil != err { 81 | return err 82 | } 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /dic/app.go: -------------------------------------------------------------------------------- 1 | package dic 2 | 3 | import ( 4 | sentrylogrus "github.com/getsentry/sentry-go/logrus" 5 | "github.com/sarulabs/di/v2" 6 | "github.com/sirupsen/logrus" 7 | "github.com/spf13/viper" 8 | "github.com/zubroide/go-api-boilerplate/controller" 9 | "github.com/zubroide/go-api-boilerplate/logger" 10 | "github.com/zubroide/go-api-boilerplate/model/db" 11 | "github.com/zubroide/go-api-boilerplate/model/service" 12 | "gorm.io/gorm" 13 | "strings" 14 | ) 15 | 16 | var Builder *di.Builder 17 | var Container di.Container 18 | 19 | const SentryHook = "SentryHook" 20 | const Logger = "logger" 21 | const Db = "db" 22 | const UserService = "service.user" 23 | const UserController = "controller.user" 24 | 25 | func InitContainer() di.Container { 26 | builder := InitBuilder() 27 | Container = builder.Build() 28 | return Container 29 | } 30 | 31 | func InitBuilder() *di.Builder { 32 | Builder, _ = di.NewBuilder() 33 | RegisterServices(Builder) 34 | return Builder 35 | } 36 | 37 | func RegisterServices(builder *di.Builder) { 38 | builder.Add(di.Def{ 39 | Name: SentryHook, 40 | Build: func(ctn di.Container) (interface{}, error) { 41 | dsn := viper.GetString("SENTRY_DSN") 42 | if dsn == "" { 43 | var sh *sentrylogrus.Hook 44 | return sh, nil 45 | } 46 | return logger.NewSentryHook(dsn) 47 | }, 48 | }) 49 | 50 | builder.Add(di.Def{ 51 | Name: Logger, 52 | Build: func(ctn di.Container) (interface{}, error) { 53 | level, _ := logrus.ParseLevel(strings.ToLower( 54 | viper.GetString("LOG_LEVEL"), 55 | )) 56 | return logger.NewLogger( 57 | ctn.Get(SentryHook).(*sentrylogrus.Hook), 58 | //nil, 59 | level, 60 | viper.GetInt("SENTRY_TIMEOUT"), 61 | ), nil 62 | }, 63 | }) 64 | 65 | builder.Add(di.Def{ 66 | Name: Db, 67 | Build: func(ctn di.Container) (interface{}, error) { 68 | return db.NewDb(), nil 69 | }, 70 | }) 71 | 72 | builder.Add(di.Def{ 73 | Name: UserService, 74 | Build: func(ctn di.Container) (interface{}, error) { 75 | return service.NewUserService(ctn.Get(Db).(*gorm.DB), ctn.Get(Logger).(logger.LoggerInterface)), nil 76 | }, 77 | }) 78 | 79 | builder.Add(di.Def{ 80 | Name: UserController, 81 | Build: func(ctn di.Container) (interface{}, error) { 82 | return controller.NewUserController(ctn.Get(UserService).(service.UserServiceInterface), ctn.Get(Logger).(logger.LoggerInterface)), nil 83 | }, 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /dic/config.go: -------------------------------------------------------------------------------- 1 | package dic 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "os" 7 | ) 8 | 9 | func ReadConfig() { 10 | var err error 11 | 12 | viper.SetConfigFile("base.env") 13 | viper.SetConfigType("props") 14 | err = viper.ReadInConfig() 15 | if err != nil { 16 | fmt.Println(err) 17 | return 18 | } 19 | 20 | if _, err := os.Stat(".env"); os.IsNotExist(err) { 21 | fmt.Println("WARNING: file .env not found") 22 | } else { 23 | viper.SetConfigFile(".env") 24 | viper.SetConfigType("props") 25 | err = viper.MergeInConfig() 26 | if err != nil { 27 | fmt.Println(err) 28 | return 29 | } 30 | } 31 | 32 | // Override config parameters from environment variables if specified 33 | for _, key := range viper.AllKeys() { 34 | viper.BindEnv(key) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dic/migrations.go: -------------------------------------------------------------------------------- 1 | package dic 2 | 3 | import ( 4 | _ "github.com/lib/pq" // for migrations 5 | _ "github.com/steinbacher/goose" // for migrations 6 | "gorm.io/gorm" 7 | ) 8 | 9 | var DB *gorm.DB 10 | 11 | func init() { 12 | ReadConfig() 13 | InitContainer() 14 | DB = Container.Get(Db).(*gorm.DB) 15 | } 16 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | 3 | # code: 4 | # driver: local 5 | # driver_opts: 6 | # type: 'none' 7 | # o: 'bind' 8 | # device: $PWD 9 | 10 | db: {} 11 | 12 | services: 13 | 14 | go: 15 | container_name: go-api-boilerplate-go 16 | build: 17 | context: . 18 | dockerfile: docker/go/Dockerfile 19 | # volumes: 20 | # - code:/go/src/go-api-boilerplate 21 | ports: 22 | - 8080:80 23 | environment: 24 | DB_URL: host=db user=go-api-boilerplate dbname=go-api-boilerplate sslmode=disable password=go-api-boilerplate 25 | links: 26 | - db 27 | restart: unless-stopped 28 | 29 | db: 30 | container_name: go-api-boilerplate-db 31 | image: postgres 32 | volumes: 33 | - db:/var/lib/postgresql 34 | environment: 35 | POSTGRES_DB: go-api-boilerplate 36 | POSTGRES_USER: go-api-boilerplate 37 | POSTGRES_PASSWORD: go-api-boilerplate 38 | restart: unless-stopped 39 | -------------------------------------------------------------------------------- /docker/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 2 | 3 | WORKDIR /go/src/go-api-boilerplate 4 | 5 | RUN go mod init \ 6 | && go get github.com/steinbacher/goose/cmd/goose \ 7 | && go install -mod=mod github.com/steinbacher/goose/cmd/goose 8 | 9 | RUN apt update \ 10 | && apt install -y apt-transport-https gnupg curl debian-keyring debian-archive-keyring \ 11 | && curl -1sLf 'https://dl.cloudsmith.io/public/go-swagger/go-swagger/gpg.2F8CB673971B5C9E.key' | gpg --dearmor -o /usr/share/keyrings/go-swagger-go-swagger-archive-keyring.gpg \ 12 | && curl -1sLf 'https://dl.cloudsmith.io/public/go-swagger/go-swagger/config.deb.txt?distro=debian&codename=any-version' | tee /etc/apt/sources.list.d/go-swagger-go-swagger.list \ 13 | && apt update \ 14 | && apt install -y swagger 15 | 16 | COPY ./docker/go/entrypoint.sh ./docker/wait-for-it.sh /root/ 17 | RUN chmod 755 /root/entrypoint.sh /root/wait-for-it.sh 18 | 19 | COPY go.mod go.sum ./ 20 | RUN go mod vendor \ 21 | && go mod download 22 | 23 | # Project files 24 | COPY . . 25 | COPY ./docker/go/dbconf.yml ./db/dbconf.yml 26 | RUN touch .env 27 | 28 | RUN swagger generate spec -o doc/swagger.json 29 | RUN go mod vendor \ 30 | && go install 31 | 32 | ENTRYPOINT ["/root/entrypoint.sh"] 33 | -------------------------------------------------------------------------------- /docker/go/dbconf.yml: -------------------------------------------------------------------------------- 1 | development: 2 | driver: postgres 3 | # Must be specified but is not used in the code 4 | open: $DB_URL 5 | # Must be specified but is not used in the code 6 | dsn: $DB_URL 7 | -------------------------------------------------------------------------------- /docker/go/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | log() { 4 | echo -e "${NAMI_DEBUG:+${CYAN}${MODULE} ${MAGENTA}$(date "+%T.%2N ")}${RESET}${@}" >&2 5 | } 6 | 7 | setup_db() { 8 | log "Run migrations..." 9 | goose up 10 | log "Run seeders..." 11 | go-api-boilerplate seed 12 | } 13 | 14 | log "Waiting for Postgres..." 15 | /root/wait-for-it.sh db:5432 --timeout=180 -- echo "PostgreSQL started" 16 | 17 | setup_db 18 | 19 | log "Start server" 20 | go-api-boilerplate server 21 | -------------------------------------------------------------------------------- /docker/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | cmdname=$(basename $0) 5 | 6 | echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $TIMEOUT -gt 0 ]]; then 28 | echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" 29 | else 30 | echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" 31 | fi 32 | start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $ISBUSY -eq 1 ]]; then 36 | nc -z $HOST $PORT 37 | result=$? 38 | else 39 | (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 40 | result=$? 41 | fi 42 | if [[ $result -eq 0 ]]; then 43 | end_ts=$(date +%s) 44 | echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $QUIET -eq 1 ]]; then 56 | timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 57 | else 58 | timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 59 | fi 60 | PID=$! 61 | trap "kill -INT -$PID" INT 62 | wait $PID 63 | RESULT=$? 64 | if [[ $RESULT -ne 0 ]]; then 65 | echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" 66 | fi 67 | return $RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | hostport=(${1//:/ }) 76 | HOST=${hostport[0]} 77 | PORT=${hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | HOST="$2" 94 | if [[ $HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | PORT="$2" 103 | if [[ $PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | TIMEOUT="$2" 112 | if [[ $TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | CLI=("$@") 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$HOST" == "" || "$PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | TIMEOUT=${TIMEOUT:-15} 140 | STRICT=${STRICT:-0} 141 | CHILD=${CHILD:-0} 142 | QUIET=${QUIET:-0} 143 | 144 | # check to see if timeout is from busybox? 145 | # check to see if timeout is from busybox? 146 | TIMEOUT_PATH=$(realpath $(which timeout)) 147 | if [[ $TIMEOUT_PATH =~ "busybox" ]]; then 148 | ISBUSY=1 149 | BUSYTIMEFLAG="-t" 150 | else 151 | ISBUSY=0 152 | BUSYTIMEFLAG="" 153 | fi 154 | 155 | if [[ $CHILD -gt 0 ]]; then 156 | wait_for 157 | RESULT=$? 158 | exit $RESULT 159 | else 160 | if [[ $TIMEOUT -gt 0 ]]; then 161 | wait_for_wrapper 162 | RESULT=$? 163 | else 164 | wait_for 165 | RESULT=$? 166 | fi 167 | fi 168 | 169 | if [[ $CLI != "" ]]; then 170 | if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then 171 | echoerr "$cmdname: strict mode, refusing to execute subprocess" 172 | exit $RESULT 173 | fi 174 | exec "${CLI[@]}" 175 | else 176 | exit $RESULT 177 | fi -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zubroide/go-api-boilerplate 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/getsentry/sentry-go v0.31.1 7 | github.com/getsentry/sentry-go/gin v0.31.1 8 | github.com/getsentry/sentry-go/logrus v0.31.1 9 | github.com/gin-gonic/gin v1.10.0 10 | github.com/lib/pq v1.10.9 11 | github.com/pkg/errors v0.9.1 12 | github.com/sarulabs/di/v2 v2.4.0 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/spf13/cobra v1.8.0 15 | github.com/spf13/viper v1.18.2 16 | github.com/steinbacher/goose v0.0.0-20160725131629-dc457c319503 17 | github.com/swaggo/gin-swagger v1.2.0 18 | gorm.io/driver/postgres v1.5.11 19 | gorm.io/gorm v1.25.12 20 | ) 21 | 22 | require ( 23 | github.com/bytedance/sonic v1.11.6 // indirect 24 | github.com/bytedance/sonic/loader v0.1.1 // indirect 25 | github.com/cloudwego/base64x v0.1.4 // indirect 26 | github.com/cloudwego/iasm v0.2.0 // indirect 27 | github.com/fsnotify/fsnotify v1.7.0 // indirect 28 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 29 | github.com/gin-contrib/sse v0.1.0 // indirect 30 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 31 | github.com/go-openapi/jsonreference v0.21.0 // indirect 32 | github.com/go-openapi/spec v0.21.0 // indirect 33 | github.com/go-openapi/swag v0.23.0 // indirect 34 | github.com/go-playground/locales v0.14.1 // indirect 35 | github.com/go-playground/universal-translator v0.18.1 // indirect 36 | github.com/go-playground/validator/v10 v10.20.0 // indirect 37 | github.com/go-sql-driver/mysql v1.9.0 // indirect 38 | github.com/goccy/go-json v0.10.2 // indirect 39 | github.com/hashicorp/hcl v1.0.0 // indirect 40 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 41 | github.com/jackc/pgpassfile v1.0.0 // indirect 42 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 43 | github.com/jackc/pgx/v5 v5.5.5 // indirect 44 | github.com/jackc/puddle/v2 v2.2.1 // indirect 45 | github.com/jinzhu/inflection v1.0.0 // indirect 46 | github.com/jinzhu/now v1.1.5 // indirect 47 | github.com/josharian/intern v1.0.0 // indirect 48 | github.com/json-iterator/go v1.1.12 // indirect 49 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 50 | github.com/kylelemons/go-gypsy v1.0.0 // indirect 51 | github.com/leodido/go-urn v1.4.0 // indirect 52 | github.com/magiconair/properties v1.8.7 // indirect 53 | github.com/mailru/easyjson v0.7.7 // indirect 54 | github.com/mattn/go-isatty v0.0.20 // indirect 55 | github.com/mattn/go-sqlite3 v1.14.24 // indirect 56 | github.com/mitchellh/mapstructure v1.5.0 // indirect 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 58 | github.com/modern-go/reflect2 v1.0.2 // indirect 59 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 60 | github.com/rogpeppe/go-internal v1.14.1 // indirect 61 | github.com/sagikazarmark/locafero v0.4.0 // indirect 62 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 63 | github.com/sourcegraph/conc v0.3.0 // indirect 64 | github.com/spf13/afero v1.11.0 // indirect 65 | github.com/spf13/cast v1.6.0 // indirect 66 | github.com/spf13/pflag v1.0.5 // indirect 67 | github.com/subosito/gotenv v1.6.0 // indirect 68 | github.com/swaggo/swag v1.5.1 // indirect 69 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 70 | github.com/ugorji/go/codec v1.2.12 // indirect 71 | go.uber.org/multierr v1.11.0 // indirect 72 | golang.org/x/arch v0.8.0 // indirect 73 | golang.org/x/crypto v0.31.0 // indirect 74 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect 75 | golang.org/x/net v0.33.0 // indirect 76 | golang.org/x/sync v0.10.0 // indirect 77 | golang.org/x/sys v0.28.0 // indirect 78 | golang.org/x/text v0.21.0 // indirect 79 | golang.org/x/tools v0.26.0 // indirect 80 | google.golang.org/protobuf v1.34.1 // indirect 81 | gopkg.in/ini.v1 v1.67.0 // indirect 82 | gopkg.in/yaml.v3 v3.0.1 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 4 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 5 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 6 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 7 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 8 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 9 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 10 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 11 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 12 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 19 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 20 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 21 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 22 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 23 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 24 | github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= 25 | github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= 26 | github.com/getsentry/sentry-go/gin v0.31.1 h1:lvOOO5j0o0IhYIXoHCmQ+D4ExhXWRCnDusV176dXWDA= 27 | github.com/getsentry/sentry-go/gin v0.31.1/go.mod h1:iMF6gA5uO2t3KVMj4QpjLi9B0U+oMidAiHAdPcJMMdQ= 28 | github.com/getsentry/sentry-go/logrus v0.31.1 h1:561777lxQWZl4qpte67Wp4zQB7V1oNVriZNN5hiOooI= 29 | github.com/getsentry/sentry-go/logrus v0.31.1/go.mod h1:UHhxir22yymjeM8dBN0LM+7Z98+cYFEev8V3AV1+UYU= 30 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 31 | github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc= 32 | github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= 33 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 34 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 35 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 36 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 37 | github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 38 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 39 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 40 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 41 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 42 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 43 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 44 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 45 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 46 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 47 | github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 48 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 49 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 50 | github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 51 | github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= 52 | github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= 53 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 54 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 55 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 56 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 57 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 58 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 59 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 60 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 61 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 62 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 63 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 64 | github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= 65 | github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= 66 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 67 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 68 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 69 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 70 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 71 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 72 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 73 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 74 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 75 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 76 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 77 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 78 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 79 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 80 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 81 | github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= 82 | github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 83 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 84 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 85 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 86 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 87 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 88 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 89 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 90 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 91 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 92 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 93 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 94 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 95 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 96 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 97 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 98 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 99 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 100 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 101 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 102 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 103 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 104 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 105 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 106 | github.com/kylelemons/go-gypsy v1.0.0 h1:7/wQ7A3UL1bnqRMnZ6T8cwCOArfZCxFmb1iTxaOOo1s= 107 | github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU= 108 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 109 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 110 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 111 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 112 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 113 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 114 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 115 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 116 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 117 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 118 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 119 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 120 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 121 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 122 | github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 123 | github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 124 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 125 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 126 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 127 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 128 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 129 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 130 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 131 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 132 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 133 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 134 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 135 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 136 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 137 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 138 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 139 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 140 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 141 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 142 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 143 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 144 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 145 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 146 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 147 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 148 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 149 | github.com/sarulabs/di/v2 v2.4.0 h1:xL2sq0jbPML1y0wpFh5mC4ASYHAiAZodnUlFMDo9Wh0= 150 | github.com/sarulabs/di/v2 v2.4.0/go.mod h1:trZu4KPwNLE623mBIIsljn1LLkNE6ee/Pk24b7yzSf8= 151 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 152 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 153 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 154 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 155 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 156 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 157 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 158 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 159 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 160 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 161 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 162 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 163 | github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= 164 | github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 165 | github.com/steinbacher/goose v0.0.0-20160725131629-dc457c319503 h1:L8+Ik+llzGKYajBWy70/EdNOi+nSnrOGFW5Yv+mTPoQ= 166 | github.com/steinbacher/goose v0.0.0-20160725131629-dc457c319503/go.mod h1:XxCzGUzauB84rz5zIXTLKEtI+/E7hvjrcyFll12vWcc= 167 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 168 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 169 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 170 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 171 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 172 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 173 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 174 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 175 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 176 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 177 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 178 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 179 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 180 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 181 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 182 | github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0= 183 | github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= 184 | github.com/swaggo/swag v1.5.1 h1:2Agm8I4K5qb00620mHq0VJ05/KT4FtmALPIcQR9lEZM= 185 | github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= 186 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 187 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 188 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 189 | github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= 190 | github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 191 | github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= 192 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 193 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 194 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 195 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 196 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 197 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 198 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 199 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 200 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 201 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 202 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 203 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 204 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 205 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= 206 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 207 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 208 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 209 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 212 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 213 | golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 214 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 215 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 216 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 217 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 218 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 219 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 220 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 221 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 222 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 223 | golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 225 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 226 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 227 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 228 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 229 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 230 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 231 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 232 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 233 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 234 | golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 235 | golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 236 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 237 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 238 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 239 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 240 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 241 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 242 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 243 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 244 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 245 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 246 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 247 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 248 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 249 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 250 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 251 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 252 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 253 | gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= 254 | gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= 255 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 256 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 257 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 258 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 259 | -------------------------------------------------------------------------------- /install/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt install -y apt-transport-https gnupg curl debian-keyring debian-archive-keyring 4 | sudo curl -1sLf 'https://dl.cloudsmith.io/public/go-swagger/go-swagger/gpg.2F8CB673971B5C9E.key' | sudo gpg --dearmor -o /usr/share/keyrings/go-swagger-go-swagger-archive-keyring.gpg 5 | sudo curl -1sLf 'https://dl.cloudsmith.io/public/go-swagger/go-swagger/config.deb.txt?distro=debian&codename=any-version' | sudo tee /etc/apt/sources.list.d/go-swagger-go-swagger.list 6 | 7 | go get github.com/steinbacher/goose/cmd/goose 8 | go install -mod=mod github.com/steinbacher/goose/cmd/goose 9 | 10 | curl -sSfL https://raw.githubusercontent.com/air-verse/air/master/install.sh | sh -s 11 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/getsentry/sentry-go" 6 | sentrylogrus "github.com/getsentry/sentry-go/logrus" 7 | "github.com/sirupsen/logrus" 8 | "os" 9 | "runtime/debug" 10 | "time" 11 | ) 12 | 13 | type LoggerInterface interface { 14 | Debug(args ...interface{}) 15 | Debugf(format string, args ...interface{}) 16 | Info(args ...interface{}) 17 | Infof(format string, args ...interface{}) 18 | Warn(args ...interface{}) 19 | Warnf(format string, args ...interface{}) 20 | Error(args ...interface{}) 21 | Errorf(format string, args ...interface{}) 22 | Fatal(args ...interface{}) 23 | Fatalf(format string, args ...interface{}) 24 | Print(args ...interface{}) 25 | } 26 | 27 | func NewLogger(sentryHook *sentrylogrus.Hook, level logrus.Level, sentryTimeout int) LoggerInterface { 28 | logger := NewLoggerWithoutSentry(level) 29 | 30 | if sentryHook != nil { 31 | logger.AddHook(sentryHook) 32 | // Flushes before calling os.Exit(1) when using logger.Fatal 33 | // (else all defers are not called, and Sentry does not have time to send the event) 34 | logrus.RegisterExitHandler(func() { 35 | sentryHook.Flush(5 * time.Second) 36 | }) 37 | } 38 | 39 | return logger 40 | } 41 | 42 | func NewSentryHook(dsn string) (*sentrylogrus.Hook, error) { 43 | sentryLevels := []logrus.Level{ 44 | logrus.WarnLevel, 45 | logrus.ErrorLevel, 46 | logrus.FatalLevel, 47 | logrus.PanicLevel, 48 | } 49 | if dsn != "" { 50 | sentry.Init(sentry.ClientOptions{ 51 | Dsn: dsn, 52 | AttachStacktrace: true, 53 | }) 54 | } 55 | sentryHook, err := sentrylogrus.New(sentryLevels, sentry.ClientOptions{ 56 | Dsn: dsn, 57 | EnableTracing: true, 58 | AttachStacktrace: true, 59 | TracesSampleRate: 1.0, 60 | //Debug: true, 61 | }) 62 | if err != nil { 63 | panic(err) 64 | } 65 | defer sentryHook.Flush(5 * time.Second) 66 | 67 | return sentryHook, nil 68 | } 69 | 70 | func NewLoggerWithoutSentry(level logrus.Level) *logrus.Logger { 71 | logger := &logrus.Logger{ 72 | Out: os.Stdout, 73 | Formatter: &logrus.TextFormatter{ 74 | ForceColors: true, 75 | FullTimestamp: true, 76 | }, 77 | Hooks: make(logrus.LevelHooks), 78 | Level: level, 79 | } 80 | 81 | return logger 82 | } 83 | 84 | func RecoverPanic() { 85 | err := recover() 86 | 87 | if err != nil { 88 | fmt.Println(err) 89 | fmt.Println(string(debug.Stack())) 90 | sentry.CurrentHub().Recover(err) 91 | sentry.Flush(time.Second * 5) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/zubroide/go-api-boilerplate/command" 5 | "github.com/zubroide/go-api-boilerplate/dic" 6 | ) 7 | 8 | func main() { 9 | dic.ReadConfig() 10 | dic.InitContainer() 11 | command.Execute() 12 | } 13 | -------------------------------------------------------------------------------- /model/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "gorm.io/driver/postgres" 6 | _ "gorm.io/driver/postgres" 7 | "gorm.io/gorm" 8 | "gorm.io/gorm/logger" 9 | ) 10 | 11 | func NewDb() *gorm.DB { 12 | dsn := viper.GetString("DB_URL") 13 | 14 | logMode := logger.Silent 15 | if viper.GetBool("DB_LOG_MODE") { 16 | logMode = logger.Info 17 | } 18 | 19 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ 20 | Logger: logger.Default.LogMode(logMode), 21 | }) 22 | if err != nil { 23 | panic("failed to connect database") 24 | } 25 | 26 | sqlDB, err := db.DB() 27 | if err != nil { 28 | panic("failed to get database instance") 29 | } 30 | 31 | sqlDB.SetMaxIdleConns(viper.GetInt("DB_MAX_CONNECTIONS")) 32 | sqlDB.SetMaxOpenConns(viper.GetInt("DB_MAX_CONNECTIONS")) 33 | 34 | return db 35 | } 36 | -------------------------------------------------------------------------------- /model/entity/user.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type User struct { 8 | gorm.Model 9 | Name string `binding:"required" json:"name"` 10 | } 11 | -------------------------------------------------------------------------------- /model/service/user_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/zubroide/go-api-boilerplate/logger" 5 | "github.com/zubroide/go-api-boilerplate/model/entity" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type UserServiceInterface interface { 10 | GetUsers(string) ([]*entity.User, error) 11 | } 12 | 13 | type UserService struct { 14 | db *gorm.DB 15 | logger logger.LoggerInterface 16 | } 17 | 18 | func NewUserService(db *gorm.DB, logger logger.LoggerInterface) UserServiceInterface { 19 | service := &UserService{db, logger} 20 | return service 21 | } 22 | 23 | func (s *UserService) GetUsers(name string) ([]*entity.User, error) { 24 | var items []*entity.User 25 | res := s.db. 26 | Where("name ilike ?||'%'", name). 27 | Order("name"). 28 | Find(&items) 29 | if res.Error != nil { 30 | return nil, res.Error 31 | } 32 | return items, nil 33 | } 34 | -------------------------------------------------------------------------------- /route/api.go: -------------------------------------------------------------------------------- 1 | // Application 2 | // 3 | // Application description 4 | // 5 | // Schemes: http 6 | // Host: localhost:8080 7 | // BasePath: / 8 | // Version: 0.0.1 9 | // 10 | // Consumes: 11 | // - application/json 12 | // - application/xml 13 | // 14 | // Produces: 15 | // - application/json 16 | // - application/xml 17 | // 18 | // swagger:meta 19 | package route 20 | 21 | import ( 22 | sentrygin "github.com/getsentry/sentry-go/gin" 23 | "github.com/gin-gonic/gin" 24 | "github.com/sarulabs/di/v2" 25 | "github.com/spf13/viper" 26 | "github.com/swaggo/gin-swagger" 27 | "github.com/swaggo/gin-swagger/swaggerFiles" 28 | "github.com/zubroide/go-api-boilerplate/controller" 29 | "github.com/zubroide/go-api-boilerplate/dic" 30 | _ "github.com/zubroide/go-api-boilerplate/route/description" // For Swagger 31 | "net/http" 32 | "time" 33 | ) 34 | 35 | var db = make(map[string]string) 36 | 37 | func Setup(builder *di.Builder) *gin.Engine { 38 | gin.SetMode(viper.GetString("GIN_MODE")) 39 | 40 | r := gin.New() 41 | r.Use(gin.Recovery()) 42 | 43 | r.Use(sentrygin.New(sentrygin.Options{ 44 | Repanic: true, 45 | WaitForDelivery: true, 46 | Timeout: time.Second * 5, 47 | })) 48 | 49 | // Display Swagger documentation 50 | r.StaticFile("doc/swagger.json", "doc/swagger.json") 51 | config := &ginSwagger.Config{ 52 | URL: "/doc/swagger.json", //The url pointing to API definition 53 | } 54 | // use ginSwagger middleware to 55 | r.GET("/doc/swagger/*any", ginSwagger.CustomWrapHandler(config, swaggerFiles.Handler)) 56 | 57 | userController := dic.Container.Get(dic.UserController).(*controller.UserController) 58 | 59 | // swagger:route GET /ping common getPing 60 | // 61 | // Ping 62 | // 63 | // Get Ping and reply Pong 64 | // 65 | // Responses: 66 | // 200: 67 | r.GET("/ping", func(c *gin.Context) { 68 | c.String(http.StatusOK, "pong") 69 | }) 70 | 71 | // swagger:route GET /users user GetUsers 72 | // 73 | // Users list 74 | // 75 | // Get users list data 76 | // 77 | // Responses: 78 | // 200: UsersResponse 79 | r.GET("/users", userController.List) 80 | 81 | // Authorized group (uses gin.BasicAuth() middleware) 82 | // Same than: 83 | // authorized := r.Group("/") 84 | // authorized.Use(gin.BasicAuth(gin.Credentials{ 85 | // "foo": "bar", 86 | // "manu": "123", 87 | //})) 88 | authorized := r.Group("/", gin.BasicAuth(gin.Accounts{ 89 | "foo": "bar", // user:foo password:bar 90 | "manu": "123", // user:manu password:123 91 | })) 92 | 93 | authorized.POST("admin", func(c *gin.Context) { 94 | user := c.MustGet(gin.AuthUserKey).(string) 95 | 96 | // Parse JSON 97 | var json struct { 98 | Value string `json:"value" binding:"required"` 99 | } 100 | 101 | if c.Bind(&json) == nil { 102 | db[user] = json.Value 103 | c.JSON(http.StatusOK, gin.H{"status": "ok"}) 104 | } 105 | }) 106 | 107 | return r 108 | } 109 | -------------------------------------------------------------------------------- /route/description/base.go: -------------------------------------------------------------------------------- 1 | package description 2 | 3 | type BaseResponseBody struct { 4 | // Response status. 5 | // One of next values: 6 | // - ok, 7 | // - error 8 | Status string 9 | } 10 | -------------------------------------------------------------------------------- /route/description/user.go: -------------------------------------------------------------------------------- 1 | package description 2 | 3 | import ( 4 | "github.com/zubroide/go-api-boilerplate/controller" 5 | "github.com/zubroide/go-api-boilerplate/model/entity" 6 | ) 7 | 8 | // User data 9 | // swagger:response UserResponse 10 | type UserResponse struct { 11 | // in: body 12 | Body struct { 13 | Entity *entity.User 14 | *BaseResponseBody 15 | } 16 | } 17 | 18 | // Users data 19 | // swagger:response UsersResponse 20 | type UsersResponse struct { 21 | // in: body 22 | Body struct { 23 | Entities []*entity.User 24 | *BaseResponseBody 25 | } 26 | } 27 | 28 | // swagger:parameters CreateUser UpdateUser 29 | type UserParameters struct { 30 | // in: body 31 | Body *entity.User 32 | } 33 | 34 | // swagger:parameters GetUsers 35 | type UsersParameters struct { 36 | // in: query 37 | *controller.UserListParameters 38 | } 39 | --------------------------------------------------------------------------------