A Microservice Toolkit from gig-delivery

2021. 4. 2

Barney

Why?

  • Copy & Paste ➔ import
     
  • Change all repo ➔ Change only one repo

Why DI?

Testability Problem

func query() (email string) {
    db, err := sql.Open("postgres", "user=postgres dbname=test ...")
    if err != nil {
        panic(err)
    }
    err = db.QueryRow(`SELECT email FROM "user" WHERE id = $1`, 1).Scan(&email)
    if err != nil {
        panic(err)
    }
    return email
}
func query(db *sql.DB) (email string) {
    err = db.QueryRow(`SELECT email FROM "user" WHERE id = $1`, 1).Scan(&email)
    if err != nil {
        panic(err)
    }
    return email
}

func TestQuery(t *testing.T) {
    db := mockDB()
    defer db.Close()
    
    email := query(db)
    assert.Equal(t, email, "email@example.com")
}

Cost of Manual Wiring

Stage 1

Everything in main()

func main() {
    // Create logger
    logger := log.New(os.Stdout, "[daangn] ", 0)
    
    // Create handlers
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    	logger.Println("Handler called")
        io.WriteString(w, "Hello, world!\n")
    })
    
    // Register handlers
    http.Handle("/", handler)
    
    // Start server
    http.ListenAndServe(":8080", nil)
}

Stage 2

Functions and Manual Wiring

type Logger interface{
    Println(v ...interface{})
}

func NewLogger() Logger {
    return log.New(os.Stdout, "[daangn] ", 0)
}

func main() {
    logger := NewLogger()
    handler := NewHandler(logger)
    RegisterHandlers(handler)
    StartServer()
}

Stage 3

Libraries and Manual Wiring

package logger

type Logger interface{
    Println(v ...interface{})
}

func NewLogger() Logger {
    return log.New(os.Stdout, "[daangn] ", 0)
}
func main() {
    logger := logger.NewLogger()
    handler := handlers.NewHandler(logger)
    server.RegisterHandlers(handler)
    server.StartServer()
}

MySQL, MongoDB, Redis, gRPC, firebase, kafka, jaeger etc...

func main() {
    ...
    if err != nil {
    	...
    }
    ...
    if err != nil {
    	...
    }
    ...
    if err != nil {
    	...
    }
    ...
}

Fx

Package fx is a framework that makes it easy to build applications out of reusable, composable modules.

No XML
No Annotations
No Magic

Stage 4

Using Fx

package logger

var Module = fx.Options(
	fx.Provide(NewLogger),
)
...
func main() {
    app := fx.New(
    	server.Module,
        logger.Module,
        fx.Provide(
        	handlers.NewHandler,
        ),
    )
    app.Run()
}

App

  • A modular application built around dependency injection

  • When created, the application immediately executes all the functions passed via Invoke options
  • To supply these functions with the parameters they need, the application looks for constructors that return the appropriate types
  • Any required types are missing or any invocations return an error, the application will fail to start
  • OnStart hooks are executed

Provider

  • It's just a regular Go function
     
  • Fx calls constructors lazily
     
  • Once instantiated, it is cached and reused
    (it's effectively singleton)

Invocation

  • We need some invocations to kick-start our application
     

  • Unlike constructors, invocations are always executed
     
  • They're always run in order

Hook

  • A Hook is a pair of start and stop callbacks
     

  • Fx imposes a time limit on OnStart and OnStop hooks

deck

By JongHoon Kim

deck

  • 69