Metode untuk mengatur DI dan siklus hidup aplikasi di GO

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)
      
      





, , , ( ) .

, , .

, 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