Golang. Архітектура

Усім привіт!

Тривалий час пишу на golang, але маю великий досвід з Java Spring Boot.
Звичайно — це різні речі але... Коли мова щодо архітектури то вже дуже багато надивився
на «restful за 5 хвилин» — і це напрягає бо звичайний блог може і буде працювати з таким підходом,

а от коли мова вже про highload або про правильну та зручну архітектуру — то вже є питання без відповіді як на мене.

Тобто, хто у темі. Трохи соурса на розглянути та розсудити по суті.
Не буду приводити страшних діаграм або теоритичних викладів як у тому універі про який вже забув,
просто приведу купу соурса — а кому буде до вподоби, то увесь архів з ідеєю ви можете безкоштовно завантажити
з мого github: github.com/basilex/brickbang

Там до речі ще є цікавий kotlin restful про теж саме github.com/basilex/sandbrick, але зараз не про це

Як би то мовити щодо розкрутки restful api сервіса уцілому...?
Як би то зруйнувати functional підхід та зробити test-oriented платформу?
Як би то зробити аналог DI та користуватися ним дуже зручно у golang межах?
Як зробити слайс(пиріг) орієнтовну базу щоби розширяти можна було безболісно та швидко?
Як юзати базу без задовбаних gorm та подібних непотрібів (моя власна думка звичайно)?
Як зробити cache орієнтовний підхід для VERY швидкоі обробки tokens щоб головну базу не тормозило?
Як збудувати увесь пиріг з смаколиками та кожному віддати свою роль що робе іі незалежно від інших?
Як боротися із sessions якщо ваш клієнт web орієнтований?
Як роздавати сервіс для інших сервісів?

Оці питання мене вже понад декілька років більш ніж турбують — просто напружують.

Ну та ось маленький вкид що може комусь та зайде))

Ідея по суті не нова але config — controller — service — repository це аля spring boot
Тут сміятися не треба — слоі мають дуже велике значення для архітектури уцілому як на мене
До речі, DI, котрий у go намастурбатили наприклад у fx ну дуже похвальна річ... Але. Не Go!

Коротше. База нижче, деталі — github.com/basilex/brickbang

+++ cmd/bootstrap.go

