MĂ©todos para organizar DI e ciclo de vida do aplicativo em GO

Existem vĂĄrias coisas que vocĂȘ pode fazer para sempre: olhar para o incĂȘndio, consertar bugs no cĂłdigo legado e, claro, falar sobre DI - e ainda nĂŁo, nĂŁo, e vocĂȘ encontrarĂĄ dependĂȘncias estranhas no prĂłximo aplicativo.

No contexto da linguagem GO, no entanto, a situação Ă© um pouco mais complicada, jĂĄ que nĂŁo hĂĄ um padrĂŁo explĂ­cito e universalmente suportado para trabalhar com dependĂȘncias, e cada um pedala sua prĂłpria scooter - o que significa que hĂĄ algo para discutir e comparar.







Neste artigo, discutirei as ferramentas e abordagens mais populares para organizar uma hierarquia de dependĂȘncias em go, com suas vantagens e desvantagens. Se vocĂȘ conhece a teoria e a abreviatura DI nĂŁo levanta nenhuma questĂŁo para vocĂȘ (incluindo a necessidade de aplicar esta abordagem), entĂŁo vocĂȘ pode começar a ler o artigo do meio, na primeira metade explicarei o que Ă© DI, porque Ă© necessĂĄrio em geral e em particular em th.







Por que precisamos de tudo isso



Para começar, o principal inimigo de todos os programadores e a principal razĂŁo para o surgimento de quase todas as ferramentas de design Ă© a complexidade. O caso trivial Ă© sempre claro, facilmente cai na cabeça, Ă© Ăłbvia e graciosamente resolvido com uma linha de cĂłdigo e nunca hĂĄ problemas com ele. É uma questĂŁo diferente quando o sistema tem dezenas e centenas de milhares (e Ă s vezes mais) linhas de cĂłdigo e muitas partes "mĂłveis" que se entrelaçam, interagem e simplesmente existem em um pequeno mundo onde parece impossĂ­vel virar sem tocar em alguĂ©m. entĂŁo cotovelos.

Para resolver o problema da complexidade, a humanidade ainda nĂŁo encontrou maneira melhor do que dividir as coisas complexas em simples, isolando-as e considerando-as separadamente.

O principal aqui Ă© o isolamento, desde que um componente nĂŁo afete os vizinhos, vocĂȘ nĂŁo pode ter medo de efeitos inesperados e da influĂȘncia implĂ­cita de um no resultado do segundo. Para garantir esse isolamento, decidimos controlar as conexĂ”es de cada componente, descrevendo explicitamente o que e como depende.

Nesse ponto, chegamos Ă  injeção de dependĂȘncia (ou injeção), que na verdade Ă© apenas uma maneira de organizar o cĂłdigo de forma que cada componente (classe, estrutura, mĂłdulo, etc.) tenha acesso apenas Ă s partes da aplicação de que precisa, ocultando tudo o que for desnecessĂĄrio. para seu trabalho ou, para citar a Wikipedia: "DI Ă© o processo de fornecer uma dependĂȘncia externa a um componente de software."







Esta abordagem resolve vĂĄrios problemas de uma vez:







  • Esconde o desnecessĂĄrio, reduzindo a carga cognitiva do desenvolvedor;
  • ( , );
  • , , ;


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