Métodos para organizar DI y el ciclo de vida de la aplicación en GO

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)
      
      





, , , ( ) .

, , .

, Avito :







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, :







  1. ( );
  2. ( ctx.cancel



    ->gCtx.cancel



    );
  3. , — , 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 (, , ).








All Articles