Méthodes d'organisation de l'ID et du cycle de vie des applications dans GO

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)
      
      





, , , ( ) .

, , .

, 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