package cmd
import (
“context”
“log/slog”
“os”
“os/signal”
“runtime”
“syscall”
“time”
“github.com/gofiber/fiber/v2”
“github.com/gofiber/fiber/v2/middleware/cors”
“brickbang/internal”
“brickbang/internal/config”
“brickbang/internal/middleware”
“brickbang/internal/utility”
)
// Timeouts are defined
// for startup and shutdown processes timeouts
const (
startupTimeout = 500 * time.Millisecond
shutdownTimeout = 5 * time.Second
)
// ServerPrefork sets GOMAXPROCS based on the configured child process count.
func ServerPrefork(maxprocs int) int {
if maxprocs <= 0 || maxprocs > runtime.NumCPU() {
maxprocs = runtime.NumCPU()
}
runtime.GOMAXPROCS(maxprocs)
return maxprocs
}
// Run initializes the Fiber server, dependencies, routes, and starts listening
func Run() {
cfg := config.Get()
maxprocs := ServerPrefork(cfg.ServerChildProcesses)
// ===== Fiber app setup =====
app := fiber.New(fiber.Config{
Prefork: true,
DisableStartupMessage: true,
ReadTimeout: cfg.ServerReadTimeout,
WriteTimeout: cfg.ServerWriteTimeout,
BodyLimit: cfg.ServerMaxHeaderBytes,
ErrorHandler: middleware.ErrorHandler,
})
// CORS middleware
if cfg.CORSEnabled {
app.Use(cors.New(cors.Config{
AllowOrigins: cfg.CORSAllowOrigin,
AllowMethods: cfg.CORSAllowMethods,
AllowHeaders: cfg.CORSAllowHeaders,
AllowCredentials: cfg.CORSAllowCredentials,
ExposeHeaders: cfg.CORSExposeHeaders,
MaxAge: int(cfg.CORSMaxAge.Seconds()),
}))
}
// Unified response middleware
app.Use(middleware.UnifiedResponse())
// Initialize dependencies and routes
container := internal.NewContainer()
internal.NewRegistrar(app, container).RegisterAll().Finalize()
// Signal handling for graceful shutdown
ctx, stop := signal.NotifyContext(
context.Background(),
os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT,
)
defer stop()
// Channel to capture server errors
serverErr := make(chan error, 1)
// Start Fiber server in goroutine
go func() {
serverErr <- app.Listen(cfg.ServerAddress)
}()
// Master vs Child process logging
if fiber.IsChild() {
slog.Info(
“BrickBang child process started”, “pid”, os.Getpid(), “env”, cfg.Env,
)
} else {
// Master process logs initial info
slog.Info("BrickBang server starting...“)
if cfg.Env == “dev” && !fiber.IsChild() {
slog.Info("BrickBang server settings:“,
“prefork”, true,
“maxproc”, maxprocs,
“address”, cfg.ServerAddress,
)
slog.Info("BrickBang server metadata:“,
“version”, internal.Version,
“staging”, internal.Staging,
“githash”, internal.Githash,
“gobuild”, internal.Gobuild,
“compile”, internal.Compile,
)
utility.InspectRoutes(app, false)
}
// Wait a short period to ensure all children started
time.Sleep(startupTimeout)
slog.Info("BrickBang server started“, “address”, cfg.ServerAddress)
}
// Wait for shutdown or server error
select {
case <-ctx.Done():
slog.Info("Received shutdown signal, exiting gracefully...")
// Shutdown Fiber with timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := app.ShutdownWithContext(shutdownCtx); err != nil {
slog.Error("Fiber shutdown error“, “error”, err)
}
// Close DB pool if exists
if container.DBPool != nil {
container.DBPool.Close()
slog.Info("Database pool closed")
}
case err := <-serverErr:
if err != nil {
slog.Error("Server stopped unexpectedly“, “error”, err)
}
}
slog.Info("BrickBang server stopped“)
}
+++ internal/container.go
package internal
import (
“context”
“log/slog”
“os”
“github.com/jackc/pgx/v5/pgxpool”
“brickbang/internal/config”
“brickbang/internal/module”
“brickbang/internal/repository/dbs”
)
// Container holds all dependencies of the application.
// It provides access to shared resources such as database pool and controllers.
type Container struct {
DBPool *pgxpool.Pool
Metadata map[string]string
// Modules (facades)
AuxModule module.IAuxModule
AuthModule module.IAuthModule
RoleModule module.IRoleModule
}
// NewContainer initializes and wires up all application dependencies.
// It follows the dependency injection pattern by creating instances in the correct order:
// Config → Database → Repository → Service → Controller.
func NewContainer() *Container {
cfg := config.Get()
ctx := context.Background()
// Initialize PostgreSQL connection pool
dbPool, err := pgxpool.New(ctx, cfg.DatabaseDSN)
if err != nil {
slog.Error("Container“, “failed to initialize database pool”, err)
os.Exit(1)
}
// Initialize metadata
// and generated SQL queries wrapper (from sqlc)
metadata := Metadata()
queries := dbs.New(dbPool)
// Initialize modules
auxModule := module.NewAuxModule(metadata)
authModule := module.NewAuthModule(queries)
roleModule := module.NewRoleModule(queries)
// Return a fully initialized dependency container
return &Container{
DBPool: dbPool,
Metadata: metadata,
AuxModule: auxModule,
AuthModule: authModule,
RoleModule: roleModule,
}
}
+++ internal/registrar.go
package internal
import (
“log/slog”
“github.com/gofiber/fiber/v2”
“brickbang/internal/middleware”
)
// Registrar manages the registration of application routes.
// It supports a chain-style grouping and automatic controller registration.
type Registrar struct {
container *Container
app *fiber.App
base fiber.Router // base group /api/v1
current fiber.Router // currently active group
path string // for logging
}
// NewRegistrar initializes the base API structure: /api/v1
func NewRegistrar(app *fiber.App, container *Container) *Registrar {
api := app.Group("/api")
v1 := api.Group("/v1“)
return &Registrar{
app: app,
container: container,
path: “/api/v1”,
base: v1,
current: v1,
}
}
// WithGroup creates a new route group from the base level (/api/v1)
// with optional middleware.
func (rcv *Registrar) WithGroup(prefix string, middlewares ...fiber.Handler) *Registrar {
group := rcv.base.Group(prefix, middlewares...)
slog.Debug("Route group registered“, “path”, rcv.path+prefix)
rcv.current = group
return rcv
}
// WithPublic defines a public route group (no middleware)
func (rcv *Registrar) WithPublic(prefix string) *Registrar {
return rcv.WithGroup(prefix)
}
// WithPrivate defines a protected route group (Auth + RBAC)
func (rcv *Registrar) WithPrivate(prefix string) *Registrar {
return rcv.WithGroup(prefix, middleware.AuthMiddleware, middleware.RBACMiddleware)
}
// WithAdmin defines an administrative route group
func (rcv *Registrar) WithAdmin(prefix string) *Registrar {
return rcv.WithGroup(prefix+"/admin", middleware.AuthMiddleware, middleware.RBACMiddleware)
}
func (rcv *Registrar) RegisterAll() *Registrar {
// Admin routes
// ...
// Public routes
rcv.WithPublic("/aux").registerAuxRoutes()
rcv.WithPublic("/auth").registerAuthPublicRoutes()
// Private routes
rcv.WithPrivate("/auth").registerAuthPrivateRoutes()
rcv.WithPrivate("/roles").registerRolesRoutes()
return rcv
}
func (rcv *Registrar) registerAuxRoutes() *Registrar {
rcv.container.AuxModule.Controller().RegisterRoutes(rcv.current)
return rcv
}
func (rcv *Registrar) registerRolesRoutes() *Registrar {
rcv.container.RoleModule.Controller().RegisterRoutes(rcv.current)
return rcv
}
func (rcv *Registrar) registerAuthPublicRoutes() *Registrar {
rcv.container.AuthModule.Controller().RegisterPublicRoutes(rcv.current)
return rcv
}
func (rcv *Registrar) registerAuthPrivateRoutes() *Registrar {
rcv.container.AuthModule.Controller().RegisterPrivateRoutes(rcv.current)
return rcv
}
func (rcv *Registrar) Finalize() {
// slog.Info("Routes registration completed“, “base_path”, rcv.path)
}
+++ service/aux_service.go
package service
import (
“time”
)
type IAuxService interface {
Health() map[string]string
Uptime() map[string]string
Metadata() map[string]string
}
type auxService struct {
metadata map[string]string
startTime time.Time
}
func NewAuxService(meta map[string]string) IAuxService {
return &auxService{
metadata: meta,
startTime: time.Now(),
}
}
func (rcv *auxService) Health() map[string]string {
return map[string]string{"status“: “ok”}
}
func (rcv *auxService) Uptime() map[string]string {
return map[string]string{
“uptime”: time.Since(rcv.startTime).String(),
}
}
func (rcv *auxService) Metadata() map[string]string {
return rcv.metadata
}

+++ controller/aux_controller.go

package controller
import (
“brickbang/internal/service”
“github.com/gofiber/fiber/v2”
)
type IAuxController interface {
RegisterRoutes(router fiber.Router)
}
type AuxController struct {
service service.IAuxService
}
func NewAuxController(service service.IAuxService) IAuxController {
return &AuxController{service: service}
}
func (rcv *AuxController) RegisterRoutes(router fiber.Router) {
router.Get("/health", rcv.health)
router.Get("/uptime", rcv.uptime)
router.Get("/metadata“, rcv.metadata)
}
func (rcv *AuxController) health(ctx *fiber.Ctx) error {
return ctx.JSON(rcv.service.Health())
}
func (rcv *AuxController) uptime(ctx *fiber.Ctx) error {
return ctx.JSON(rcv.service.Uptime())
}
func (rcv *AuxController) metadata(ctx *fiber.Ctx) error {
return ctx.JSON(rcv.service.Metadata())
}
+++ config/config_service.go

package config
import (
“log/slog”
“os”
“path/filepath”
“runtime”
“strconv”
“sync”
“time”
“github.com/joho/godotenv”
)
type Config struct {
Env string
DatabaseDSN string
ServerAddress string
ServerReadTimeout time.Duration
ServerWriteTimeout time.Duration
ServerGracefulTimeout time.Duration
ServerMaxHeaderBytes int
ServerChildProcesses int
CORSEnabled bool
CORSAllowOrigin string
CORSAllowMethods string
CORSAllowHeaders string
CORSAllowCredentials bool
CORSExposeHeaders string
CORSMaxAge time.Duration
TLSEnabled bool
TLSCertFile string
TLSKeyFile string
JWTSecret string
JWTAccessExpiration time.Duration
JWTRefreshExpiration time.Duration
}
var (
cfg *Config
once sync.Once
)
func Init(env string) *Config {
once.Do(func() {
if env == “" {
env = “dev”
}
cfg = &Config{Env: env}
envFile := filepath.Join(".env." + env)
if err := godotenv.Load(envFile); err != nil {
slog.Warn("Failed to load .env file, fallback to system env“, “file”, envFile, “err”, err)
}
cfg.DatabaseDSN = getEnv("DATABASE_DSN“, “postgres://postgres:password@postgres:5432/brickbang_dev?sslmode=disable”)
cfg.ServerAddress = getEnv("SERVER_ADDRESS“, “0.0.0.0:8080”)
cfg.ServerReadTimeout = parseDuration(getEnv("SERVER_READ_TIMEOUT“, “3s”))
cfg.ServerWriteTimeout = parseDuration(getEnv("SERVER_WRITE_TIMEOUT“, “3s”))
cfg.ServerGracefulTimeout = parseDuration(getEnv("SERVER_GRACEFUL_TIMEOUT“, “5s”))
cfg.ServerMaxHeaderBytes = parseInt(getEnv("SERVER_MAX_HEADER_BYTES“, “1048576”))
cfg.ServerChildProcesses = parseInt(getEnv("SERVER_CHILD_PROCESSES", strconv.Itoa(runtime.NumCPU())))
cfg.CORSEnabled = parseBool(getEnv("CORS_ENABLED“, “true”))
cfg.CORSAllowOrigin = getEnv("CORS_ALLOW_ORIGIN“, “*”)
cfg.CORSAllowMethods = getEnv("CORS_ALLOW_METHODS“, “GET,POST,PUT,PATCH,DELETE,OPTIONS”)
cfg.CORSAllowHeaders = getEnv("CORS_ALLOW_HEADERS“, “Accept,Authorization,Content-Type,X-CSRF-Token”)
cfg.CORSAllowCredentials = parseBool(getEnv("CORS_ALLOW_CREDENTIALS“, “false”))
cfg.CORSExposeHeaders = getEnv("CORS_EXPOSE_HEADERS“, “*”)
cfg.CORSMaxAge = parseDuration(getEnv("CORS_MAX_AGE“, “10m”))
cfg.TLSEnabled = parseBool(getEnv("TLS_ENABLED“, “false”))
cfg.TLSCertFile = getEnv("TLS_CERT_FILE“, “./cert/server.crt”)
cfg.TLSKeyFile = getEnv("TLS_KEY_FILE“, “./cert/server.key”)
cfg.JWTSecret = getEnv("JWT_SECRET“, “devsecret”)
cfg.JWTAccessExpiration = parseDuration(getEnv("JWT_ACCESS_EXPIRATION“, “15m”))
cfg.JWTRefreshExpiration = parseDuration(getEnv("JWT_REFRESH_EXPIRATION“, “24h”))
})
return cfg
}
func Get() *Config {
if cfg == nil {
return Init("") // fallback: dev
}
return cfg
}
func getEnv(key, def string) string {
if v := os.Getenv(key); v != “" {
return v
}
return def
}
func parseBool(s string) bool {
return s == “true” || s == “1”
}
func parseInt(s string) int {
n, _ := strconv.Atoi(s)
return n
}
func parseDuration(s string) time.Duration {
d, _ := time.ParseDuration(s)
return d
}

Тобто. На github.com/basilex/brickbang усе уцілому — а тут тільки відправні моменти

З повагою до всіх

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Будь ласка, відформатуй код і налаштуй підсвітку, щоб код став читабельним

// Initialize metadata
// and generated SQL queries wrapper (from sqlc)

Я вперше дізнався про sqlc на проєкті, який налаштовував Олександр, а за рік до цього шукав щось схоже на sqlc.

Обидно, когда такой роскошный язык, как Go воспринимают как новый PHP.

голанд придуман только потому что у гугла мания придумывать новые языки у которых даже нет своей ниши. как у того же дартса.
для серверных решений лучше подходить java или core.net
для системных утилит — rust
golan это ни туда ни сюда

Golang на бекенди, у dart/flutter інша ніша, джавою гарна можлива опція прибити залізку, раст для систулз поки зарано стверджувати, і т.д.

А лучше чтоб все было на пхп было написано, открыл любую тулзу или ядро линукса, а там один и тот же язык, а не зоопарк который у каждого свой самый лучший

Підписатись на коментарі