طرق تنظيم دورة حياة DI والتطبيق في GO

هناك العديد من الأشياء التي يمكنك القيام بها إلى الأبد: انظر إلى النار ، وأصلح الأخطاء في الكود القديم ، وتحدث بالطبع عن DI - وما زلت لا ، لا ، وستصادف تبعيات غريبة في التطبيق التالي.

ومع ذلك ، في سياق لغة GO ، يكون الموقف أكثر تعقيدًا بعض الشيء ، حيث لا يوجد معيار صريح ومدعوم على نطاق واسع للعمل مع التبعيات ، وكل شخص يقوم بالدوس على سكوتره الصغير - مما يعني أن هناك شيئًا للمناقشة والمقارنة.







في هذه المقالة ، سأناقش الأدوات والأساليب الأكثر شيوعًا لتنظيم التسلسل الهرمي للتبعية ، مع مزاياها وعيوبها. إذا كنت تعرف النظرية والاختصار DI لا يثير لك أي أسئلة (بما في ذلك الحاجة إلى تطبيق هذا النهج) ، فيمكنك البدء في قراءة المقالة من المنتصف ، في النصف الأول سأشرح ماهية DI ، ولماذا هو مطلوب بشكل عام وفي خاصة في ال.







لماذا نحتاج كل هذا



بادئ ذي بدء ، يعد التعقيد هو العدو الرئيسي لجميع المبرمجين والسبب الرئيسي لظهور جميع أدوات التصميم تقريبًا. دائمًا ما تكون الحالة التافهة واضحة ، وتقع بسهولة في الرأس ، ويتم حلها بشكل واضح ورشيق بسطر واحد من التعليمات البرمجية ، ولا توجد مشاكل معها أبدًا. إنها مسألة مختلفة عندما يحتوي النظام على عشرات ومئات الآلاف (وأحيانًا أكثر) من سطور التعليمات البرمجية ، والعديد من الأجزاء "المتحركة" التي تتشابك وتتفاعل وتتواجد في عالم صغير واحد حيث يبدو من المستحيل الدوران دون لمس شخص ما. ثم المرفقين.

لحل مشكلة التعقيد ، لم تجد البشرية بعد طريقة أفضل من تقسيم الأشياء المعقدة إلى أشياء بسيطة ، وعزلها والنظر فيها بشكل منفصل.

الشيء الأساسي هنا هو العزلة ، طالما أن أحد المكونات لا يؤثر على المكونات المجاورة ، فلا يمكنك الخوف من التأثيرات غير المتوقعة والتأثير الضمني لأحد المكونات على نتيجة الثاني. لضمان هذه العزلة ، قررنا التحكم في اتصالات كل مكون ، مع وصف صريح لما يعتمد عليه وكيف.

في هذه المرحلة ، نأتي إلى حقن التبعية (أو الحقن) ، وهو في الحقيقة مجرد طريقة لتنظيم الكود بحيث يكون لكل مكون (فئة ، هيكل ، وحدة ، إلخ) حق الوصول فقط إلى أجزاء التطبيق التي يحتاجها ، وإخفاء كل شيء غير ضروري منه. لعملها أو ، على حد تعبير ويكيبيديا: "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)
      
      





, , , ( ) .

, , .

, 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