first commit
This commit is contained in:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
28
Makefile
Normal file
28
Makefile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
.PHONY: *
|
||||||
|
|
||||||
|
help: ## This help dialog.
|
||||||
|
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2 | "sort -u"}' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
run: ## Start the application
|
||||||
|
docker compose up --build --remove-orphans
|
||||||
|
|
||||||
|
down: ## Stop the application
|
||||||
|
docker compose down --remove-orphans
|
||||||
|
|
||||||
|
setup-local: copy-config install-dependencies run ## Sets up your local environment
|
||||||
|
|
||||||
|
install-lint: ## Install Go lint
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
|
install-dependencies: ## Install go project dependencies
|
||||||
|
go mod download
|
||||||
|
go mod vendor
|
||||||
|
|
||||||
|
copy-config: ## Copy config file
|
||||||
|
cp .env.dist .env
|
||||||
|
|
||||||
|
lint: install-lint ## Run lint
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
tests: down ## Run tests
|
||||||
|
go test -v ./...
|
||||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Go boilerplate project
|
||||||
|
|
||||||
|
### A straightforward Go boilerplate project equipped with essential definitions to kickstart your project.
|
||||||
|
|
||||||
|
## How to run the application
|
||||||
|
1 - Copy the .env.dist to .env (you can edit the values if you want)
|
||||||
|
|
||||||
|
`$ cp .env.dist .env`
|
||||||
|
|
||||||
|
2 - Run make command, it will show the list of commands we have to make easier to start the application.
|
||||||
|
|
||||||
|
`$ make`
|
||||||
|
|
||||||
|
3 - Start the application
|
||||||
|
|
||||||
|
`$ make setup-local`
|
||||||
|
|
||||||
|
4 - Run the tests
|
||||||
|
|
||||||
|
`$ make tests`
|
||||||
|
|
||||||
|
5 - Run the lint
|
||||||
|
|
||||||
|
`$ make lint`
|
||||||
|
|
||||||
|
6 - Run all the validations (lint + tests)
|
||||||
|
|
||||||
|
`$ make validation`
|
||||||
44
cmd/app/main.go
Normal file
44
cmd/app/main.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/felipecoppola/go-boilerplate/internal/config"
|
||||||
|
"github.com/felipecoppola/go-boilerplate/internal/infrastructure/os"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Globals that are set at build time.
|
||||||
|
var (
|
||||||
|
appName = ""
|
||||||
|
appVersion = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
defer func() { _ = zap.L().Sync() }()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
logger := zap.L().With(
|
||||||
|
zap.String("app_name", appName),
|
||||||
|
zap.String("app_version", appVersion),
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg, err := config.NewConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error creating config: %w", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("starting app")
|
||||||
|
os.SignalListener(logger, cancel)
|
||||||
|
|
||||||
|
if err = run(ctx, cfg); err != nil {
|
||||||
|
logger.Error("command failed: %w", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("command completed")
|
||||||
|
}
|
||||||
29
cmd/app/service.go
Normal file
29
cmd/app/service.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/kjannette/go-http-server/cmd/app/handler/httpjson"
|
||||||
|
"github.com/kjannnette/go-http-server/internal/config"
|
||||||
|
"github.com/kjannette/go-http-server/pkg/logger"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func run(ctx context.Context, cfg *config.Config) error {
|
||||||
|
lg := zap.L().With(zap.String("app_name", appName), zap.String("app_version", appVersion))
|
||||||
|
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
app, h := httpjson.New(lg)
|
||||||
|
|
||||||
|
eg.Go(func(l logger.Logger) func() error {
|
||||||
|
return func() error {
|
||||||
|
l.Info("starting server")
|
||||||
|
defer l.Info("terminated server")
|
||||||
|
return h.RunServer(ctx, cfg, app)
|
||||||
|
}
|
||||||
|
}(lg.Named("run_server")))
|
||||||
|
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
||||||
56
cmd/handler/httpjson/handler.go
Normal file
56
cmd/handler/httpjson/handler.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package httpjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kjannette/go-http-server/internal/config"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler has services that are required by the handler.
|
||||||
|
type Handler struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Handler instance.
|
||||||
|
func New(logger *zap.Logger) (*echo.Echo, Handler) {
|
||||||
|
h := Handler{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
h.setupHealthCheckRoutes(e)
|
||||||
|
|
||||||
|
return e, h
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunServer runs http server and listens until context is canceled.
|
||||||
|
func (h *Handler) RunServer(ctx context.Context, conf *config.Config, e *echo.Echo) error {
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
timeout, cancel := context.WithTimeout(context.Background(), time.Duration(conf.API.ReadTimeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_ = e.Shutdown(timeout) // try gracefully first
|
||||||
|
_ = e.Close() // immediate termination
|
||||||
|
}()
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", conf.API.Port),
|
||||||
|
ReadTimeout: time.Duration(conf.API.ReadTimeout) * time.Second,
|
||||||
|
WriteTimeout: time.Duration(conf.API.WriteTimeout) * time.Second,
|
||||||
|
IdleTimeout: time.Duration(conf.API.IdleTimeout) * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.StartServer(server); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return errors.Wrap(err, "server error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
15
cmd/handler/httpjson/health.go
Normal file
15
cmd/handler/httpjson/health.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package httpjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) setupHealthCheckRoutes(app *echo.Echo) {
|
||||||
|
app.GET("/health", h.healthCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) healthCheck(c echo.Context) error {
|
||||||
|
return c.String(http.StatusOK, "OK")
|
||||||
|
}
|
||||||
17
cmd/main.go
Normal file
17
cmd/main.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/kjannette/go-server/internal/config"
|
||||||
|
"github.com/kjannette/go-server/internal/infrastructure/os"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
appName = ""
|
||||||
|
appVarsion = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
defer func() {_ = zap.L().Sync() }()
|
||||||
|
}
|
||||||
0
cmd/service.go
Normal file
0
cmd/service.go
Normal file
29
internal/config/config.go
Normal file
29
internal/config/config.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application configuration.
|
||||||
|
type Config struct {
|
||||||
|
API struct {
|
||||||
|
Port int `envconfig:"API_PORT" default:"3000"`
|
||||||
|
ReadTimeout int `envconfig:"API_READ_TIMEOUT" default:"7"`
|
||||||
|
WriteTimeout int `envconfig:"API_WRITE_TIMEOUT" default:"5"`
|
||||||
|
IdleTimeout int `envconfig:"API_IDLE_TIMEOUT" default:"5"`
|
||||||
|
Timeout int `envconfig:"API_TIMEOUT" default:"5"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig creates a new initialised application Config.
|
||||||
|
func NewConfig() (*Config, error) {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
if err := envconfig.Process("", &config); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
0
internal/domain/.gitkeep
Normal file
0
internal/domain/.gitkeep
Normal file
29
internal/infrastructure/os/signal.go
Normal file
29
internal/infrastructure/os/signal.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application configuration.
|
||||||
|
type Config struct {
|
||||||
|
API struct {
|
||||||
|
Port int `envconfig:"API_PORT" default:"3000"`
|
||||||
|
ReadTimeout int `envconfig:"API_READ_TIMEOUT" default:"7"`
|
||||||
|
WriteTimeout int `envconfig:"API_WRITE_TIMEOUT" default:"5"`
|
||||||
|
IdleTimeout int `envconfig:"API_IDLE_TIMEOUT" default:"5"`
|
||||||
|
Timeout int `envconfig:"API_TIMEOUT" default:"5"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig creates a new initialised application Config.
|
||||||
|
func NewConfig() (*Config, error) {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
if err := envconfig.Process("", &config); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
53
pkg/logger/logger.go
Normal file
53
pkg/logger/logger.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.go/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Debug(msg string, fields ...zap.Field)
|
||||||
|
Info(msg string, fields ...zap.Field)
|
||||||
|
Warn(msg string, fields ...zap.Field)
|
||||||
|
Error(msg string, fields ...zap.Field)
|
||||||
|
With(fields ...zap.Field) *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a function to provide the Logger creation with extra initialisation options.
|
||||||
|
type Option func(*zap.Config)
|
||||||
|
|
||||||
|
// init initializes zap's global loggers (i.e. zap.L() and zap.S()).
|
||||||
|
// Log level is retrieved from environment variable LOG_LEVEL, defaulting to "info" otherwise.
|
||||||
|
// To activate this facility, add a blank import to this package.
|
||||||
|
func init() {
|
||||||
|
level := os.Getenv("LOG_LEVEL")
|
||||||
|
if level == "" {
|
||||||
|
level = "info"
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeZapGlobals(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeZapGlobals(level string) {
|
||||||
|
var zapLevel zapcore.Level
|
||||||
|
if err := zapLevel.UnmarshalText([]byte(level)); err != nil {
|
||||||
|
log.Fatalf("failed to parse logger level: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("setting logging level to '%s'.\n", zapLevel.String())
|
||||||
|
|
||||||
|
cfg := zap.NewProductionConfig()
|
||||||
|
cfg.Level = zap.NewAtomicLevelAt(zapLevel)
|
||||||
|
cfg.OutputPaths = []string{"stdout"}
|
||||||
|
cfg.ErrorOutputPaths = []string{"stderr"}
|
||||||
|
cfg.EncoderConfig.EncodeTime = zapcore.RFC3339NanoTimeEncoder
|
||||||
|
|
||||||
|
logger, err := cfg.Build()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to initialize logger: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.ReplaceGlobals(logger)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user