永遠にできることがいくつかあります。火を見て、レガシーコードのバグを修正し、もちろんDIについて話します。それでも、いいえ、いいえ、次のアプリケーションで奇妙な依存関係に遭遇します。
ただし、GO言語のコンテキストでは、依存関係を操作するための明示的で広くサポートされている標準がなく、誰もが自分の小さなスクーターをペダルで操作するため、状況は少し複雑になります。つまり、話し合い、比較することがあります。
この記事では、依存関係の階層をgoで編成するための最も一般的なツールとアプローチ、およびそれらの長所と短所について説明します。理論を知っていて、DIがあなたに質問を提起しない場合(このアプローチを適用する必要性を含む)、途中から記事を読み始めることができます。前半では、DIとは何か、なぜ一般的に必要なのか、特にth。
なぜこれがすべて必要なのですか
そもそも、すべてのプログラマーの主な敵であり、ほとんどすべての設計ツールが登場する主な理由は複雑さです。些細なケースは常に明確で、頭に浮かびやすく、1行のコードで明らかに優雅に解決され、問題は発生しません。システムに数万行(場合によってはそれ以上)のコードがあり、非常に多くの「動く」部分が絡み合って相互作用し、誰かに触れずに向きを変えることが不可能に見える1つの小さな世界に存在する場合は別の問題です。その後、肘。
複雑さの問題を解決するために、人類はまだ複雑なものを単純なものに分割し、それらを分離して別々に検討するよりも良い方法を見つけていません。
ここで重要なのは分離です。1つのコンポーネントが隣接するコンポーネントに影響を与えない限り、予期しない影響や、2番目のコンポーネントの結果に対する暗黙の影響を恐れることはできません。この分離を確実にするために、各コンポーネントの接続を制御し、それが何にどのように依存するかを明示的に説明することにしました。
この時点で、依存関係インジェクション(またはインジェクション)に到達します。これは、各コンポーネント(クラス、構造、モジュールなど)が必要なアプリケーションの部分のみにアクセスできるようにコードを編成する方法であり、不要なものはすべて隠します。その仕事のために、またはウィキペディアを引用すると、「DIはソフトウェアコンポーネントに外部依存関係を提供するプロセスです。」
このアプローチは、いくつかの問題を一度に解決します。
- 不要なものを隠し、開発者の認知的負荷を軽減します。
- ( , );
- , , ;
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 (, , ).