├── README.md ├── database ├── db.go └── mock.go ├── main.go └── service ├── service.go └── service_test.go /README.md: -------------------------------------------------------------------------------- 1 | # The many flavours of dependency injection in Go 2 | 3 | One of the most challenging aspects of building applications in Go is managing the many dependencies that your application uses. In an ideal world, your application would be stateless, with only one input and one output -- essentially acting like a pure function. 4 | 5 | However, most medium to large scale applications _will_ have at least some dependencies. The applications we build at Gojek almost always have more than one of the following: 6 | 7 | - A postgres database 8 | - A redis cache 9 | - An HTTP client 10 | - A message queue 11 | - Another HTTP client 12 | 13 | For each of these dependencies, there is a bunch of stuff we have to consider: 14 | - __Initialization__: How are we going to set up the initial connection or state of a dependency? This is something that will need to happen only once in the applications life cycle. 15 | - __Testing__: How can we write independant test cases for services using an external dependency? Keep in mind that while writing test cases, we need to simulate the failure of our dependencies as well. 16 | - __State__: How do we expose a reference to each dependency (which is supposed to be constant) without creating any sort of global state (which, as we all know, is the root of all evil)? 17 | 18 | ## Dependency injection to the rescue 19 | 20 | The dependency injection pattern helps us solve these problems. Treating the external dependencies of our applications as individual dependencies for each service in our codebase allows us to have a more modular, and focussed approach during development, and follows a few premises: 21 | 22 | - __Dependencies are stateful__ : The only reason you would consider treating something as a dependency is if it had some sort of state. For example, a database has its connection pool as its state. This also means that there should be some kind of initialization involved before you can use a dependency 23 | - __Dependencies are represented by interfaces__ : A dependency is characterized by its contract. The service using it should not know about its implementation or internal state. 24 | 25 | ## Using dependency injection in a Go application 26 | 27 | We can illustrate the use of dependency injection by building an application that makes use of it. Let's consider a service that is dependent on a database as its store. The service will fetch an entry from a database, and log the result after performing some validations. 28 | 29 | We can define the service as a structure with the store as its dependency: 30 | 31 | ```go 32 | package service 33 | 34 | type Service struct { 35 | store database.Store 36 | } 37 | ``` 38 | 39 | Here, `database.Store` is the dependencies interface, that we can define in another package: 40 | 41 | ```go 42 | package database 43 | 44 | type Store interface { 45 | // Get will fetch the value (which is an integer) for a given ID 46 | Get(ID int) (int, error) 47 | } 48 | ``` 49 | 50 | The service can then use the dependency through its methods: 51 | 52 | ```go 53 | func (s *Service) GetNumber(ID int) error { 54 | // Use the `Get` method of the dependency to retreive the value of the database entry 55 | result, err := s.store.Get(ID) 56 | if err != nil { 57 | return err 58 | } 59 | // Perform some validation, and output an error if it is too high 60 | if result > 10 { 61 | return fmt.Errorf("result too high: %d", result) 62 | } 63 | // Return nil, if the result is valid 64 | return nil 65 | } 66 | ``` 67 | 68 | Note, that we have not defined the implementation of our dependency as yet (in fact, that's one of the last things we will do!) 69 | 70 | ### Testing the service 71 | 72 | One of the most powerful features of dependency injection, is that you can test any dependant service without having any actual implementation of the dependency. In fact, we can mock our dependency to behave the way we want it to, so that we can test our service to handle different failure scenarios. 73 | 74 | First, we will have to mock our dependency: 75 | 76 | ```go 77 | package database 78 | 79 | import ( 80 | // We use the "testify" library for mocking our store 81 | "github.com/stretchr/testify/mock" 82 | ) 83 | 84 | // Create a MockStore struct with an embedded mock instance 85 | type MockStore struct { 86 | mock.Mock 87 | } 88 | 89 | func (m *MockStore) Get(ID int) (int, error) { 90 | // This allows us to pass in mocked results, so that the mock store will return whatever we define 91 | returnVals := m.Called(ID) 92 | // return the values which we define 93 | return returnVals.Get(0).(int), returnVals.Error(1) 94 | } 95 | ``` 96 | 97 | We can then use this mock store to simulate the dependency in our service when we test it: 98 | 99 | ```go 100 | func TestServiceSuccess(t *testing.T) { 101 | // Create a new instance of the mock store 102 | m := new(database.MockStore) 103 | // In the "On" method, we assert that we want the "Get" method 104 | // to be called with one argument, that is 2 105 | // In the "Return" method, we define the return values to be 7, and nil (for the result and error values) 106 | m.On("Get", 2).Return(7, nil) 107 | // Next, we create a new instance of our service with the mock store as its "store" dependency 108 | s := Service{m} 109 | // The "GetNumber" method call is then made 110 | err := s.GetNumber(2) 111 | // The expectations that we defined for our mock store earlier are asserted here 112 | m.AssertExpectations(t) 113 | // Finally, we assert that we should'nt get any error 114 | if err != nil { 115 | t.Errorf("error should be nil, got: %v", err) 116 | } 117 | } 118 | ``` 119 | 120 | We can now use the mock to simulate error scenarios, and test for them as well: 121 | 122 | ```go 123 | func TestServiceResultTooHigh(t *testing.T) { 124 | m := new(database.MockStore) 125 | // In this case, we simulate a return value of 24, which would fail the services validation 126 | m.On("Get", 2).Return(24, nil) 127 | s := Service{m} 128 | err := s.GetNumber(2) 129 | m.AssertExpectations(t) 130 | // We assert that we expect the "result too high" error given by the service 131 | if err.Error() != "result too high: 24" { 132 | t.Errorf("error should be 'result too high: 24', got: %v", err) 133 | } 134 | } 135 | 136 | func TestServiceStoreError(t *testing.T) { 137 | m := new(database.MockStore) 138 | // In this case, we simulate the case where the store returns an error, which may occur if it is unable to fetch the value 139 | m.On("Get", 2).Return(0, errors.New("failed")) 140 | s := Service{m} 141 | err := s.GetNumber(2) 142 | m.AssertExpectations(t) 143 | if err.Error() != "failed" { 144 | t.Errorf("error should be 'failed', got: %v", err) 145 | } 146 | } 147 | ``` 148 | 149 | ### Implementing and initializing the real store 150 | 151 | Now that we know our service works well with the mock store, we can implement the actual one: 152 | 153 | ```go 154 | // The actual store would contain some state. In this case it's the sql.db instance, that holds the connection to our database 155 | type store struct { 156 | db *sql.DB 157 | } 158 | 159 | // Implement the "Get" method, in order to comply with the "Store" interface 160 | func (d *store) Get(ID int) (int, error) { 161 | //we would perform some external database operation with d.db 162 | // for the sake of clarity, that code is not shown here 163 | return 0, nil 164 | } 165 | 166 | // Add a constructor function to return a new instance of a store 167 | func NewStore(db *sql.DB) Store { 168 | return &store{db} 169 | } 170 | ``` 171 | 172 | We can now put together the "store" as a dependency to the service and construct a simple command line app: 173 | 174 | ```go 175 | func main() { 176 | // Create a new DB connection 177 | connString := "dbname= sslmode=disable" 178 | db, _ := sql.Open("postgres", connString) 179 | 180 | // Create a store dependency with the db connection 181 | store := database.NewStore(db) 182 | // Create the service by injecting the store as a dependency 183 | service := &service.Service{Store: store} 184 | 185 | // The following code implements a simple command line app to read the ID as input 186 | // and output the validity of the result of the entry with that ID in the database 187 | scanner := bufio.NewScanner(os.Stdin) 188 | for scanner.Scan(){ 189 | ID, _ := strconv.Atoi(scanner.Text()) 190 | err := service.GetNumber(ID) 191 | if err != nil { 192 | fmt.Printf("result invalid: %v", err) 193 | continue 194 | } 195 | fmt.Println("result valid") 196 | } 197 | } 198 | ``` 199 | 200 | What we have essentially done by using dependency injection, is converted something that looks like a dependency graph, into something that looks like a pure function, with the dependencies now part of the service: 201 | 202 | 203 | ## Alternative implementations of dependency injection 204 | 205 | Adding dependencies as object attributes isn't the only way to inject them. Sometimes, when your interface has just one method, it's more convenient to use a more functional form of dependency injection. If we were to assume that our service _only_ had a `GetNumber` method, we could use curried functions to add dependencies as variables inside the closure: 206 | 207 | ```go 208 | func NewGetNumber(store database.Store) func(int) error { 209 | return func(ID int) error { 210 | // "store" is still a dependency, only, now it's accessible through the function closure 211 | result, err := store.Get(ID) 212 | if err != nil { 213 | return err 214 | } 215 | if result > 10 { 216 | return fmt.Errorf("result too high: %d", result) 217 | } 218 | return nil 219 | } 220 | } 221 | ``` 222 | 223 | And you can generate the `GetNumber` function by calling the function constructor with a store implementation: 224 | 225 | ```go 226 | GetNumber := NewGetNumber(store) 227 | ``` 228 | 229 | `GetNumber` now has the same functionality as the previous OOP based approach. This method of deriving dependant functions using currying is especially useful when you require single functions instead of a whole suite of methods (for example, in HTTP handler functions). 230 | 231 | ## When to avoid dependency injection 232 | 233 | As with all things, there is no silver bullet to solve all your problems, and this is true for the dependency injection pattern as well. Although it can make your code more modular, this pattern also comes with the cost of increased complexity during initialization. You cannot simply call a method of a dependancy without explicitly passing it down during initialization. This also makes it harder to add new services, since there is more boilerplate code to get it up and running the first time. Sometimes, when there are a lot of embedded dependencies (if your dependencies have their own dependencies), initialization can be a nightmare. 234 | 235 | If the application you are building is simple, or if you have many embedded dependencies then you should probably assess if it is worth the trade-offs to implement this pattern. -------------------------------------------------------------------------------- /database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | type Store interface { 8 | Get(ID int) (int, error) 9 | } 10 | 11 | func NewStore(db *sql.DB) Store { 12 | return &store{db} 13 | } 14 | 15 | // The actual store would contain some state. In this case it's the sql.db instance, that holds the connection to our database 16 | type store struct { 17 | db *sql.DB 18 | } 19 | 20 | func (d *store) Get(ID int) (int, error) { 21 | //we would perform some external database operation with d.db 22 | return 0, nil 23 | } 24 | -------------------------------------------------------------------------------- /database/mock.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | ) 6 | 7 | type MockStore struct { 8 | mock.Mock 9 | } 10 | 11 | func (m *MockStore) Get(ID int) (int, error) { 12 | returnVals := m.Called(ID) 13 | return returnVals.Get(0).(int), returnVals.Error(1) 14 | } 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "database/sql" 6 | "fmt" 7 | "os" 8 | "bufio" 9 | "github.com/sohamkamani/go-dependency-injection-example/database" 10 | "github.com/sohamkamani/go-dependency-injection-example/service" 11 | 12 | ) 13 | 14 | func main() { 15 | // Create a new DB connection 16 | connString := "dbname= sslmode=disable" 17 | db, _ := sql.Open("postgres", connString) 18 | 19 | // Create a store dependency with the db connection 20 | store := database.NewStore(db) 21 | // Create the service by injecting the store as a dependency 22 | service := &service.Service{Store: store} 23 | 24 | // The following code implements a simple command line app to read the ID as input 25 | // and output the validity of the result of the entry with that ID in the database 26 | scanner := bufio.NewScanner(os.Stdin) 27 | for scanner.Scan(){ 28 | ID, _ := strconv.Atoi(scanner.Text()) 29 | err := service.GetNumber(ID) 30 | if err != nil { 31 | fmt.Printf("result invalid: %v", err) 32 | continue 33 | } 34 | fmt.Println("result valid") 35 | } 36 | } -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sohamkamani/go-dependency-injection-example/database" 6 | ) 7 | 8 | type Service struct { 9 | Store database.Store 10 | } 11 | 12 | func (s *Service) GetNumber(ID int) error { 13 | // Use the `Get` method of the dependency to retreive the value of the database entry 14 | result, err := s.Store.Get(ID) 15 | if err != nil { 16 | return err 17 | } 18 | // Perform some validation, and output an error if it is too high 19 | if result > 10 { 20 | return fmt.Errorf("result too high: %d", result) 21 | } 22 | // Return nil, if the result is valid 23 | return nil 24 | } 25 | 26 | func NewGetNumber(store database.Store) func(int) error { 27 | return func(ID int) error { 28 | // Use the `Get` method of the dependency to retreive the value of the database entry 29 | result, err := store.Get(ID) 30 | if err != nil { 31 | return err 32 | } 33 | // Perform some validation, and output an error if it is too high 34 | if result > 10 { 35 | return fmt.Errorf("result too high: %d", result) 36 | } 37 | // Return nil, if the result is valid 38 | return nil 39 | } 40 | } -------------------------------------------------------------------------------- /service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "github.com/sohamkamani/go-dependency-injection-example/database" 6 | "testing" 7 | ) 8 | 9 | func TestServiceSuccess(t *testing.T) { 10 | // Create a new instance of the mock store 11 | m := new(database.MockStore) 12 | // In the "On" method, we assert that we want the "Get" method 13 | // to be called with one argument, that is 2 14 | // In the "Return" method, we define the return values to be 7, and nil (for the result and error values) 15 | m.On("Get", 2).Return(7, nil) 16 | // Next, we create a new instance of our service with the mock store as its "store" dependency 17 | s := Service{m} 18 | // The "GetNumber" method call is then made 19 | err := s.GetNumber(2) 20 | // The expectations that we defined for our mock store earlier are asserted here 21 | m.AssertExpectations(t) 22 | // Finally, we assert that we should'nt get any error 23 | if err != nil { 24 | t.Errorf("error should be nil, got: %v", err) 25 | } 26 | } 27 | 28 | func TestServiceResultTooHigh(t *testing.T) { 29 | m := new(database.MockStore) 30 | // In this case, we simulate a return value of 24, which would fail the services validation 31 | m.On("Get", 2).Return(24, nil) 32 | s := Service{m} 33 | err := s.GetNumber(2) 34 | m.AssertExpectations(t) 35 | // We assert that we expect the "result too high" error given by the service 36 | if err.Error() != "result too high: 24" { 37 | t.Errorf("error should be 'result too high: 24', got: %v", err) 38 | } 39 | } 40 | 41 | func TestServiceStoreError(t *testing.T) { 42 | m := new(database.MockStore) 43 | // In this case, we simulate the case where the store returns an error, which may occur if it is unable to fetch the value 44 | m.On("Get", 2).Return(0, errors.New("failed")) 45 | s := Service{m} 46 | err := s.GetNumber(2) 47 | m.AssertExpectations(t) 48 | if err.Error() != "failed" { 49 | t.Errorf("error should be 'failed', got: %v", err) 50 | } 51 | } --------------------------------------------------------------------------------