Hay varias cosas que puede hacer para siempre: mirar el fuego, corregir errores en el código heredado y, por supuesto, hablar sobre DI, y aún así, no, no, y encontrará dependencias extrañas en la próxima aplicación.
Sin embargo, en el contexto del lenguaje GO, la situación es un poco más complicada, ya que no existe un estándar explícito y ampliamente respaldado para trabajar con dependencias, y cada uno pedalea su propio patinete, lo que significa que hay algo que discutir y comparar.
En este artículo, discutiré las herramientas y enfoques más populares para organizar una jerarquía de dependencias en marcha, con sus ventajas y desventajas. Si conoce la teoría y la abreviatura DI no le genera ninguna pregunta (incluida la necesidad de aplicar este enfoque), entonces puede comenzar a leer el artículo desde el medio, en la primera mitad explicaré qué es DI, por qué es necesario en absoluto y en particular en th.
¿Por qué necesitamos todo esto?
Para empezar, el principal enemigo de todos los programadores y el principal motivo de la aparición de casi todas las herramientas de diseño es la complejidad. El caso trivial siempre es claro, cae fácilmente en la cabeza, se resuelve de manera obvia y elegante con una línea de código, y nunca hay problemas con él. Es un asunto diferente cuando el sistema tiene decenas y cientos de miles (y a veces más) líneas de código, y una gran cantidad de partes "móviles" que se entrelazan, interactúan y simplemente existen en un mundo pequeño donde parece imposible dar la vuelta sin tocar a alguien. luego los codos.
Para resolver el problema de la complejidad, la humanidad aún no ha encontrado una mejor manera que descomponer las cosas complejas en simples, aislarlas y considerarlas por separado.
La clave aquí es el aislamiento, siempre que un componente no afecte a los vecinos, no se pueden temer los efectos inesperados y la influencia implícita de uno sobre el resultado del segundo. Para asegurar este aislamiento, decidimos controlar las conexiones de cada componente, describiendo explícitamente qué y cómo depende.
En este punto, llegamos a la inyección de dependencias (o inyección), que en realidad es solo una forma de organizar el código para que cada componente (clase, estructura, módulo, etc.) tenga acceso solo a las partes de la aplicación que necesita, ocultando todo lo innecesario. por su trabajo o, para citar wikipedia: "DI es el proceso de proporcionar una dependencia externa a un componente de software".
Este enfoque resuelve varios problemas a la vez:
- Oculta lo innecesario, reduciendo la carga cognitiva del desarrollador;
- ( , );
- , , ;
DI
. :
- — : , , (, ), ;
- — ;
- — , , , .
— — DI , .
, (, DI) — , , , .
, DI ( , ), (DI-, , ), , , , - .
:
, , JSON’ , .
, :
- , , ;
- , ;
- ( ) ;
, ?
, , , internal server error? ( , , , , ?)
, / , ( - )?
: , , .
SIGINT, , , . "" , , Graceful shutdown.
, , , , , .
, , , DI:
- , , , , , , ;
- : , , ;
DI Java
, , - . , , .
, , - : , . : -, (, , - ), -, ( , ), " ", , ( ) .
, , , , , . , , .
.
, , . , , , .
https://github.com/vivid-money/article-golang-di.
, , Logger — , , DBConn , HTTPServer, , , () . , Logger->DBConn->HTTPServer, .
, DBConn ( DBConn.Connect()
), httpServer.Serve
, , .
Reflection based container
, https://github.com/uber-go/dig https://github.com/uber-go/fx.
, , . , :
// , - , .
logger := log.New(os.Stderr, "", 0)
logger.Print("Started")
container := dig.New() //
// .
// Dig , , .
_ = container.Provide(func() components.Logger {
logger.Print("Provided logger")
return logger // .
})
_ = container.Provide(components.NewDBConn)
_ = container.Provide(components.NewHTTPServer)
_ = container.Invoke(func(_ *components.HTTPServer) {
// HTTPServer, "" , .
logger.Print("Can work with HTTPServer")
// , .
})
/*
Output:
---
Started
Provided logger
New DBConn
New HTTPServer
Can work with HTTPServer
*/
fx :
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// , - ,
// .
logger := log.New(os.Stderr, "", 0)
logger.Print("Started")
// fx, "".
app := fx.New(
fx.Provide(func() components.Logger {
return logger // .
}),
fx.Provide(
func(logger components.Logger, lc fx.Lifecycle) *components.DBConn { // lc - .
conn := components.NewDBConn(logger)
// .
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
if err := conn.Connect(ctx); err != nil {
return fmt.Errorf("can't connect to db: %w", err)
}
return nil
},
OnStop: func(ctx context.Context) error {
return conn.Stop(ctx)
},
})
return conn
},
func(logger components.Logger, dbConn *components.DBConn, lc fx.Lifecycle) *components.HTTPServer {
s := components.NewHTTPServer(logger, dbConn)
lc.Append(fx.Hook{
OnStart: func(_ context.Context) error {
go func() {
defer cancel()
// , .. Serve - .
if err := s.Serve(context.Background()); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Print("Error: ", err)
}
}()
return nil
},
OnStop: func(ctx context.Context) error {
return s.Stop(ctx)
},
})
return s
},
),
fx.Invoke(
// - "", , .
func(*components.HTTPServer) {
go func() {
components.AwaitSignal(ctx) // , .
cancel()
}()
},
),
fx.NopLogger,
)
_ = app.Start(ctx)
<-ctx.Done() //
_ = app.Stop(context.Background())
/*
Output:
---
Started
New DBConn
New HTTPServer
Connecting DBConn
Connected DBConn
Serving HTTPServer
^CStop HTTPServer
Stopped HTTPServer
Stop DBConn
Stopped DBConn
*/
, Serve ( ListenAndServe) ? : (go blockingFunc()
), . , , , , .
fx, (fx.In
, fx.Out
) (optional
, name
), , , - .
, , , fx.Supply
, - , .
"" :
- , , , " ". , ;
- , - , ;
- , ;
- ;
- xml yaml;
:
- , ;
- , , compile-time — (, - ) , . , .
- fx:
-
- ( Serve ), , , ;
, go https://github.com/google/wire .
, , , . , , , , compile-time .
, , . , , , , — , . :
, .
- ( "" , ):
// +build wireinject
package main
import (
"context"
"github.com/google/wire"
"github.com/vivid-money/article-golang-di/pkg/components"
)
func initializeHTTPServer(
_ context.Context,
_ components.Logger,
closer func(), // ,
) (
res *components.HTTPServer,
cleanup func(), // ,
err error,
) {
wire.Build(
NewDBConn,
NewHTTPServer,
)
return &components.HTTPServer{}, nil, nil
}
, wire
( go generate
), wire , wire , :
func initializeHTTPServer(contextContext context.Context, logger components.Logger, closer func()) (*components.HTTPServer, func(), error) {
dbConn, cleanup, err := NewDBConn(contextContext, logger)
if err != nil {
return nil, nil, err
}
httpServer, cleanup2 := NewHTTPServer(contextContext, logger, dbConn, closer)
return httpServer, func() {
cleanup2()
cleanup()
}, nil
}
initializeHTTPServer
, "" :
package main
//go:generate wire
import (
"context"
"fmt"
"log"
"os"
"errors"
"net/http"
"github.com/vivid-money/article-golang-di/pkg/components"
)
// wire lifecycle (, Cleanup-),
// , ,
// cleanup- .
func NewDBConn(ctx context.Context, logger components.Logger) (*components.DBConn, func(), error) {
conn := components.NewDBConn(logger)
if err := conn.Connect(ctx); err != nil {
return nil, nil, fmt.Errorf("can't connect to db: %w", err)
}
return conn, func() {
if err := conn.Stop(context.Background()); err != nil {
logger.Print("Error trying to stop dbconn", err)
}
}, nil
}
func NewHTTPServer(
ctx context.Context,
logger components.Logger,
conn *components.DBConn,
closer func(),
) (*components.HTTPServer, func()) {
srv := components.NewHTTPServer(logger, conn)
go func() {
if err := srv.Serve(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Print("Error serving http: ", err)
}
closer()
}()
return srv, func() {
if err := srv.Stop(context.Background()); err != nil {
logger.Print("Error trying to stop http server", err)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// , - , .
logger := log.New(os.Stderr, "", 0)
logger.Print("Started")
// . "" ,
// Server' , cleanup-.
// .
lifecycleCtx, cancelLifecycle := context.WithCancel(context.Background())
defer cancelLifecycle()
// , Serve .
_, cleanup, _ := initializeHTTPServer(ctx, logger, func() {
cancelLifecycle()
})
defer cleanup()
go func() {
components.AwaitSignal(ctx) //
cancelLifecycle()
}()
<-lifecycleCtx.Done()
/*
Output:
---
New DBConn
Connecting DBConn
Connected DBConn
New HTTPServer
Serving HTTPServer
^CStop HTTPServer
Stopped HTTPServer
Stop DBConn
Stopped DBConn
*/
}
:
- ;
- ;
- ;
- ,
wire.Build
; - xml;
- Wire cleanup-, .
:
- , - ;
- , - ; , , , "" ;
- wire ( , ):
-
- , , ;
-
- , , / , , ;
-
- "" ;
-
- Cleanup' , , .
, , ( , ) . , , , wire dig/fx, , , ( ).
( - -- -), — .
, , :
logger := log.New(os.Stderr, "", 0)
dbConn := components.NewDBConn(logger)
httpServer := components.NewHTTPServer(logger, dbConn)
doSomething(httpServer)
errgroup.
:
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
logger := log.New(os.Stderr, "", 0)
logger.Print("Started")
g, gCtx := errgroup.WithContext(ctx)
dbConn := components.NewDBConn(logger)
g.Go(func() error {
// dbConn .
if err := dbConn.Connect(gCtx); err != nil {
return fmt.Errorf("can't connect to db: %w", err)
}
return nil
})
httpServer := components.NewHTTPServer(logger, dbConn)
g.Go(func() error {
go func() {
// , httpServer ( http.ListenAndServe, )
// , .
<-gCtx.Done()
if err := httpServer.Stop(context.Background()); err != nil {
logger.Print("Stopped http server with error:", err)
}
}()
if err := httpServer.Serve(gCtx); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("can't serve http: %w", err)
}
return nil
})
go func() {
components.AwaitSignal(gCtx)
cancel()
}()
_ = g.Wait()
/*
Output:
---
Started
New DBConn
New HTTPServer
Connecting DBConn
Connected DBConn
Serving HTTPServer
^CStop HTTPServer
Stop DBConn
Stopped DBConn
Stopped HTTPServer
Finished serving HTTPServer
*/
}
?
, , g, :
- ( );
- (
ctx.cancel
->gCtx.cancel
); - , — , gCtx .
, : errgroup . , gCtx .Done()
, cancel
, - (, ) .
:
- errgroup , ;
- errgroup , - . - , , , . , - , - , ?
— lifecycle.
, , : errgroup , , .
- :
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
logger := log.New(os.Stderr, "", 0)
logger.Print("Started")
lc := lifecycle.NewLifecycle()
dbConn := components.NewDBConn(logger)
lc.AddServer(func(ctx context.Context) error { //
return dbConn.Connect(ctx)
}).AddShutdowner(func(ctx context.Context) error {
return dbConn.Stop(ctx)
})
httpSrv := components.NewHTTPServer(logger, dbConn)
lc.Add(httpSrv) // httpSrv Server Shutdowner
go func() {
components.AwaitSignal(ctx)
lc.Stop(context.Background())
}()
_ = lc.Serve(ctx)
, , , , .
( lifecycle
, )
Java - , , , "" , .
, .
, , , - , , , , , .
, , "" , , , , ( ). , — main-.
, defer, , , .
, -, defer' return' , - (, ), -, . , , , :
a, err := NewA()
if err != nil {
panic("cant create a: " + err.Error())
}
go a.Serve()
defer a.Stop()
b, err := NewB(a)
if err != nil {
panic("cant create b: " + err.Error())
}
go b.Serve()
defer b.Stop()
/*
: A, B
: B, A
*/
, , ( , ). :
- ErrSet — / ;
- Serve — -server, server , WithCancel, -server' ( , server' );
- Shutdown — ErrSet, , - ;
, :
package main
import (
"context"
"fmt"
"log"
"os"
"errors"
"net/http"
"github.com/vivid-money/article-golang-di/pkg/components"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
logger := log.New(os.Stderr, "", 0)
logger.Print("Started")
go func() {
components.AwaitSignal(ctx)
cancel()
}()
errset := &ErrSet{}
errset.Add(runApp(ctx, logger, errset))
_ = errset.Error() //
/*
Output:
---
Started
New DBConn
Connecting DBConn
Connected DBConn
New HTTPServer
Serving HTTPServer
^CStop HTTPServer
Stop DBConn
Stopped DBConn
Stopped HTTPServer
Finished serving HTTPServer
*/
}
func runApp(ctx context.Context, logger components.Logger, errSet *ErrSet) error {
var err error
dbConn := components.NewDBConn(logger)
if err := dbConn.Connect(ctx); err != nil {
return fmt.Errorf("cant connect dbConn: %w", err)
}
defer Shutdown("dbConn", errSet, dbConn.Stop)
httpServer := components.NewHTTPServer(logger, dbConn)
if ctx, err = Serve(ctx, "httpServer", errSet, httpServer.Serve); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("cant serve httpServer: %w", err)
}
defer Shutdown("httpServer", errSet, httpServer.Stop)
components.AwaitSignal(ctx)
return ctx.Err()
}
, , , .
?
- , New-Serve-defer-Shutdown ( , , , );
- , , , ;
- ;
- ( ) ;
- , , ;
- 100% , , ;
- , , ;
- , ;
.
, , golang.
fx ( go), , — .
Wire , .
( , ) , go
, context
, defer
.
, , , . , wire (, , ).