Ada beberapa hal yang dapat Anda lakukan selamanya: lihat api, perbaiki bug di kode lama dan, tentu saja, bicarakan tentang DI - dan tetap tidak, tidak, dan Anda akan menemukan dependensi aneh di aplikasi berikutnya.
Namun, dalam konteks bahasa GO, situasinya sedikit lebih rumit, karena tidak ada standar yang eksplisit dan didukung secara luas untuk bekerja dengan dependensi, dan setiap orang mengayuh skuter kecil mereka sendiri - yang berarti ada sesuatu untuk didiskusikan dan dibandingkan.
Pada artikel ini, saya akan membahas alat dan pendekatan paling populer untuk mengatur hierarki ketergantungan, dengan kelebihan dan kekurangannya. Jika Anda mengetahui teori dan singkatan DI tidak menimbulkan pertanyaan bagi Anda (termasuk perlunya menerapkan pendekatan ini), maka Anda dapat mulai membaca artikel dari tengah, di paruh pertama saya akan menjelaskan apa itu DI, mengapa itu diperlukan secara umum dan di khususnya di th.
Mengapa kita membutuhkan semua ini
Untuk memulainya, musuh utama semua programmer dan alasan utama munculnya hampir semua alat desain adalah kompleksitas. Kasus sepele selalu jelas, mudah terbayang, diselesaikan dengan jelas dan anggun dengan satu baris kode, dan tidak pernah ada masalah dengannya. Ini adalah masalah yang berbeda ketika sistem memiliki puluhan dan ratusan ribu (dan terkadang lebih) baris kode, dan banyak sekali bagian "bergerak" yang terjalin, berinteraksi, dan hanya ada di satu dunia kecil di mana tampaknya tidak mungkin untuk berbalik tanpa menyentuh lalu siku.
Untuk memecahkan masalah kompleksitas, umat manusia belum menemukan cara yang lebih baik daripada memecah hal-hal kompleks menjadi yang sederhana, mengisolasi dan mempertimbangkannya secara terpisah.
Kuncinya di sini adalah isolasi, selama satu komponen tidak memengaruhi komponen yang berdekatan, Anda tidak perlu takut akan efek tak terduga dan pengaruh implisit salah satunya pada hasil komponen kedua. Untuk memastikan isolasi ini, kami memutuskan untuk mengontrol koneksi setiap komponen, secara eksplisit menjelaskan apa dan bagaimana bergantung.
Pada titik ini, kita sampai pada injeksi ketergantungan (atau injeksi), yang sebenarnya hanyalah cara untuk mengatur kode sehingga setiap komponen (kelas, struktur, modul, dll.) Memiliki akses hanya ke bagian aplikasi yang dibutuhkan, menyembunyikan semua yang tidak diperlukan darinya. untuk pekerjaannya atau, mengutip wikipedia: "DI adalah proses menyediakan ketergantungan eksternal ke komponen perangkat lunak."
Pendekatan ini menyelesaikan beberapa masalah sekaligus:
- Menyembunyikan yang tidak perlu, mengurangi beban kognitif pada pengembang;
- ( , );
- , , ;
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 (, , ).