├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── di.go ├── di_example_test.go ├── di_test.go ├── docs └── .gitkeep ├── examples ├── healthcheckable │ └── example.go ├── interface │ ├── car.go │ ├── engine.go │ ├── main.go │ └── wheel.go ├── shutdownable │ └── example.go └── simple │ └── example.go ├── go.mod ├── go.sum ├── injector.go ├── injector_example_test.go ├── injector_test.go ├── main_test.go ├── service.go ├── service_eager.go ├── service_eager_test.go ├── service_lazy.go ├── service_lazy_test.go ├── service_test.go ├── utils.go └── utils_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [samber] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | golangci: 9 | name: lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.18 15 | stable: false 16 | - uses: actions/checkout@v2 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v8 19 | with: 20 | args: --timeout 120s --max-same-issues 50 21 | 22 | - name: Bearer 23 | uses: bearer/bearer-action@v2 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | semver: 7 | type: string 8 | description: 'Semver (eg: v1.2.3)' 9 | required: true 10 | 11 | jobs: 12 | release: 13 | if: github.triggering_actor == 'samber' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.18 22 | stable: false 23 | 24 | - name: Test 25 | run: make test 26 | 27 | # remove tests in order to clean dependencies 28 | - name: Remove xxx_test.go files 29 | run: rm -rf *_test.go docker-compose.yml examples/ 30 | 31 | - name: Cleanup dependencies 32 | run: go mod tidy 33 | 34 | - name: List files 35 | run: tree -Cfi 36 | - name: Write new go.mod into logs 37 | run: cat go.mod 38 | - name: Write new go.sum into logs 39 | run: cat go.sum 40 | 41 | - name: Create tag 42 | run: | 43 | git config --global user.name '${{ github.triggering_actor }}' 44 | git config --global user.email "${{ github.triggering_actor}}@users.noreply.github.com" 45 | 46 | git add . 47 | git commit --allow-empty -m 'bump ${{ inputs.semver }}' 48 | git tag ${{ inputs.semver }} 49 | git push origin ${{ inputs.semver }} 50 | 51 | - name: Release 52 | uses: softprops/action-gh-release@v1 53 | with: 54 | name: ${{ inputs.semver }} 55 | tag_name: ${{ inputs.semver }} 56 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | tags: 6 | branches: 7 | pull_request: 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | go: 16 | - '1.18' 17 | - '1.19' 18 | - '1.20' 19 | - '1.21' 20 | - '1.22' 21 | - '1.x' 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: ${{ matrix.go }} 29 | stable: false 30 | 31 | - name: Build 32 | run: make build 33 | 34 | - name: Test 35 | run: make test 36 | 37 | - name: Test 38 | run: make coverage 39 | 40 | - name: Codecov 41 | uses: codecov/codecov-action@v5 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | file: ./cover.out 45 | flags: unittests 46 | verbose: true 47 | if: matrix.go == '1.18' 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/go 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=go 4 | 5 | ### Go ### 6 | # If you prefer the allow list template instead of the deny list, see community template: 7 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 8 | # 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | # Go workspace file 26 | go.work 27 | 28 | ### Go Patch ### 29 | /vendor/ 30 | /Godeps/ 31 | 32 | # End of https://www.toptal.com/developers/gitignore/api/go 33 | 34 | cover.out 35 | cover.html 36 | .vscode 37 | 38 | .idea/ 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Samuel Berthe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | BIN=go 3 | # BIN=go1.18beta1 4 | 5 | go1.18beta1: 6 | go install golang.org/dl/go1.18beta1@latest 7 | go1.18beta1 download 8 | 9 | build: 10 | ${BIN} build -v ./... 11 | 12 | test: 13 | go test -race -v ./... 14 | watch-test: 15 | reflex -t 50ms -s -- sh -c 'gotest -race -v ./...' 16 | 17 | bench: 18 | go test -benchmem -count 3 -bench ./... 19 | watch-bench: 20 | reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' 21 | 22 | coverage: 23 | ${BIN} test -v -coverprofile=cover.out -covermode=atomic . 24 | ${BIN} tool cover -html=cover.out -o cover.html 25 | 26 | # tools 27 | tools: 28 | ${BIN} install github.com/cespare/reflex@latest 29 | ${BIN} install github.com/rakyll/gotest@latest 30 | ${BIN} install github.com/psampaz/go-mod-outdated@latest 31 | ${BIN} install github.com/jondot/goweight@latest 32 | ${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 33 | ${BIN} get -t -u golang.org/x/tools/cmd/cover 34 | ${BIN} get -t -u github.com/sonatype-nexus-community/nancy@latest 35 | go mod tidy 36 | 37 | lint: 38 | golangci-lint run --timeout 60s --max-same-issues 50 ./... 39 | lint-fix: 40 | golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... 41 | 42 | audit: tools 43 | ${BIN} mod tidy 44 | ${BIN} list -json -m all | nancy sleuth 45 | 46 | outdated: tools 47 | ${BIN} mod tidy 48 | ${BIN} list -u -m -json all | go-mod-outdated -update -direct 49 | 50 | weight: tools 51 | goweight 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # do - Dependency Injection 3 | 4 | [![tag](https://img.shields.io/github/tag/samber/do.svg)](https://github.com/samber/do/releases) 5 | ![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.18-%23007d9c) 6 | [![GoDoc](https://godoc.org/github.com/samber/do?status.svg)](https://pkg.go.dev/github.com/samber/do) 7 | ![Build Status](https://github.com/samber/do/actions/workflows/test.yml/badge.svg) 8 | [![Go report](https://goreportcard.com/badge/github.com/samber/do)](https://goreportcard.com/report/github.com/samber/do) 9 | [![Coverage](https://img.shields.io/codecov/c/github/samber/do)](https://codecov.io/gh/samber/do) 10 | [![License](https://img.shields.io/github/license/samber/do)](./LICENSE) 11 | 12 | **⚙️ A dependency injection toolkit based on Go 1.18+ Generics.** 13 | 14 | This library implements the Dependency Injection design pattern. It may replace the `uber/dig` fantastic package in simple Go projects. `samber/do` uses Go 1.18+ generics and therefore offers a typesafe API. 15 | 16 | **See also:** 17 | 18 | - [samber/lo](https://github.com/samber/lo): A Lodash-style Go library based on Go 1.18+ Generics 19 | - [samber/mo](https://github.com/samber/mo): Monads based on Go 1.18+ Generics (Option, Result, Either...) 20 | 21 | **Why this name?** 22 | 23 | I love **short name** for such a utility library. This name is the sum of `DI` and `Go` and no Go package currently uses this name. 24 | 25 | **⭕⭕⭕⭕⭕⭕ About v2 ⭕⭕⭕⭕⭕⭕** 26 | 27 | Check out the beta now! 28 | 29 | ```bash 30 | go get -u github.com/samber/do/v2@v2.0.0-beta.7 31 | ``` 32 | 33 | Documentation: https://do.samber.dev/ 34 | 35 | Please report bugs here: [#45](https://github.com/samber/do/pull/45). 36 | 37 | ## 💡 Features 38 | 39 | - Service registration 40 | - Service invocation 41 | - Service health check 42 | - Service shutdown 43 | - Service lifecycle hooks 44 | - Named or anonymous services 45 | - Eagerly or lazily loaded services 46 | - Dependency graph resolution 47 | - Default injector 48 | - Injector cloning 49 | - Service override 50 | - Lightweight, no dependencies 51 | - No code generation 52 | 53 | 🚀 Services are loaded in invocation order. 54 | 55 | 🕵️ Service health can be checked individually or globally. Services implementing `do.Healthcheckable` interface will be called via `do.HealthCheck[type]()` or `injector.HealthCheck()`. 56 | 57 | 🛑 Services can be shutdowned properly, in back-initialization order. Services implementing `do.Shutdownable` interface will be called via `do.Shutdown[type]()` or `injector.Shutdown()`. 58 | 59 | ## 🚀 Install 60 | 61 | ```sh 62 | go get github.com/samber/do@v1 63 | ``` 64 | 65 | This library is v1 and follows SemVer strictly. 66 | 67 | No breaking changes will be made to exported APIs before v2.0.0. 68 | 69 | This library has no dependencies except the Go std lib. 70 | 71 | ## 💡 Quick start 72 | 73 | You can import `do` using: 74 | 75 | ```go 76 | import ( 77 | "github.com/samber/do" 78 | ) 79 | ``` 80 | 81 | Then instantiate services: 82 | 83 | ```go 84 | func main() { 85 | injector := do.New() 86 | 87 | // provides CarService 88 | do.Provide(injector, NewCarService) 89 | 90 | // provides EngineService 91 | do.Provide(injector, NewEngineService) 92 | 93 | car := do.MustInvoke[*CarService](injector) 94 | car.Start() 95 | // prints "car starting" 96 | 97 | do.HealthCheck[EngineService](injector) 98 | // returns "engine broken" 99 | 100 | // injector.ShutdownOnSIGTERM() // will block until receiving sigterm signal 101 | injector.Shutdown() 102 | // prints "car stopped" 103 | } 104 | ``` 105 | 106 | Services: 107 | 108 | ```go 109 | type EngineService interface{} 110 | 111 | func NewEngineService(i *do.Injector) (EngineService, error) { 112 | return &engineServiceImplem{}, nil 113 | } 114 | 115 | type engineServiceImplem struct {} 116 | 117 | // [Optional] Implements do.Healthcheckable. 118 | func (c *engineServiceImplem) HealthCheck() error { 119 | return fmt.Errorf("engine broken") 120 | } 121 | ``` 122 | 123 | ```go 124 | func NewCarService(i *do.Injector) (*CarService, error) { 125 | engine := do.MustInvoke[EngineService](i) 126 | car := CarService{Engine: engine} 127 | return &car, nil 128 | } 129 | 130 | type CarService struct { 131 | Engine EngineService 132 | } 133 | 134 | func (c *CarService) Start() { 135 | println("car starting") 136 | } 137 | 138 | // [Optional] Implements do.Shutdownable. 139 | func (c *CarService) Shutdown() error { 140 | println("car stopped") 141 | return nil 142 | } 143 | ``` 144 | 145 | ## 🤠 Spec 146 | 147 | [GoDoc: https://godoc.org/github.com/samber/do](https://godoc.org/github.com/samber/do) 148 | 149 | Injector: 150 | 151 | - [do.New](https://pkg.go.dev/github.com/samber/do#New) 152 | - [do.NewWithOpts](https://pkg.go.dev/github.com/samber/do#NewWithOpts) 153 | - [injector.Clone](https://pkg.go.dev/github.com/samber/do#injector.Clone) 154 | - [injector.CloneWithOpts](https://pkg.go.dev/github.com/samber/do#injector.CloneWithOpts) 155 | - [injector.HealthCheck](https://pkg.go.dev/github.com/samber/do#injector.HealthCheck) 156 | - [injector.Shutdown](https://pkg.go.dev/github.com/samber/do#injector.Shutdown) 157 | - [injector.ShutdownOnSIGTERM](https://pkg.go.dev/github.com/samber/do#injector.ShutdownOnSIGTERM) 158 | - [injector.ShutdownOnSignals](https://pkg.go.dev/github.com/samber/do#injector.ShutdownOnSignals) 159 | - [injector.ListProvidedServices](https://pkg.go.dev/github.com/samber/do#injector.ListProvidedServices) 160 | - [injector.ListInvokedServices](https://pkg.go.dev/github.com/samber/do#injector.ListInvokedServices) 161 | - [do.HealthCheck](https://pkg.go.dev/github.com/samber/do#HealthCheck) 162 | - [do.HealthCheckNamed](https://pkg.go.dev/github.com/samber/do#HealthCheckNamed) 163 | - [do.Shutdown](https://pkg.go.dev/github.com/samber/do#Shutdown) 164 | - [do.ShutdownNamed](https://pkg.go.dev/github.com/samber/do#ShutdownNamed) 165 | - [do.MustShutdown](https://pkg.go.dev/github.com/samber/do#MustShutdown) 166 | - [do.MustShutdownNamed](https://pkg.go.dev/github.com/samber/do#MustShutdownNamed) 167 | 168 | Service registration: 169 | 170 | - [do.Provide](https://pkg.go.dev/github.com/samber/do#Provide) 171 | - [do.ProvideNamed](https://pkg.go.dev/github.com/samber/do#ProvideNamed) 172 | - [do.ProvideNamedValue](https://pkg.go.dev/github.com/samber/do#ProvideNamedValue) 173 | - [do.ProvideValue](https://pkg.go.dev/github.com/samber/do#ProvideValue) 174 | 175 | Service invocation: 176 | 177 | - [do.Invoke](https://pkg.go.dev/github.com/samber/do#Invoke) 178 | - [do.MustInvoke](https://pkg.go.dev/github.com/samber/do#MustInvoke) 179 | - [do.InvokeNamed](https://pkg.go.dev/github.com/samber/do#InvokeNamed) 180 | - [do.MustInvokeNamed](https://pkg.go.dev/github.com/samber/do#MustInvokeNamed) 181 | 182 | Service override: 183 | 184 | - [do.Override](https://pkg.go.dev/github.com/samber/do#Override) 185 | - [do.OverrideNamed](https://pkg.go.dev/github.com/samber/do#OverrideNamed) 186 | - [do.OverrideNamedValue](https://pkg.go.dev/github.com/samber/do#OverrideNamedValue) 187 | - [do.OverrideValue](https://pkg.go.dev/github.com/samber/do#OverrideValue) 188 | 189 | ### Injector (DI container) 190 | 191 | Build a container for your components. `Injector` is responsible for building services in the right order, and managing service lifecycle. 192 | 193 | ```go 194 | injector := do.New() 195 | ``` 196 | 197 | Or use `nil` as the default injector: 198 | 199 | ```go 200 | do.Provide(nil, func (i *Injector) (int, error) { 201 | return 42, nil 202 | }) 203 | 204 | service := do.MustInvoke[int](nil) 205 | ``` 206 | 207 | You can check health of services implementing `func HealthCheck() error`. 208 | 209 | ```go 210 | type DBService struct { 211 | db *sql.DB 212 | } 213 | 214 | func (s *DBService) HealthCheck() error { 215 | return s.db.Ping() 216 | } 217 | 218 | injector := do.New() 219 | do.Provide(injector, ...) 220 | do.Invoke(injector, ...) 221 | 222 | statuses := injector.HealthCheck() 223 | // map[string]error{ 224 | // "*DBService": nil, 225 | // } 226 | ``` 227 | 228 | De-initialize all compoments properly. Services implementing `func Shutdown() error` will be called synchronously in back-initialization order. 229 | 230 | ```go 231 | type DBService struct { 232 | db *sql.DB 233 | } 234 | 235 | func (s *DBService) Shutdown() error { 236 | return s.db.Close() 237 | } 238 | 239 | injector := do.New() 240 | do.Provide(injector, ...) 241 | do.Invoke(injector, ...) 242 | 243 | // shutdown all services in reverse order 244 | injector.Shutdown() 245 | ``` 246 | 247 | List services: 248 | 249 | ```go 250 | type DBService struct { 251 | db *sql.DB 252 | } 253 | 254 | injector := do.New() 255 | 256 | do.Provide(injector, ...) 257 | println(do.ListProvidedServices()) 258 | // output: []string{"*DBService"} 259 | 260 | do.Invoke(injector, ...) 261 | println(do.ListInvokedServices()) 262 | // output: []string{"*DBService"} 263 | ``` 264 | 265 | ### Service registration 266 | 267 | Services can be registered in multiple way: 268 | 269 | - with implicit name (struct or interface name) 270 | - with explicit name 271 | - eagerly 272 | - lazily 273 | 274 | Anonymous service, loaded lazily: 275 | 276 | ```go 277 | type DBService struct { 278 | db *sql.DB 279 | } 280 | 281 | do.Provide[DBService](injector, func(i *Injector) (*DBService, error) { 282 | db, err := sql.Open(...) 283 | if err != nil { 284 | return nil, err 285 | } 286 | 287 | return &DBService{db: db}, nil 288 | }) 289 | ``` 290 | 291 | Named service, loaded lazily: 292 | 293 | ```go 294 | type DBService struct { 295 | db *sql.DB 296 | } 297 | 298 | do.ProvideNamed(injector, "dbconn", func(i *Injector) (*DBService, error) { 299 | db, err := sql.Open(...) 300 | if err != nil { 301 | return nil, err 302 | } 303 | 304 | return &DBService{db: db}, nil 305 | }) 306 | ``` 307 | 308 | Anonymous service, loaded eagerly: 309 | 310 | ```go 311 | type Config struct { 312 | uri string 313 | } 314 | 315 | do.ProvideValue[Config](injector, Config{uri: "postgres://user:pass@host:5432/db"}) 316 | ``` 317 | 318 | Named service, loaded eagerly: 319 | 320 | ```go 321 | type Config struct { 322 | uri string 323 | } 324 | 325 | do.ProvideNamedValue(injector, "configuration", Config{uri: "postgres://user:pass@host:5432/db"}) 326 | ``` 327 | 328 | ### Service invocation 329 | 330 | Loads anonymous service: 331 | 332 | ```go 333 | type DBService struct { 334 | db *sql.DB 335 | } 336 | 337 | dbService, err := do.Invoke[DBService](injector) 338 | ``` 339 | 340 | Loads anonymous service or panics if service was not registered: 341 | 342 | ```go 343 | type DBService struct { 344 | db *sql.DB 345 | } 346 | 347 | dbService := do.MustInvoke[DBService](injector) 348 | ``` 349 | 350 | Loads named service: 351 | 352 | ```go 353 | config, err := do.InvokeNamed[Config](injector, "configuration") 354 | ``` 355 | 356 | Loads named service or panics if service was not registered: 357 | 358 | ```go 359 | config := do.MustInvokeNamed[Config](injector, "configuration") 360 | ``` 361 | 362 | ### Individual service healthcheck 363 | 364 | Check health of anonymous service: 365 | 366 | ```go 367 | type DBService struct { 368 | db *sql.DB 369 | } 370 | 371 | dbService, err := do.Invoke[DBService](injector) 372 | err = do.HealthCheck[DBService](injector) 373 | ``` 374 | 375 | Check health of named service: 376 | 377 | ```go 378 | config, err := do.InvokeNamed[Config](injector, "configuration") 379 | err = do.HealthCheckNamed(injector, "configuration") 380 | ``` 381 | 382 | ### Individual service shutdown 383 | 384 | Unloads anonymous service: 385 | 386 | ```go 387 | type DBService struct { 388 | db *sql.DB 389 | } 390 | 391 | dbService, err := do.Invoke[DBService](injector) 392 | err = do.Shutdown[DBService](injector) 393 | ``` 394 | 395 | Unloads anonymous service or panics if service was not registered: 396 | 397 | ```go 398 | type DBService struct { 399 | db *sql.DB 400 | } 401 | 402 | dbService := do.MustInvoke[DBService](injector) 403 | do.MustShutdown[DBService](injector) 404 | ``` 405 | 406 | Unloads named service: 407 | 408 | ```go 409 | config, err := do.InvokeNamed[Config](injector, "configuration") 410 | err = do.ShutdownNamed(injector, "configuration") 411 | ``` 412 | 413 | Unloads named service or panics if service was not registered: 414 | 415 | ```go 416 | config := do.MustInvokeNamed[Config](injector, "configuration") 417 | do.MustShutdownNamed(injector, "configuration") 418 | ``` 419 | 420 | ### Service override 421 | 422 | By default, providing a service twice will panic. Service can be replaced at runtime using `do.Override` helper. 423 | 424 | ```go 425 | do.Provide[Vehicle](injector, func (i *do.Injector) (Vehicle, error) { 426 | return &CarImplem{}, nil 427 | }) 428 | 429 | do.Override[Vehicle](injector, func (i *do.Injector) (Vehicle, error) { 430 | return &BusImplem{}, nil 431 | }) 432 | ``` 433 | 434 | ### Hooks 435 | 436 | 2 lifecycle hooks are available in Injectors: 437 | - After registration 438 | - After shutdown 439 | 440 | ```go 441 | injector := do.NewWithOpts(&do.InjectorOpts{ 442 | HookAfterRegistration: func(injector *do.Injector, serviceName string) { 443 | fmt.Printf("Service registered: %s\n", serviceName) 444 | }, 445 | HookAfterShutdown: func(injector *do.Injector, serviceName string) { 446 | fmt.Printf("Service stopped: %s\n", serviceName) 447 | }, 448 | 449 | Logf: func(format string, args ...any) { 450 | log.Printf(format, args...) 451 | }, 452 | }) 453 | ``` 454 | 455 | ### Cloning injector 456 | 457 | Cloned injector have same service registrations as it's parent, but it doesn't share invoked service state. 458 | 459 | Clones are useful for unit testing by replacing some services to mocks. 460 | 461 | ```go 462 | var injector *do.Injector; 463 | 464 | func init() { 465 | do.Provide[Service](injector, func (i *do.Injector) (Service, error) { 466 | return &RealService{}, nil 467 | }) 468 | do.Provide[*App](injector, func (i *do.Injector) (*App, error) { 469 | return &App{i.MustInvoke[Service](i)}, nil 470 | }) 471 | } 472 | 473 | func TestService(t *testing.T) { 474 | i := injector.Clone() 475 | defer i.Shutdown() 476 | 477 | // replace Service to MockService 478 | do.Override[Service](i, func (i *do.Injector) (Service, error) { 479 | return &MockService{}, nil 480 | })) 481 | 482 | app := do.Invoke[*App](i) 483 | // do unit testing with mocked service 484 | } 485 | ``` 486 | 487 | ## 🛩 Benchmark 488 | 489 | // @TODO 490 | 491 | ## 🤝 Contributing 492 | 493 | - Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :)) 494 | - Fork the [project](https://github.com/samber/do) 495 | - Fix [open issues](https://github.com/samber/do/issues) or request new features 496 | 497 | Don't hesitate ;) 498 | 499 | ### With Docker 500 | 501 | ```bash 502 | docker-compose run --rm dev 503 | ``` 504 | 505 | ### Without Docker 506 | 507 | ```bash 508 | # Install some dev dependencies 509 | make tools 510 | 511 | # Run tests 512 | make test 513 | # or 514 | make watch-test 515 | ``` 516 | 517 | ## 👤 Contributors 518 | 519 | ![Contributors](https://contrib.rocks/image?repo=samber/do) 520 | 521 | ## 💫 Show your support 522 | 523 | Give a ⭐️ if this project helped you! 524 | 525 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/samber?style=for-the-badge)](https://github.com/sponsors/samber) 526 | 527 | ## 📝 License 528 | 529 | Copyright © 2022 [Samuel Berthe](https://github.com/samber). 530 | 531 | This project is [MIT](./LICENSE) licensed. 532 | -------------------------------------------------------------------------------- /di.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func Provide[T any](i *Injector, provider Provider[T]) { 8 | name := generateServiceName[T]() 9 | 10 | ProvideNamed[T](i, name, provider) 11 | } 12 | 13 | func ProvideNamed[T any](i *Injector, name string, provider Provider[T]) { 14 | _i := getInjectorOrDefault(i) 15 | if _i.exists(name) { 16 | panic(fmt.Errorf("DI: service `%s` has already been declared", name)) 17 | } 18 | 19 | service := newServiceLazy(name, provider) 20 | _i.set(name, service) 21 | 22 | _i.logf("service %s injected", name) 23 | } 24 | 25 | func ProvideValue[T any](i *Injector, value T) { 26 | name := generateServiceName[T]() 27 | 28 | ProvideNamedValue[T](i, name, value) 29 | } 30 | 31 | func ProvideNamedValue[T any](i *Injector, name string, value T) { 32 | _i := getInjectorOrDefault(i) 33 | if _i.exists(name) { 34 | panic(fmt.Errorf("DI: service `%s` has already been declared", name)) 35 | } 36 | 37 | service := newServiceEager(name, value) 38 | _i.set(name, service) 39 | 40 | _i.logf("service %s injected", name) 41 | } 42 | 43 | func Override[T any](i *Injector, provider Provider[T]) { 44 | name := generateServiceName[T]() 45 | 46 | OverrideNamed[T](i, name, provider) 47 | } 48 | 49 | func OverrideNamed[T any](i *Injector, name string, provider Provider[T]) { 50 | _i := getInjectorOrDefault(i) 51 | 52 | service := newServiceLazy(name, provider) 53 | _i.set(name, service) 54 | 55 | _i.logf("service %s overridden", name) 56 | } 57 | 58 | func OverrideValue[T any](i *Injector, value T) { 59 | name := generateServiceName[T]() 60 | 61 | OverrideNamedValue[T](i, name, value) 62 | } 63 | 64 | func OverrideNamedValue[T any](i *Injector, name string, value T) { 65 | _i := getInjectorOrDefault(i) 66 | 67 | service := newServiceEager(name, value) 68 | _i.set(name, service) 69 | 70 | _i.logf("service %s overridden", name) 71 | } 72 | 73 | func Invoke[T any](i *Injector) (T, error) { 74 | name := generateServiceName[T]() 75 | return InvokeNamed[T](i, name) 76 | } 77 | 78 | func MustInvoke[T any](i *Injector) T { 79 | s, err := Invoke[T](i) 80 | must(err) 81 | return s 82 | } 83 | 84 | func InvokeNamed[T any](i *Injector, name string) (T, error) { 85 | return invokeImplem[T](i, name) 86 | } 87 | 88 | func MustInvokeNamed[T any](i *Injector, name string) T { 89 | s, err := InvokeNamed[T](i, name) 90 | must(err) 91 | return s 92 | } 93 | 94 | func invokeImplem[T any](i *Injector, name string) (T, error) { 95 | _i := getInjectorOrDefault(i) 96 | 97 | serviceAny, ok := _i.get(name) 98 | if !ok { 99 | return empty[T](), _i.serviceNotFound(name) 100 | } 101 | 102 | service, ok := serviceAny.(Service[T]) 103 | if !ok { 104 | return empty[T](), _i.serviceNotFound(name) 105 | } 106 | 107 | instance, err := service.getInstance(_i) 108 | if err != nil { 109 | return empty[T](), err 110 | } 111 | 112 | _i.onServiceInvoke(name) 113 | 114 | _i.logf("service %s invoked", name) 115 | 116 | return instance, nil 117 | } 118 | 119 | func HealthCheck[T any](i *Injector) error { 120 | name := generateServiceName[T]() 121 | return getInjectorOrDefault(i).healthcheckImplem(name) 122 | } 123 | 124 | func HealthCheckNamed(i *Injector, name string) error { 125 | return getInjectorOrDefault(i).healthcheckImplem(name) 126 | } 127 | 128 | func Shutdown[T any](i *Injector) error { 129 | name := generateServiceName[T]() 130 | return getInjectorOrDefault(i).shutdownImplem(name) 131 | } 132 | 133 | func MustShutdown[T any](i *Injector) { 134 | name := generateServiceName[T]() 135 | must(getInjectorOrDefault(i).shutdownImplem(name)) 136 | } 137 | 138 | func ShutdownNamed(i *Injector, name string) error { 139 | return getInjectorOrDefault(i).shutdownImplem(name) 140 | } 141 | 142 | func MustShutdownNamed(i *Injector, name string) { 143 | must(getInjectorOrDefault(i).shutdownImplem(name)) 144 | } 145 | -------------------------------------------------------------------------------- /di_example_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ExampleProvide() { 8 | injector := New() 9 | 10 | type test struct { 11 | foobar string 12 | } 13 | 14 | Provide(injector, func(i *Injector) (*test, error) { 15 | return &test{foobar: "foobar"}, nil 16 | }) 17 | value, err := Invoke[*test](injector) 18 | 19 | fmt.Println(value) 20 | fmt.Println(err) 21 | // Output: 22 | // &{foobar} 23 | // 24 | } 25 | 26 | func ExampleInvoke() { 27 | injector := New() 28 | 29 | type test struct { 30 | foobar string 31 | } 32 | 33 | Provide(injector, func(i *Injector) (*test, error) { 34 | return &test{foobar: "foobar"}, nil 35 | }) 36 | value, err := Invoke[*test](injector) 37 | 38 | fmt.Println(value) 39 | fmt.Println(err) 40 | // Output: 41 | // &{foobar} 42 | // 43 | } 44 | 45 | func ExampleMustInvoke() { 46 | injector := New() 47 | 48 | type test struct { 49 | foobar string 50 | } 51 | 52 | Provide(injector, func(i *Injector) (*test, error) { 53 | return &test{foobar: "foobar"}, nil 54 | }) 55 | value := MustInvoke[*test](injector) 56 | 57 | fmt.Println(value) 58 | // Output: 59 | // &{foobar} 60 | } 61 | 62 | func ExampleProvideNamed() { 63 | injector := New() 64 | 65 | type test struct { 66 | foobar string 67 | } 68 | 69 | ProvideNamed(injector, "my_service", func(i *Injector) (*test, error) { 70 | return &test{foobar: "foobar"}, nil 71 | }) 72 | value, err := InvokeNamed[*test](injector, "my_service") 73 | 74 | fmt.Println(value) 75 | fmt.Println(err) 76 | // Output: 77 | // &{foobar} 78 | // 79 | } 80 | 81 | func ExampleInvokeNamed() { 82 | injector := New() 83 | 84 | type test struct { 85 | foobar string 86 | } 87 | 88 | ProvideNamed(injector, "my_service", func(i *Injector) (*test, error) { 89 | return &test{foobar: "foobar"}, nil 90 | }) 91 | value, err := InvokeNamed[*test](injector, "my_service") 92 | 93 | fmt.Println(value) 94 | fmt.Println(err) 95 | // Output: 96 | // &{foobar} 97 | // 98 | } 99 | 100 | func ExampleMustInvokeNamed() { 101 | injector := New() 102 | 103 | type test struct { 104 | foobar string 105 | } 106 | 107 | ProvideNamed(injector, "my_service", func(i *Injector) (*test, error) { 108 | return &test{foobar: "foobar"}, nil 109 | }) 110 | value := MustInvokeNamed[*test](injector, "my_service") 111 | 112 | fmt.Println(value) 113 | // Output: 114 | // &{foobar} 115 | } 116 | 117 | func ExampleProvideValue() { 118 | injector := New() 119 | 120 | type test struct { 121 | foobar string 122 | } 123 | 124 | ProvideValue(injector, &test{foobar: "foobar"}) 125 | value, err := Invoke[*test](injector) 126 | 127 | fmt.Println(value) 128 | fmt.Println(err) 129 | // Output: 130 | // &{foobar} 131 | // 132 | } 133 | 134 | func ExampleProvideNamedValue() { 135 | injector := New() 136 | 137 | type test struct { 138 | foobar string 139 | } 140 | 141 | ProvideNamedValue(injector, "my_service", &test{foobar: "foobar"}) 142 | value, err := InvokeNamed[*test](injector, "my_service") 143 | 144 | fmt.Println(value) 145 | fmt.Println(err) 146 | // Output: 147 | // &{foobar} 148 | // 149 | } 150 | 151 | func ExampleOverride() { 152 | injector := New() 153 | 154 | type test struct { 155 | foobar string 156 | } 157 | 158 | Provide(injector, func(i *Injector) (*test, error) { 159 | return &test{foobar: "foobar1"}, nil 160 | }) 161 | Override(injector, func(i *Injector) (*test, error) { 162 | return &test{foobar: "foobar2"}, nil 163 | }) 164 | value, err := Invoke[*test](injector) 165 | 166 | fmt.Println(value) 167 | fmt.Println(err) 168 | // Output: 169 | // &{foobar2} 170 | // 171 | } 172 | 173 | func ExampleOverrideNamed() { 174 | injector := New() 175 | 176 | type test struct { 177 | foobar string 178 | } 179 | 180 | ProvideNamed(injector, "my_service", func(i *Injector) (*test, error) { 181 | return &test{foobar: "foobar1"}, nil 182 | }) 183 | OverrideNamed(injector, "my_service", func(i *Injector) (*test, error) { 184 | return &test{foobar: "foobar2"}, nil 185 | }) 186 | value, err := InvokeNamed[*test](injector, "my_service") 187 | 188 | fmt.Println(value) 189 | fmt.Println(err) 190 | // Output: 191 | // &{foobar2} 192 | // 193 | } 194 | 195 | func ExampleOverrideNamedValue() { 196 | injector := New() 197 | 198 | type test struct { 199 | foobar string 200 | } 201 | 202 | ProvideNamedValue(injector, "my_service", &test{foobar: "foobar1"}) 203 | OverrideNamedValue(injector, "my_service", &test{foobar: "foobar2"}) 204 | value, err := InvokeNamed[*test](injector, "my_service") 205 | 206 | fmt.Println(value) 207 | fmt.Println(err) 208 | // Output: 209 | // &{foobar2} 210 | // 211 | } 212 | -------------------------------------------------------------------------------- /di_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDefaultInjector(t *testing.T) { 11 | is := assert.New(t) 12 | 13 | type test struct { 14 | foobar string 15 | } 16 | 17 | DefaultInjector = New() 18 | 19 | Provide(nil, func(i *Injector) (*test, error) { 20 | return &test{foobar: "42"}, nil 21 | }) 22 | 23 | is.Len(DefaultInjector.services, 1) 24 | 25 | service, err := Invoke[*test](nil) 26 | 27 | is.Equal(test{foobar: "42"}, *service) 28 | is.Nil(err) 29 | } 30 | 31 | func TestProvide(t *testing.T) { 32 | is := assert.New(t) 33 | 34 | type test struct{} 35 | 36 | i := New() 37 | 38 | Provide(i, func(i *Injector) (*test, error) { 39 | return &test{}, nil 40 | }) 41 | 42 | Provide(i, func(i *Injector) (test, error) { 43 | return test{}, fmt.Errorf("error") 44 | }) 45 | 46 | is.Panics(func() { 47 | // try to erase previous instance 48 | Provide(i, func(i *Injector) (test, error) { 49 | return test{}, fmt.Errorf("error") 50 | }) 51 | }) 52 | 53 | is.Len(i.services, 2) 54 | 55 | s1, ok1 := i.services["*do.test"] 56 | is.True(ok1) 57 | if ok1 { 58 | s, ok := s1.(Service[*test]) 59 | is.True(ok) 60 | if ok { 61 | is.Equal("*do.test", s.getName()) 62 | } 63 | } 64 | 65 | s2, ok2 := i.services["do.test"] 66 | is.True(ok2) 67 | if ok2 { 68 | s, ok := s2.(Service[test]) 69 | is.True(ok) 70 | if ok { 71 | is.Equal("do.test", s.getName()) 72 | } 73 | } 74 | 75 | _, ok3 := i.services["*do.plop"] 76 | is.False(ok3) 77 | } 78 | 79 | func TestProvideValue(t *testing.T) { 80 | is := assert.New(t) 81 | 82 | i := New() 83 | 84 | type test struct { 85 | foobar string 86 | } 87 | _test := test{foobar: "foobar"} 88 | 89 | ProvideNamedValue(i, "foobar", 42) 90 | ProvideNamedValue(i, "hello", _test) 91 | 92 | is.Len(i.services, 2) 93 | 94 | s1, ok1 := i.services["foobar"] 95 | is.True(ok1) 96 | if ok1 { 97 | s, ok := s1.(Service[int]) 98 | is.True(ok) 99 | if ok { 100 | is.Equal("foobar", s.getName()) 101 | instance, err := s.getInstance(i) 102 | is.EqualValues(42, instance) 103 | is.Nil(err) 104 | } 105 | } 106 | 107 | s2, ok2 := i.services["hello"] 108 | is.True(ok2) 109 | if ok2 { 110 | s, ok := s2.(Service[test]) 111 | is.True(ok) 112 | if ok { 113 | is.Equal("hello", s.getName()) 114 | instance, err := s.getInstance(i) 115 | is.EqualValues(_test, instance) 116 | is.Nil(err) 117 | } 118 | } 119 | } 120 | 121 | func TestInvoke(t *testing.T) { 122 | is := assert.New(t) 123 | 124 | type test struct { 125 | foobar string 126 | } 127 | 128 | i := New() 129 | 130 | Provide(i, func(i *Injector) (test, error) { 131 | return test{foobar: "foobar"}, nil 132 | }) 133 | 134 | is.Len(i.services, 1) 135 | 136 | s0a, ok0a := i.services["do.test"] 137 | is.True(ok0a) 138 | 139 | s0b, ok0b := s0a.(*ServiceLazy[test]) 140 | is.True(ok0b) 141 | is.False(s0b.built) 142 | 143 | s1, err1 := Invoke[test](i) 144 | is.Nil(err1) 145 | if err1 == nil { 146 | is.Equal("foobar", s1.foobar) 147 | } 148 | 149 | is.True(s0b.built) 150 | 151 | _, err2 := Invoke[*test](i) 152 | is.NotNil(err2) 153 | is.Errorf(err2, "do: service not found") 154 | } 155 | 156 | func TestInvokeNamed(t *testing.T) { 157 | is := assert.New(t) 158 | 159 | i := New() 160 | 161 | type test struct { 162 | foobar string 163 | } 164 | _test := test{foobar: "foobar"} 165 | 166 | ProvideNamedValue(i, "foobar", 42) 167 | ProvideNamedValue(i, "hello", _test) 168 | 169 | is.Len(i.services, 2) 170 | 171 | service0, err0 := InvokeNamed[string](i, "plop") 172 | is.NotNil(err0) 173 | is.Empty(service0) 174 | 175 | instance1, err1 := InvokeNamed[test](i, "hello") 176 | is.Nil(err1) 177 | is.EqualValues(_test, instance1) 178 | is.EqualValues("foobar", instance1.foobar) 179 | 180 | instance2, err2 := InvokeNamed[int](i, "foobar") 181 | is.Nil(err2) 182 | is.EqualValues(42, instance2) 183 | } 184 | 185 | func TestMustInvoke(t *testing.T) { 186 | is := assert.New(t) 187 | 188 | i := New() 189 | 190 | type test struct { 191 | foobar string 192 | } 193 | _test := test{foobar: "foobar"} 194 | 195 | Provide(i, func(i *Injector) (test, error) { 196 | return _test, nil 197 | }) 198 | 199 | is.Len(i.services, 1) 200 | 201 | is.Panics(func() { 202 | _ = MustInvoke[string](i) 203 | }) 204 | 205 | is.NotPanics(func() { 206 | instance1 := MustInvoke[test](i) 207 | is.EqualValues(_test, instance1) 208 | }) 209 | } 210 | 211 | func TestMustInvokeNamed(t *testing.T) { 212 | is := assert.New(t) 213 | 214 | i := New() 215 | 216 | ProvideNamedValue(i, "foobar", 42) 217 | 218 | is.Len(i.services, 1) 219 | 220 | is.Panics(func() { 221 | _ = MustInvokeNamed[string](i, "hello") 222 | }) 223 | 224 | is.Panics(func() { 225 | _ = MustInvokeNamed[string](i, "foobar") 226 | }) 227 | 228 | is.NotPanics(func() { 229 | instance1 := MustInvokeNamed[int](i, "foobar") 230 | is.EqualValues(42, instance1) 231 | }) 232 | } 233 | 234 | func TestShutdown(t *testing.T) { 235 | is := assert.New(t) 236 | 237 | type test struct { 238 | foobar string 239 | } 240 | 241 | i := New() 242 | 243 | Provide(i, func(i *Injector) (test, error) { 244 | return test{foobar: "foobar"}, nil 245 | }) 246 | 247 | instance, err := Invoke[test](i) 248 | is.Equal(test{foobar: "foobar"}, instance) 249 | is.Nil(err) 250 | 251 | err = Shutdown[test](i) 252 | is.Nil(err) 253 | 254 | instance, err = Invoke[test](i) 255 | is.Empty(instance) 256 | is.NotNil(err) 257 | 258 | err = Shutdown[test](i) 259 | is.NotNil(err) 260 | } 261 | 262 | func TestMustShutdown(t *testing.T) { 263 | is := assert.New(t) 264 | 265 | type test struct { 266 | foobar string 267 | } 268 | 269 | i := New() 270 | 271 | Provide(i, func(i *Injector) (test, error) { 272 | return test{foobar: "foobar"}, nil 273 | }) 274 | 275 | instance, err := Invoke[test](i) 276 | is.Equal(test{foobar: "foobar"}, instance) 277 | is.Nil(err) 278 | 279 | is.NotPanics(func() { 280 | MustShutdown[test](i) 281 | }) 282 | 283 | instance, err = Invoke[test](i) 284 | is.Empty(instance) 285 | is.NotNil(err) 286 | 287 | is.Panics(func() { 288 | MustShutdown[test](i) 289 | }) 290 | } 291 | 292 | func TestShutdownNamed(t *testing.T) { 293 | is := assert.New(t) 294 | 295 | i := New() 296 | 297 | ProvideNamedValue(i, "foobar", 42) 298 | 299 | instance, err := InvokeNamed[int](i, "foobar") 300 | is.Equal(42, instance) 301 | is.Nil(err) 302 | 303 | err = ShutdownNamed(i, "foobar") 304 | is.Nil(err) 305 | 306 | instance, err = InvokeNamed[int](i, "foobar") 307 | is.Empty(instance) 308 | is.NotNil(err) 309 | 310 | err = ShutdownNamed(i, "foobar") 311 | is.NotNil(err) 312 | } 313 | 314 | func TestMustShutdownNamed(t *testing.T) { 315 | is := assert.New(t) 316 | 317 | i := New() 318 | 319 | ProvideNamedValue(i, "foobar", 42) 320 | 321 | instance, err := InvokeNamed[int](i, "foobar") 322 | is.Equal(42, instance) 323 | is.Nil(err) 324 | 325 | is.NotPanics(func() { 326 | MustShutdownNamed(i, "foobar") 327 | }) 328 | 329 | instance, err = InvokeNamed[int](i, "foobar") 330 | is.Empty(instance) 331 | is.NotNil(err) 332 | 333 | is.Panics(func() { 334 | MustShutdownNamed(i, "foobar") 335 | }) 336 | } 337 | 338 | func TestDoubleInjection(t *testing.T) { 339 | is := assert.New(t) 340 | 341 | type test struct{} 342 | 343 | i := New() 344 | 345 | is.NotPanics(func() { 346 | Provide(i, func(i *Injector) (*test, error) { 347 | return &test{}, nil 348 | }) 349 | }) 350 | 351 | is.PanicsWithError("DI: service `*do.test` has already been declared", func() { 352 | Provide(i, func(i *Injector) (*test, error) { 353 | return &test{}, nil 354 | }) 355 | }) 356 | 357 | is.PanicsWithError("DI: service `*do.test` has already been declared", func() { 358 | ProvideValue(i, &test{}) 359 | }) 360 | 361 | is.PanicsWithError("DI: service `*do.test` has already been declared", func() { 362 | ProvideNamed(i, "*do.test", func(i *Injector) (*test, error) { 363 | return &test{}, nil 364 | }) 365 | }) 366 | 367 | is.PanicsWithError("DI: service `*do.test` has already been declared", func() { 368 | ProvideNamedValue(i, "*do.test", &test{}) 369 | }) 370 | } 371 | 372 | func TestOverride(t *testing.T) { 373 | is := assert.New(t) 374 | 375 | type test struct { 376 | foobar int 377 | } 378 | 379 | i := New() 380 | 381 | is.NotPanics(func() { 382 | Provide(i, func(i *Injector) (*test, error) { 383 | return &test{42}, nil 384 | }) 385 | is.Equal(42, MustInvoke[*test](i).foobar) 386 | 387 | Override(i, func(i *Injector) (*test, error) { 388 | return &test{1}, nil 389 | }) 390 | is.Equal(1, MustInvoke[*test](i).foobar) 391 | 392 | OverrideNamed(i, "*do.test", func(i *Injector) (*test, error) { 393 | return &test{2}, nil 394 | }) 395 | is.Equal(2, MustInvoke[*test](i).foobar) 396 | 397 | OverrideValue(i, &test{3}) 398 | is.Equal(3, MustInvoke[*test](i).foobar) 399 | 400 | OverrideNamedValue(i, "*do.test", &test{4}) 401 | is.Equal(4, MustInvoke[*test](i).foobar) 402 | }) 403 | } 404 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samber/do/1a2b8dbe41c1a596205fe1ae82ad77a6cb4a2599/docs/.gitkeep -------------------------------------------------------------------------------- /examples/healthcheckable/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/samber/do" 7 | ) 8 | 9 | /** 10 | * Wheel 11 | */ 12 | type Wheel struct { 13 | } 14 | 15 | /** 16 | * Engine 17 | */ 18 | type Engine struct{} 19 | 20 | func (e *Engine) HealthCheck() error { 21 | return fmt.Errorf("engine broken") 22 | } 23 | 24 | /** 25 | * Car 26 | */ 27 | type Car struct { 28 | Engine *Engine 29 | Wheels []*Wheel 30 | } 31 | 32 | func (c *Car) Start() { 33 | println("vroooom") 34 | } 35 | 36 | /** 37 | * Run example 38 | */ 39 | func main() { 40 | injector := do.New() 41 | 42 | // provide wheels 43 | do.ProvideNamedValue(injector, "wheel-1", &Wheel{}) 44 | do.ProvideNamedValue(injector, "wheel-2", &Wheel{}) 45 | do.ProvideNamedValue(injector, "wheel-3", &Wheel{}) 46 | do.ProvideNamedValue(injector, "wheel-4", &Wheel{}) 47 | 48 | // provide car 49 | do.Provide(injector, func(i *do.Injector) (*Car, error) { 50 | car := Car{ 51 | Engine: do.MustInvoke[*Engine](i), 52 | Wheels: []*Wheel{ 53 | do.MustInvokeNamed[*Wheel](i, "wheel-1"), 54 | do.MustInvokeNamed[*Wheel](i, "wheel-2"), 55 | do.MustInvokeNamed[*Wheel](i, "wheel-3"), 56 | do.MustInvokeNamed[*Wheel](i, "wheel-4"), 57 | }, 58 | } 59 | 60 | return &car, nil 61 | }) 62 | 63 | // provide engine 64 | do.Provide(injector, func(i *do.Injector) (*Engine, error) { 65 | return &Engine{}, nil 66 | }) 67 | 68 | // start car 69 | car := do.MustInvoke[*Car](injector) 70 | car.Start() 71 | 72 | // check single service 73 | fmt.Println(do.HealthCheck[*Engine](injector)) 74 | // check all services 75 | fmt.Println(injector.HealthCheck()) 76 | } 77 | -------------------------------------------------------------------------------- /examples/interface/car.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/samber/do" 4 | 5 | type Car interface { 6 | Start() 7 | } 8 | 9 | type carImplem struct { 10 | Engine Engine 11 | Wheels []*Wheel 12 | } 13 | 14 | func (c *carImplem) Start() { 15 | println("vroooom") 16 | } 17 | 18 | func NewCar(i *do.Injector) (Car, error) { 19 | wheels := []*Wheel{ 20 | do.MustInvokeNamed[*Wheel](i, "wheel-1"), 21 | do.MustInvokeNamed[*Wheel](i, "wheel-2"), 22 | do.MustInvokeNamed[*Wheel](i, "wheel-3"), 23 | do.MustInvokeNamed[*Wheel](i, "wheel-4"), 24 | } 25 | 26 | engine := do.MustInvoke[Engine](i) 27 | 28 | car := carImplem{ 29 | Engine: engine, 30 | Wheels: wheels, 31 | } 32 | 33 | return &car, nil 34 | } 35 | -------------------------------------------------------------------------------- /examples/interface/engine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/samber/do" 4 | 5 | type Engine interface{} 6 | 7 | type engineImplem struct { 8 | } 9 | 10 | func NewEngine(i *do.Injector) (Engine, error) { 11 | return &engineImplem{}, nil 12 | } 13 | -------------------------------------------------------------------------------- /examples/interface/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/samber/do" 5 | ) 6 | 7 | func main() { 8 | injector := do.New() 9 | 10 | // provide wheels 11 | do.ProvideNamedValue(injector, "wheel-1", NewWheel()) 12 | do.ProvideNamedValue(injector, "wheel-2", NewWheel()) 13 | do.ProvideNamedValue(injector, "wheel-3", NewWheel()) 14 | do.ProvideNamedValue(injector, "wheel-4", NewWheel()) 15 | 16 | // provide car 17 | do.Provide(injector, NewCar) 18 | 19 | // provide engine 20 | do.Provide(injector, NewEngine) 21 | 22 | // start car 23 | car := do.MustInvoke[Car](injector) 24 | car.Start() 25 | } 26 | -------------------------------------------------------------------------------- /examples/interface/wheel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Wheel struct { 4 | } 5 | 6 | func NewWheel() *Wheel { 7 | return &Wheel{} 8 | } 9 | -------------------------------------------------------------------------------- /examples/shutdownable/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/samber/do" 7 | ) 8 | 9 | /** 10 | * Wheel 11 | */ 12 | type Wheel struct { 13 | } 14 | 15 | /** 16 | * Engine 17 | */ 18 | type Engine struct { 19 | } 20 | 21 | func (c *Engine) Shutdown() error { 22 | println("engine stopped") 23 | return nil 24 | } 25 | 26 | /** 27 | * Car 28 | */ 29 | type Car struct { 30 | Engine *Engine 31 | Wheels []*Wheel 32 | } 33 | 34 | func (c *Car) Shutdown() error { 35 | println("car stopped") 36 | return nil 37 | } 38 | 39 | func (c *Car) Start() { 40 | println("vroooom") 41 | } 42 | 43 | /** 44 | * Run example 45 | */ 46 | func main() { 47 | injector := do.New() 48 | 49 | // provide wheels 50 | do.ProvideNamedValue(injector, "wheel-1", &Wheel{}) 51 | do.ProvideNamedValue(injector, "wheel-2", &Wheel{}) 52 | do.ProvideNamedValue(injector, "wheel-3", &Wheel{}) 53 | do.ProvideNamedValue(injector, "wheel-4", &Wheel{}) 54 | 55 | // provide car 56 | do.Provide(injector, func(i *do.Injector) (*Car, error) { 57 | car := Car{ 58 | Engine: do.MustInvoke[*Engine](i), 59 | Wheels: []*Wheel{ 60 | do.MustInvokeNamed[*Wheel](i, "wheel-1"), 61 | do.MustInvokeNamed[*Wheel](i, "wheel-2"), 62 | do.MustInvokeNamed[*Wheel](i, "wheel-3"), 63 | do.MustInvokeNamed[*Wheel](i, "wheel-4"), 64 | }, 65 | } 66 | 67 | return &car, nil 68 | }) 69 | 70 | // provide engine 71 | do.Provide(injector, func(i *do.Injector) (*Engine, error) { 72 | return &Engine{}, nil 73 | }) 74 | 75 | // start car 76 | car := do.MustInvoke[*Car](injector) 77 | car.Start() 78 | 79 | err := injector.ShutdownOnSIGTERM() 80 | if err != nil { 81 | log.Fatal(err.Error()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/simple/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/samber/do" 5 | ) 6 | 7 | /** 8 | * Wheel 9 | */ 10 | type Wheel struct { 11 | } 12 | 13 | /** 14 | * Engine 15 | */ 16 | type Engine struct { 17 | } 18 | 19 | /** 20 | * Car 21 | */ 22 | type Car struct { 23 | Engine *Engine 24 | Wheels []*Wheel 25 | } 26 | 27 | func (c *Car) Start() { 28 | println("vroooom") 29 | } 30 | 31 | /** 32 | * Run example 33 | */ 34 | func main() { 35 | injector := do.New() 36 | 37 | // provide wheels 38 | do.ProvideNamedValue(injector, "wheel-1", &Wheel{}) 39 | do.ProvideNamedValue(injector, "wheel-2", &Wheel{}) 40 | do.ProvideNamedValue(injector, "wheel-3", &Wheel{}) 41 | do.ProvideNamedValue(injector, "wheel-4", &Wheel{}) 42 | 43 | // provide car 44 | do.Provide(injector, func(i *do.Injector) (*Car, error) { 45 | car := Car{ 46 | Engine: do.MustInvoke[*Engine](i), 47 | Wheels: []*Wheel{ 48 | do.MustInvokeNamed[*Wheel](i, "wheel-1"), 49 | do.MustInvokeNamed[*Wheel](i, "wheel-2"), 50 | do.MustInvokeNamed[*Wheel](i, "wheel-3"), 51 | do.MustInvokeNamed[*Wheel](i, "wheel-4"), 52 | }, 53 | } 54 | 55 | return &car, nil 56 | }) 57 | 58 | // provide engine 59 | do.Provide(injector, func(i *do.Injector) (*Engine, error) { 60 | return &Engine{}, nil 61 | }) 62 | 63 | // start car 64 | car := do.MustInvoke[*Car](injector) 65 | car.Start() 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/samber/do 2 | 3 | go 1.18 4 | 5 | // 6 | // Dependencies are excluded from releases. Please check CI. 7 | // 8 | 9 | require ( 10 | github.com/stretchr/testify v1.10.0 11 | go.uber.org/goleak v1.2.1 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/kr/text v0.2.0 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 5 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 6 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 10 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 11 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 12 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /injector.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "strings" 8 | "sync" 9 | "syscall" 10 | ) 11 | 12 | var DefaultInjector = New() 13 | 14 | func getInjectorOrDefault(i *Injector) *Injector { 15 | if i != nil { 16 | return i 17 | } 18 | 19 | return DefaultInjector 20 | } 21 | 22 | func New() *Injector { 23 | return NewWithOpts(&InjectorOpts{}) 24 | } 25 | 26 | type InjectorOpts struct { 27 | HookAfterRegistration func(injector *Injector, serviceName string) 28 | HookAfterShutdown func(injector *Injector, serviceName string) 29 | 30 | Logf func(format string, args ...any) 31 | } 32 | 33 | func NewWithOpts(opts *InjectorOpts) *Injector { 34 | logf := opts.Logf 35 | if logf == nil { 36 | logf = func(format string, args ...any) {} 37 | } 38 | 39 | logf("injector created") 40 | 41 | return &Injector{ 42 | mu: sync.RWMutex{}, 43 | services: make(map[string]any), 44 | 45 | orderedInvocation: map[string]int{}, 46 | orderedInvocationIndex: 0, 47 | 48 | hookAfterRegistration: opts.HookAfterRegistration, 49 | hookAfterShutdown: opts.HookAfterShutdown, 50 | 51 | logf: logf, 52 | } 53 | } 54 | 55 | type Injector struct { 56 | mu sync.RWMutex 57 | services map[string]any 58 | 59 | // It should be a graph instead of simple ordered list. 60 | orderedInvocation map[string]int // map is faster than slice 61 | orderedInvocationIndex int 62 | 63 | hookAfterRegistration func(injector *Injector, serviceName string) 64 | hookAfterShutdown func(injector *Injector, serviceName string) 65 | 66 | logf func(format string, args ...any) 67 | } 68 | 69 | func (i *Injector) ListProvidedServices() []string { 70 | i.mu.RLock() 71 | names := keys(i.services) 72 | i.mu.RUnlock() 73 | 74 | i.logf("exported list of services: %v", names) 75 | 76 | return names 77 | } 78 | 79 | func (i *Injector) ListInvokedServices() []string { 80 | i.mu.RLock() 81 | names := keys(i.orderedInvocation) 82 | i.mu.RUnlock() 83 | 84 | i.logf("exported list of invoked services: %v", names) 85 | 86 | return names 87 | } 88 | 89 | func (i *Injector) HealthCheck() map[string]error { 90 | i.mu.RLock() 91 | names := keys(i.services) 92 | i.mu.RUnlock() 93 | 94 | i.logf("requested healthcheck") 95 | 96 | results := map[string]error{} 97 | 98 | for _, name := range names { 99 | results[name] = i.healthcheckImplem(name) 100 | } 101 | 102 | i.logf("got healthcheck results: %v", results) 103 | 104 | return results 105 | } 106 | 107 | func (i *Injector) Shutdown() error { 108 | i.mu.RLock() 109 | invocations := invertMap(i.orderedInvocation) 110 | invocationIndex := i.orderedInvocationIndex 111 | i.mu.RUnlock() 112 | 113 | i.logf("requested shutdown") 114 | 115 | for index := invocationIndex; index >= 0; index-- { 116 | name, ok := invocations[index] 117 | if !ok { 118 | continue 119 | } 120 | 121 | err := i.shutdownImplem(name) 122 | if err != nil { 123 | return err 124 | } 125 | } 126 | 127 | i.logf("shutdowned services") 128 | 129 | return nil 130 | } 131 | 132 | // ShutdownOnSIGTERM listens for sigterm signal in order to graceful stop service. 133 | // It will block until receiving a sigterm signal. 134 | func (i *Injector) ShutdownOnSIGTERM() error { 135 | return i.ShutdownOnSignals(syscall.SIGTERM) 136 | } 137 | 138 | // ShutdownOnSignals listens for signals defined in signals parameter in order to graceful stop service. 139 | // It will block until receiving any of these signal. 140 | // If no signal is provided in signals parameter, syscall.SIGTERM will be added as default signal. 141 | func (i *Injector) ShutdownOnSignals(signals ...os.Signal) error { 142 | // Make sure there is at least syscall.SIGTERM as a signal 143 | if len(signals) < 1 { 144 | signals = append(signals, syscall.SIGTERM) 145 | } 146 | ch := make(chan os.Signal, 1) 147 | 148 | signal.Notify(ch, signals...) 149 | 150 | <-ch 151 | signal.Stop(ch) 152 | close(ch) 153 | 154 | return i.Shutdown() 155 | } 156 | 157 | func (i *Injector) healthcheckImplem(name string) error { 158 | i.mu.Lock() 159 | 160 | serviceAny, ok := i.services[name] 161 | if !ok { 162 | i.mu.Unlock() 163 | return fmt.Errorf("DI: could not find service `%s`", name) 164 | } 165 | 166 | i.mu.Unlock() 167 | 168 | service, ok := serviceAny.(healthcheckableService) 169 | if ok { 170 | i.logf("requested healthcheck for service %s", name) 171 | 172 | err := service.healthcheck() 173 | if err != nil { 174 | return err 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (i *Injector) shutdownImplem(name string) error { 182 | i.mu.Lock() 183 | 184 | serviceAny, ok := i.services[name] 185 | if !ok { 186 | i.mu.Unlock() 187 | return fmt.Errorf("DI: could not find service `%s`", name) 188 | } 189 | 190 | i.mu.Unlock() 191 | 192 | service, ok := serviceAny.(shutdownableService) 193 | if ok { 194 | i.logf("requested shutdown for service %s", name) 195 | 196 | err := service.shutdown() 197 | if err != nil { 198 | return err 199 | } 200 | } 201 | 202 | i.mu.Lock() 203 | delete(i.services, name) 204 | delete(i.orderedInvocation, name) 205 | i.mu.Unlock() 206 | 207 | i.onServiceShutdown(name) 208 | 209 | return nil 210 | } 211 | 212 | func (i *Injector) exists(name string) bool { 213 | i.mu.RLock() 214 | defer i.mu.RUnlock() 215 | 216 | _, ok := i.services[name] 217 | return ok 218 | } 219 | 220 | func (i *Injector) get(name string) (any, bool) { 221 | i.mu.RLock() 222 | defer i.mu.RUnlock() 223 | 224 | s, ok := i.services[name] 225 | return s, ok 226 | } 227 | 228 | func (i *Injector) set(name string, service any) { 229 | i.mu.Lock() 230 | defer i.mu.Unlock() 231 | 232 | i.services[name] = service 233 | 234 | // defering hook call will unlock mutex 235 | defer i.onServiceRegistration(name) 236 | } 237 | 238 | func (i *Injector) remove(name string) { 239 | i.mu.Lock() 240 | defer i.mu.Unlock() 241 | 242 | delete(i.services, name) 243 | } 244 | 245 | func (i *Injector) forEach(cb func(name string, service any)) { 246 | i.mu.Lock() 247 | defer i.mu.Unlock() 248 | 249 | for name, service := range i.services { 250 | cb(name, service) 251 | } 252 | } 253 | 254 | func (i *Injector) serviceNotFound(name string) error { 255 | // @TODO: use the Keys+Map functions from `golang.org/x/exp/maps` as 256 | // soon as it is released in stdlib. 257 | servicesNames := keys(i.services) 258 | servicesNames = mAp(servicesNames, func(name string) string { 259 | return fmt.Sprintf("`%s`", name) 260 | }) 261 | 262 | return fmt.Errorf("DI: could not find service `%s`, available services: %s", name, strings.Join(servicesNames, ", ")) 263 | } 264 | 265 | func (i *Injector) onServiceInvoke(name string) { 266 | i.mu.Lock() 267 | defer i.mu.Unlock() 268 | 269 | if _, ok := i.orderedInvocation[name]; !ok { 270 | i.orderedInvocation[name] = i.orderedInvocationIndex 271 | i.orderedInvocationIndex++ 272 | } 273 | } 274 | 275 | func (i *Injector) onServiceRegistration(name string) { 276 | if i.hookAfterRegistration != nil { 277 | i.hookAfterRegistration(i, name) 278 | } 279 | } 280 | 281 | func (i *Injector) onServiceShutdown(name string) { 282 | if i.hookAfterShutdown != nil { 283 | i.hookAfterShutdown(i, name) 284 | } 285 | } 286 | 287 | // Clone clones injector with provided services but not with invoked instances. 288 | func (i *Injector) Clone() *Injector { 289 | return i.CloneWithOpts(&InjectorOpts{}) 290 | } 291 | 292 | // CloneWithOpts clones injector with provided services but not with invoked instances, with options. 293 | func (i *Injector) CloneWithOpts(opts *InjectorOpts) *Injector { 294 | clone := NewWithOpts(opts) 295 | 296 | i.mu.RLock() 297 | defer i.mu.RUnlock() 298 | 299 | for name, serviceAny := range i.services { 300 | if service, ok := serviceAny.(cloneableService); ok { 301 | clone.services[name] = service.clone() 302 | } else { 303 | clone.services[name] = service 304 | } 305 | defer clone.onServiceRegistration(name) 306 | } 307 | 308 | i.logf("injector cloned") 309 | 310 | return clone 311 | } 312 | -------------------------------------------------------------------------------- /injector_example_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | type dbService struct { 9 | db *sql.DB 10 | } 11 | 12 | func (s *dbService) HealthCheck() error { 13 | return nil 14 | } 15 | 16 | func (s *dbService) Shutdown() error { 17 | return nil 18 | } 19 | 20 | func dbServiceProvider(i *Injector) (*dbService, error) { 21 | return &dbService{db: nil}, nil 22 | } 23 | 24 | func ExampleNew() { 25 | injector := New() 26 | 27 | ProvideNamedValue(injector, "PG_URI", "postgres://user:pass@host:5432/db") 28 | uri, err := InvokeNamed[string](injector, "PG_URI") 29 | 30 | fmt.Println(uri) 31 | fmt.Println(err) 32 | // Output: 33 | // postgres://user:pass@host:5432/db 34 | // 35 | } 36 | 37 | func ExampleDefaultInjector() { 38 | ProvideNamedValue(nil, "PG_URI", "postgres://user:pass@host:5432/db") 39 | uri, err := InvokeNamed[string](nil, "PG_URI") 40 | 41 | fmt.Println(uri) 42 | fmt.Println(err) 43 | // Output: 44 | // postgres://user:pass@host:5432/db 45 | // 46 | } 47 | 48 | func ExampleNewWithOpts() { 49 | injector := NewWithOpts(&InjectorOpts{ 50 | HookAfterShutdown: func(injector *Injector, serviceName string) { 51 | fmt.Printf("service shutdown: %s\n", serviceName) 52 | }, 53 | }) 54 | 55 | ProvideNamed(injector, "PG_URI", func(i *Injector) (string, error) { 56 | return "postgres://user:pass@host:5432/db", nil 57 | }) 58 | MustInvokeNamed[string](injector, "PG_URI") 59 | _ = injector.Shutdown() 60 | 61 | // Output: 62 | // service shutdown: PG_URI 63 | } 64 | 65 | func ExampleInjector_ListProvidedServices() { 66 | injector := New() 67 | 68 | ProvideNamedValue(injector, "PG_URI", "postgres://user:pass@host:5432/db") 69 | services := injector.ListProvidedServices() 70 | 71 | fmt.Println(services) 72 | // Output: 73 | // [PG_URI] 74 | } 75 | 76 | func ExampleInjector_ListInvokedServices_invoked() { 77 | injector := New() 78 | 79 | type test struct { 80 | foobar string 81 | } 82 | 83 | ProvideNamed(injector, "SERVICE_NAME", func(i *Injector) (test, error) { 84 | return test{foobar: "foobar"}, nil 85 | }) 86 | _, _ = InvokeNamed[test](injector, "SERVICE_NAME") 87 | services := injector.ListInvokedServices() 88 | 89 | fmt.Println(services) 90 | // Output: 91 | // [SERVICE_NAME] 92 | } 93 | 94 | func ExampleInjector_ListInvokedServices_notInvoked() { 95 | injector := New() 96 | 97 | type test struct { 98 | foobar string 99 | } 100 | 101 | ProvideNamed(injector, "SERVICE_NAME", func(i *Injector) (test, error) { 102 | return test{foobar: "foobar"}, nil 103 | }) 104 | services := injector.ListInvokedServices() 105 | 106 | fmt.Println(services) 107 | // Output: 108 | // [] 109 | } 110 | 111 | func ExampleInjector_HealthCheck() { 112 | injector := New() 113 | 114 | Provide(injector, dbServiceProvider) 115 | health := injector.HealthCheck() 116 | 117 | fmt.Println(health) 118 | // Output: 119 | // map[*do.dbService:] 120 | } 121 | 122 | func ExampleInjector_Shutdown() { 123 | injector := New() 124 | 125 | Provide(injector, dbServiceProvider) 126 | err := injector.Shutdown() 127 | 128 | fmt.Println(err) 129 | // Output: 130 | // 131 | } 132 | 133 | func ExampleInjector_Clone() { 134 | injector := New() 135 | 136 | ProvideNamedValue(injector, "PG_URI", "postgres://user:pass@host:5432/db") 137 | injector2 := injector.Clone() 138 | services := injector2.ListProvidedServices() 139 | 140 | fmt.Println(services) 141 | // Output: 142 | // [PG_URI] 143 | } 144 | -------------------------------------------------------------------------------- /injector_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestInjectorNew(t *testing.T) { 12 | is := assert.New(t) 13 | 14 | i := New() 15 | is.NotNil(i) 16 | is.Empty(i.services) 17 | } 18 | 19 | func TestInjectorNewWithOpts(t *testing.T) { 20 | is := assert.New(t) 21 | 22 | count := 0 23 | 24 | i := NewWithOpts(&InjectorOpts{ 25 | HookAfterRegistration: func(injector *Injector, serviceName string) { 26 | is.Equal("foobar", serviceName) 27 | count++ 28 | }, 29 | HookAfterShutdown: func(injector *Injector, serviceName string) { 30 | is.Equal("foobar", serviceName) 31 | count++ 32 | }, 33 | }) 34 | 35 | ProvideNamedValue(i, "foobar", 42) 36 | 37 | is.NotPanics(func() { 38 | MustInvokeNamed[int](i, "foobar") 39 | }) 40 | 41 | err := i.Shutdown() 42 | is.Nil(err) 43 | 44 | is.Equal(2, count) 45 | } 46 | 47 | func TestInjectorListProvidedServices(t *testing.T) { 48 | is := assert.New(t) 49 | 50 | i := New() 51 | 52 | is.NotPanics(func() { 53 | ProvideValue[int](i, 42) 54 | ProvideValue[float64](i, 21) 55 | }) 56 | 57 | is.NotPanics(func() { 58 | services := i.ListProvidedServices() 59 | is.ElementsMatch([]string{"int", "float64"}, services) 60 | }) 61 | } 62 | 63 | func TestInjectorListInvokedServices(t *testing.T) { 64 | is := assert.New(t) 65 | 66 | i := New() 67 | 68 | is.NotPanics(func() { 69 | ProvideValue[int](i, 42) 70 | ProvideValue[float64](i, 21) 71 | MustInvoke[int](i) 72 | }) 73 | 74 | is.NotPanics(func() { 75 | services := i.ListInvokedServices() 76 | is.Equal([]string{"int"}, services) 77 | }) 78 | } 79 | 80 | type testHealthCheck struct { 81 | } 82 | 83 | func (t *testHealthCheck) HealthCheck() error { 84 | return fmt.Errorf("broken") 85 | } 86 | 87 | func TestInjectorHealthCheck(t *testing.T) { 88 | is := assert.New(t) 89 | 90 | i := New() 91 | 92 | is.NotPanics(func() { 93 | ProvideValue[int](i, 42) 94 | ProvideNamed(i, "testHealthCheck", func(i *Injector) (*testHealthCheck, error) { 95 | return &testHealthCheck{}, nil 96 | }) 97 | }) 98 | 99 | // before invocation 100 | is.NotPanics(func() { 101 | got := i.HealthCheck() 102 | expected := map[string]error{ 103 | "int": nil, 104 | "testHealthCheck": nil, 105 | } 106 | 107 | is.Equal(expected, got) 108 | }) 109 | 110 | is.NotPanics(func() { 111 | MustInvokeNamed[int](i, "int") 112 | MustInvokeNamed[*testHealthCheck](i, "testHealthCheck") 113 | }) 114 | 115 | // after invocation 116 | is.NotPanics(func() { 117 | got := i.HealthCheck() 118 | expected := map[string]error{ 119 | "int": nil, 120 | "testHealthCheck": fmt.Errorf("broken"), 121 | } 122 | 123 | is.Equal(expected, got) 124 | }) 125 | } 126 | 127 | func TestInjectorExists(t *testing.T) { 128 | is := assert.New(t) 129 | 130 | i := New() 131 | 132 | service := &ServiceEager[int]{ 133 | name: "foobar", 134 | instance: 42, 135 | } 136 | i.services["foobar"] = service 137 | 138 | is.True(i.exists("foobar")) 139 | is.False(i.exists("foobaz")) 140 | } 141 | 142 | func TestInjectorGet(t *testing.T) { 143 | is := assert.New(t) 144 | 145 | i := New() 146 | 147 | service := &ServiceEager[int]{ 148 | name: "foobar", 149 | instance: 42, 150 | } 151 | i.services["foobar"] = service 152 | 153 | // existing service 154 | { 155 | s1, ok1 := i.get("foobar") 156 | is.True(ok1) 157 | is.NotEmpty(s1) 158 | if ok1 { 159 | s, ok := s1.(Service[int]) 160 | is.True(ok) 161 | 162 | v, err := s.getInstance(i) 163 | is.Nil(err) 164 | is.Equal(42, v) 165 | } 166 | } 167 | 168 | // not existing service 169 | { 170 | s2, ok2 := i.get("foobaz") 171 | is.False(ok2) 172 | is.Empty(s2) 173 | } 174 | } 175 | 176 | func TestInjectorSet(t *testing.T) { 177 | is := assert.New(t) 178 | 179 | i := New() 180 | 181 | service1 := &ServiceEager[int]{ 182 | name: "foobar", 183 | instance: 42, 184 | } 185 | 186 | service2 := &ServiceEager[int]{ 187 | name: "foobar", 188 | instance: 21, 189 | } 190 | 191 | i.set("foobar", service1) 192 | is.Len(i.services, 1) 193 | 194 | s1, ok1 := i.services["foobar"] 195 | is.True(ok1) 196 | is.True(reflect.DeepEqual(service1, s1)) 197 | 198 | // erase 199 | i.set("foobar", service2) 200 | is.Len(i.services, 1) 201 | 202 | s2, ok2 := i.services["foobar"] 203 | is.True(ok2) 204 | is.True(reflect.DeepEqual(service2, s2)) 205 | } 206 | 207 | func TestInjectorRemove(t *testing.T) { 208 | is := assert.New(t) 209 | 210 | i := New() 211 | 212 | service := &ServiceEager[int]{ 213 | name: "foobar", 214 | instance: 42, 215 | } 216 | 217 | i.set("foobar", service) 218 | is.Len(i.services, 1) 219 | i.remove("foobar") 220 | is.Len(i.services, 0) 221 | } 222 | 223 | func TestInjectorForEach(t *testing.T) { 224 | is := assert.New(t) 225 | 226 | i := New() 227 | 228 | service := &ServiceEager[int]{ 229 | name: "foobar", 230 | instance: 42, 231 | } 232 | i.set("foobar", service) 233 | 234 | count := 0 235 | 236 | i.forEach(func(name string, service any) { 237 | is.Equal("foobar", name) 238 | count++ 239 | }) 240 | 241 | is.Equal(1, count) 242 | } 243 | 244 | func TestInjectorServiceNotFound(t *testing.T) { 245 | is := assert.New(t) 246 | 247 | i := New() 248 | 249 | service1 := &ServiceEager[int]{ 250 | name: "foo", 251 | instance: 42, 252 | } 253 | 254 | service2 := &ServiceEager[int]{ 255 | name: "bar", 256 | instance: 21, 257 | } 258 | 259 | i.set("foo", service1) 260 | i.set("bar", service2) 261 | is.Len(i.services, 2) 262 | 263 | err := i.serviceNotFound("hello") 264 | is.ErrorContains(err, "DI: could not find service `hello`, available services:") 265 | is.ErrorContains(err, "`foo`") 266 | is.ErrorContains(err, "`bar`") 267 | } 268 | 269 | func TestInjectorOnServiceInvoke(t *testing.T) { 270 | is := assert.New(t) 271 | 272 | i := New() 273 | 274 | i.onServiceInvoke("foo") 275 | i.onServiceInvoke("bar") 276 | 277 | is.Equal(0, i.orderedInvocation["foo"]) 278 | is.Equal(1, i.orderedInvocation["bar"]) 279 | is.Equal(2, i.orderedInvocationIndex) 280 | } 281 | 282 | func TestInjectorCloneEager(t *testing.T) { 283 | is := assert.New(t) 284 | 285 | count := 0 286 | 287 | // setup original container 288 | i1 := New() 289 | ProvideNamedValue(i1, "foobar", 42) 290 | is.NotPanics(func() { 291 | value := MustInvokeNamed[int](i1, "foobar") 292 | is.Equal(42, value) 293 | }) 294 | 295 | // clone container 296 | i2 := i1.Clone() 297 | // invoked instance is not reused 298 | s1, err := InvokeNamed[int](i2, "foobar") 299 | is.NoError(err) 300 | is.Equal(42, s1) 301 | 302 | // service can be overridden 303 | OverrideNamed(i2, "foobar", func(_ *Injector) (int, error) { 304 | count++ 305 | return 6 * 9, nil 306 | }) 307 | s2, err := InvokeNamed[int](i2, "foobar") 308 | is.NoError(err) 309 | is.Equal(54, s2) 310 | is.Equal(1, count) 311 | } 312 | 313 | func TestInjectorCloneLazy(t *testing.T) { 314 | is := assert.New(t) 315 | 316 | count := 0 317 | 318 | // setup original container 319 | i1 := New() 320 | ProvideNamed(i1, "foobar", func(_ *Injector) (int, error) { 321 | count++ 322 | return 42, nil 323 | }) 324 | is.NotPanics(func() { 325 | value := MustInvokeNamed[int](i1, "foobar") 326 | is.Equal(42, value) 327 | }) 328 | is.Equal(1, count) 329 | 330 | // clone container 331 | i2 := i1.Clone() 332 | // invoked instance is not reused 333 | s1, err := InvokeNamed[int](i2, "foobar") 334 | is.NoError(err) 335 | is.Equal(42, s1) 336 | is.Equal(2, count) 337 | 338 | // service can be overridden 339 | OverrideNamed(i2, "foobar", func(_ *Injector) (int, error) { 340 | count++ 341 | return 6 * 9, nil 342 | }) 343 | s2, err := InvokeNamed[int](i2, "foobar") 344 | is.NoError(err) 345 | is.Equal(54, s2) 346 | is.Equal(3, count) 347 | } 348 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.uber.org/goleak" 7 | ) 8 | 9 | func TestMain(m *testing.M) { 10 | goleak.VerifyTestMain(m) 11 | } 12 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Service[T any] interface { 8 | getName() string 9 | getInstance(*Injector) (T, error) 10 | healthcheck() error 11 | shutdown() error 12 | clone() any 13 | } 14 | 15 | type healthcheckableService interface { 16 | healthcheck() error 17 | } 18 | 19 | type shutdownableService interface { 20 | shutdown() error 21 | } 22 | 23 | func generateServiceName[T any]() string { 24 | var t T 25 | 26 | // struct 27 | name := fmt.Sprintf("%T", t) 28 | if name != "" { 29 | return name 30 | } 31 | 32 | // interface 33 | return fmt.Sprintf("%T", new(T)) 34 | } 35 | 36 | type Healthcheckable interface { 37 | HealthCheck() error 38 | } 39 | 40 | type Shutdownable interface { 41 | Shutdown() error 42 | } 43 | 44 | type cloneableService interface { 45 | clone() any 46 | } 47 | -------------------------------------------------------------------------------- /service_eager.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | type ServiceEager[T any] struct { 4 | name string 5 | instance T 6 | } 7 | 8 | func newServiceEager[T any](name string, instance T) Service[T] { 9 | return &ServiceEager[T]{ 10 | name: name, 11 | instance: instance, 12 | } 13 | } 14 | 15 | //nolint:unused 16 | func (s *ServiceEager[T]) getName() string { 17 | return s.name 18 | } 19 | 20 | //nolint:unused 21 | func (s *ServiceEager[T]) getInstance(i *Injector) (T, error) { 22 | return s.instance, nil 23 | } 24 | 25 | func (s *ServiceEager[T]) healthcheck() error { 26 | instance, ok := any(s.instance).(Healthcheckable) 27 | if ok { 28 | return instance.HealthCheck() 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (s *ServiceEager[T]) shutdown() error { 35 | instance, ok := any(s.instance).(Shutdownable) 36 | if ok { 37 | return instance.Shutdown() 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (s *ServiceEager[T]) clone() any { 44 | return s 45 | } 46 | -------------------------------------------------------------------------------- /service_eager_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestServiceEagerName(t *testing.T) { 10 | is := assert.New(t) 11 | 12 | type test struct { 13 | foobar string 14 | } 15 | _test := test{foobar: "foobar"} 16 | 17 | service1 := newServiceEager("foobar", 42) 18 | is.Equal("foobar", service1.getName()) 19 | 20 | service2 := newServiceEager("foobar", _test) 21 | is.Equal("foobar", service2.getName()) 22 | } 23 | 24 | func TestServiceEagerInstance(t *testing.T) { 25 | is := assert.New(t) 26 | 27 | type test struct { 28 | foobar string 29 | } 30 | _test := test{foobar: "foobar"} 31 | 32 | service1 := newServiceEager("foobar", _test) 33 | is.Equal(&ServiceEager[test]{name: "foobar", instance: _test}, service1) 34 | 35 | instance1, err1 := service1.getInstance(nil) 36 | is.Nil(err1) 37 | is.Equal(_test, instance1) 38 | 39 | service2 := newServiceEager("foobar", 42) 40 | is.Equal(&ServiceEager[int]{name: "foobar", instance: 42}, service2) 41 | 42 | instance2, err2 := service2.getInstance(nil) 43 | is.Nil(err2) 44 | is.Equal(42, instance2) 45 | } 46 | -------------------------------------------------------------------------------- /service_lazy.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Provider[T any] func(*Injector) (T, error) 8 | 9 | type ServiceLazy[T any] struct { 10 | mu sync.RWMutex 11 | name string 12 | instance T 13 | 14 | // lazy loading 15 | built bool 16 | provider Provider[T] 17 | } 18 | 19 | func newServiceLazy[T any](name string, provider Provider[T]) Service[T] { 20 | return &ServiceLazy[T]{ 21 | name: name, 22 | 23 | built: false, 24 | provider: provider, 25 | } 26 | } 27 | 28 | //nolint:unused 29 | func (s *ServiceLazy[T]) getName() string { 30 | return s.name 31 | } 32 | 33 | //nolint:unused 34 | func (s *ServiceLazy[T]) getInstance(i *Injector) (T, error) { 35 | s.mu.Lock() 36 | defer s.mu.Unlock() 37 | 38 | if !s.built { 39 | err := s.build(i) 40 | if err != nil { 41 | return empty[T](), err 42 | } 43 | } 44 | 45 | return s.instance, nil 46 | } 47 | 48 | //nolint:unused 49 | func (s *ServiceLazy[T]) build(i *Injector) (err error) { 50 | defer func() { 51 | if r := recover(); r != nil { 52 | if e, ok := r.(error); ok { 53 | err = e 54 | } else { 55 | panic(r) 56 | } 57 | } 58 | }() 59 | 60 | instance, err := s.provider(i) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | s.instance = instance 66 | s.built = true 67 | 68 | return nil 69 | } 70 | 71 | func (s *ServiceLazy[T]) healthcheck() error { 72 | s.mu.RLock() 73 | defer s.mu.RUnlock() 74 | 75 | if !s.built { 76 | return nil 77 | } 78 | 79 | instance, ok := any(s.instance).(Healthcheckable) 80 | if ok { 81 | return instance.HealthCheck() 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func (s *ServiceLazy[T]) shutdown() error { 88 | s.mu.Lock() 89 | defer s.mu.Unlock() 90 | 91 | if !s.built { 92 | return nil 93 | } 94 | 95 | instance, ok := any(s.instance).(Shutdownable) 96 | if ok { 97 | err := instance.Shutdown() 98 | if err != nil { 99 | return err 100 | } 101 | } 102 | 103 | s.built = false 104 | s.instance = empty[T]() 105 | 106 | return nil 107 | } 108 | 109 | func (s *ServiceLazy[T]) clone() any { 110 | // reset `build` flag and instance 111 | return &ServiceLazy[T]{ 112 | name: s.name, 113 | 114 | built: false, 115 | provider: s.provider, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /service_lazy_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type lazyTest struct { 11 | idx int 12 | err error 13 | } 14 | 15 | func (t *lazyTest) Shutdown() error { 16 | return t.err 17 | } 18 | 19 | func TestServiceLazyName(t *testing.T) { 20 | is := assert.New(t) 21 | 22 | type test struct { 23 | foobar string 24 | } 25 | _test := test{foobar: "foobar"} 26 | 27 | provider1 := func(i *Injector) (int, error) { 28 | return 42, nil 29 | } 30 | provider2 := func(i *Injector) (test, error) { 31 | return _test, nil 32 | } 33 | 34 | service1 := newServiceLazy("foobar", provider1) 35 | is.Equal("foobar", service1.getName()) 36 | 37 | service2 := newServiceLazy("foobar", provider2) 38 | is.Equal("foobar", service2.getName()) 39 | } 40 | 41 | func TestServiceLazyInstance(t *testing.T) { 42 | is := assert.New(t) 43 | 44 | type test struct { 45 | foobar string 46 | } 47 | _test := test{foobar: "foobar"} 48 | 49 | provider1 := func(i *Injector) (int, error) { 50 | return 42, nil 51 | } 52 | provider2 := func(i *Injector) (test, error) { 53 | return _test, nil 54 | } 55 | provider3 := func(i *Injector) (int, error) { 56 | panic("error") 57 | } 58 | provider4 := func(i *Injector) (int, error) { 59 | panic(fmt.Errorf("error")) 60 | } 61 | provider5 := func(i *Injector) (int, error) { 62 | return 42, fmt.Errorf("error") 63 | } 64 | 65 | i := New() 66 | 67 | service1 := newServiceLazy("foobar", provider1) 68 | instance1, err1 := service1.getInstance(i) 69 | is.Nil(err1) 70 | is.Equal(42, instance1) 71 | 72 | service2 := newServiceLazy("hello", provider2) 73 | instance2, err2 := service2.getInstance(i) 74 | is.Nil(err2) 75 | is.Equal(_test, instance2) 76 | 77 | is.Panics(func() { 78 | service3 := newServiceLazy("baz", provider3) 79 | _, _ = service3.getInstance(i) 80 | }) 81 | 82 | is.NotPanics(func() { 83 | service4 := newServiceLazy("plop", provider4) 84 | instance4, err4 := service4.getInstance(i) 85 | is.NotNil(err4) 86 | is.Empty(instance4) 87 | expected := fmt.Errorf("error") 88 | is.Equal(expected, err4) 89 | }) 90 | 91 | is.NotPanics(func() { 92 | service5 := newServiceLazy("plop", provider5) 93 | instance5, err5 := service5.getInstance(i) 94 | is.NotNil(err5) 95 | is.Empty(instance5) 96 | expected := fmt.Errorf("error") 97 | is.Equal(expected, err5) 98 | }) 99 | } 100 | 101 | func TestServiceLazyInstanceShutDown(t *testing.T) { 102 | is := assert.New(t) 103 | 104 | index := 1 105 | provider1 := func(i *Injector) (*lazyTest, error) { 106 | index++ 107 | return &lazyTest{index, nil}, nil 108 | } 109 | provider2 := func(i *Injector) (*lazyTest, error) { 110 | index++ 111 | return &lazyTest{index, assert.AnError}, nil 112 | } 113 | 114 | i := New() 115 | 116 | service1 := newServiceLazy("foobar", provider1) 117 | instance1, err := service1.getInstance(i) 118 | is.Nil(err) 119 | is.True(service1.(*ServiceLazy[*lazyTest]).built) 120 | err = service1.shutdown() 121 | is.False(service1.(*ServiceLazy[*lazyTest]).built) 122 | is.Nil(err) 123 | instance2, err := service1.getInstance(i) 124 | is.Nil(err) 125 | is.NotEqual(instance1.idx, instance2.idx) 126 | is.Equal(instance1.idx+1, instance2.idx) 127 | 128 | service2 := newServiceLazy("foobar", provider2).(*ServiceLazy[*lazyTest]) 129 | is.False(service2.built) 130 | is.Nil(err) 131 | err = service2.build(i) 132 | is.Nil(err) 133 | is.True(service2.built) 134 | err = service2.shutdown() 135 | is.Error(assert.AnError, err) 136 | is.True(service2.built) 137 | } 138 | -------------------------------------------------------------------------------- /service_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGenerateServiceName(t *testing.T) { 10 | is := assert.New(t) 11 | 12 | type test struct{} //nolint:unused 13 | 14 | name := generateServiceName[test]() 15 | is.Equal("do.test", name) 16 | 17 | name = generateServiceName[*test]() 18 | is.Equal("*do.test", name) 19 | 20 | name = generateServiceName[int]() 21 | is.Equal("int", name) 22 | } 23 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | func empty[T any]() (t T) { 4 | return 5 | } 6 | 7 | func must(err error) { 8 | if err != nil { 9 | panic(err) 10 | } 11 | } 12 | 13 | func keys[K comparable, V any](in map[K]V) []K { 14 | result := make([]K, 0, len(in)) 15 | 16 | for k := range in { 17 | result = append(result, k) 18 | } 19 | 20 | return result 21 | } 22 | 23 | func mAp[T any, R any](collection []T, iteratee func(T) R) []R { 24 | result := make([]R, len(collection)) 25 | 26 | for i, item := range collection { 27 | result[i] = iteratee(item) 28 | } 29 | 30 | return result 31 | } 32 | 33 | func invertMap[K comparable, V comparable](in map[K]V) map[V]K { 34 | out := map[V]K{} 35 | 36 | for k, v := range in { 37 | out[v] = k 38 | } 39 | 40 | return out 41 | } 42 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package do 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestUtilsEmpty(t *testing.T) { 11 | is := assert.New(t) 12 | 13 | value1 := empty[int]() 14 | is.Empty(value1) 15 | 16 | value2 := empty[*int]() 17 | is.Nil(value2) 18 | is.Empty(value2) 19 | } 20 | 21 | func TestUtilsMust(t *testing.T) { 22 | is := assert.New(t) 23 | 24 | is.Panics(func() { 25 | must(fmt.Errorf("error")) 26 | }) 27 | is.NotPanics(func() { 28 | must(nil) 29 | }) 30 | } 31 | --------------------------------------------------------------------------------