Il y a plusieurs choses que vous pouvez faire pour toujours: regarder le feu, corriger les bogues dans le code hérité et, bien sûr, parler de DI - et toujours non, non, et vous rencontrerez d'étranges dépendances dans la prochaine application.
Dans le contexte du langage GO, cependant, la situation est un peu plus compliquée, car il n'y a pas de norme explicite et universellement prise en charge pour travailler avec les dépendances, et chacun pédale son propre petit scooter - ce qui signifie qu'il y a quelque chose à discuter et à comparer.
Dans cet article, je discuterai des outils et des approches les plus populaires pour organiser une hiérarchie de dépendances dans go, avec leurs avantages et leurs inconvénients. Si vous connaissez la théorie et que l'abréviation DI ne vous pose aucune question (y compris la nécessité d'appliquer cette approche), alors vous pouvez commencer à lire l'article par le milieu, dans la première moitié, j'expliquerai ce qu'est DI, pourquoi elle est nécessaire en général et en en particulier dans th.
Pourquoi avons-nous besoin de tout cela
Pour commencer, le principal ennemi de tous les programmeurs et la principale raison de l'émergence de presque tous les outils de conception est la complexité. Le cas trivial est toujours clair, tombe facilement dans la tête, est évidemment et gracieusement résolu avec une ligne de code, et il n'y a jamais de problèmes avec cela. C'est une question différente lorsque le système a des dizaines et des centaines de milliers (et parfois plus) de lignes de code, et un grand nombre de pièces «en mouvement» qui s'entrelacent, interagissent et n'existent que dans un petit monde où il semble impossible de se retourner sans toucher quelqu'un. puis les coudes.
Pour résoudre le problème de la complexité, l'humanité n'a pas encore trouvé de meilleur moyen que de diviser les choses complexes en choses simples, de les isoler et de les considérer séparément.
La clé ici est l'isolement, tant qu'un composant n'affecte pas les composants voisins, vous ne pouvez pas avoir peur des effets inattendus et de l'influence implicite de l'un sur le résultat du second. Pour assurer cet isolement, nous décidons de contrôler les connexions de chaque composant, en décrivant explicitement de quoi il dépend et comment.
À ce stade, nous arrivons à l'injection de dépendances (ou injection), qui n'est en réalité qu'un moyen d'organiser le code afin que chaque composant (classe, structure, module, etc.) n'ait accès qu'aux parties de l'application dont il a besoin, en lui cachant tout ce qui est inutile. pour son travail ou, pour citer wikipedia: "DI est le processus de fourniture d'une dépendance externe à un composant logiciel."
Cette approche résout plusieurs problèmes à la fois:
- Cache les inutiles, réduisant la charge cognitive du développeur;
- ( , );
- , , ;
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 (, , ).