├── CODEOWNERS ├── .codecov.yml ├── docs ├── _templates │ ├── breadcrumbs.html │ ├── footer.html │ └── layout.html ├── .gitignore ├── requirements.txt ├── Makefile ├── getting-started │ ├── 01-installation.md │ ├── index.md │ ├── 02-first-container.md │ ├── 06-next-steps.md │ ├── 03-adding-services.md │ └── 04-using-lifetimes.md ├── _static │ └── customize.css ├── conf.py ├── index.rst ├── features │ ├── parameter-objects.md │ ├── keyed-services.md │ ├── interface-binding.md │ ├── resource-cleanup.md │ ├── result-objects.md │ └── service-groups.md └── concepts │ ├── how-it-works.md │ ├── modules.md │ └── scopes.md ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── pr-check.yml │ ├── pr-labeler.yml │ ├── test.yml │ ├── tag.yml │ └── release.yml └── pull_request_template.md ├── go.mod ├── benchmarks ├── go.mod └── go.sum ├── chi ├── go.mod ├── go.sum └── chi.go ├── http ├── go.mod ├── go.sum └── http.go ├── .readthedocs.yaml ├── echo ├── go.mod ├── go.sum └── echo.go ├── go.sum ├── .gitignore ├── fiber ├── go.mod └── go.sum ├── LICENSE ├── internal └── graph │ └── errors.go ├── .golangci.yml ├── gin ├── go.mod ├── gin.go └── go.sum ├── lifetime.go ├── inout.go ├── lifetime_test.go ├── CONTRIBUTING.md └── provider_test.go /CODEOWNERS: -------------------------------------------------------------------------------- 1 | 2 | * @junioryono -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 80..100 3 | round: down 4 | precision: 2 5 | 6 | status: 7 | project: 8 | default: 9 | enabled: yes 10 | target: 85% 11 | if_not_found: success 12 | if_ci_failed: error 13 | -------------------------------------------------------------------------------- /docs/_templates/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {% extends "!breadcrumbs.html" %} 2 | 3 | {% block breadcrumbs %} 4 | {% if announcement %} 5 |
6 |
{{ announcement | safe }}
7 |
8 | {% endif %} 9 | {{ super() }} 10 | {% endblock %} -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Sphinx build artifacts 2 | _build/ 3 | _venv/ 4 | __pycache__/ 5 | *.pyc 6 | 7 | # OS files 8 | .DS_Store 9 | Thumbs.db 10 | 11 | # Editor files 12 | *.swp 13 | *.swo 14 | *~ 15 | .vscode/ 16 | .idea/ 17 | 18 | # Environment 19 | .env 20 | .env.local -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "chore" 9 | prefix-development: "chore" 10 | include: "scope" 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junioryono/godi/v4 2 | 3 | go 1.24.6 4 | 5 | require github.com/stretchr/testify v1.11.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junioryono/godi/v4/benchmarks 2 | 3 | go 1.24.6 4 | 5 | require ( 6 | github.com/junioryono/godi/v4 v4.0.0 7 | github.com/samber/do/v2 v2.0.0 8 | go.uber.org/dig v1.19.0 9 | ) 10 | 11 | require github.com/samber/go-type-to-string v1.8.0 // indirect 12 | 13 | replace github.com/junioryono/godi/v4 => ../ 14 | -------------------------------------------------------------------------------- /chi/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junioryono/godi/v4/chi 2 | 3 | go 1.24.6 4 | 5 | require ( 6 | github.com/junioryono/godi/v4 v4.0.0 7 | github.com/stretchr/testify v1.11.1 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | 16 | replace github.com/junioryono/godi/v4 => ../ 17 | -------------------------------------------------------------------------------- /http/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junioryono/godi/v4/http 2 | 3 | go 1.24.6 4 | 5 | require ( 6 | github.com/junioryono/godi/v4 v4.0.0 7 | github.com/stretchr/testify v1.11.1 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | 16 | replace github.com/junioryono/godi/v4 => ../ 17 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version, and other tools you might need 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.13" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # Optionally, but recommended, 18 | # declare the Python requirements required to build your documentation 19 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Babel==2.17.0 2 | Jinja2==3.1.6 3 | MarkupSafe==3.0.2 4 | Pygments==2.19.1 5 | Sphinx==7.4.7 6 | certifi==2025.1.31 7 | chardet==5.2.0 8 | commonmark==0.9.1 9 | docutils==0.20.1 10 | idna==3.10 11 | imagesize==1.4.1 12 | myst-parser==3.0.1 13 | packaging==25.0 14 | pyparsing==3.2.3 15 | pytz==2025.2 16 | requests==2.32.3 17 | snowballstemmer==2.2.0 18 | sphinx-favicon==1.0.1 19 | sphinx-rtd-theme==3.0.2 20 | sphinx-copybutton==0.5.2 21 | sphinxcontrib-applehelp==2.0.0 22 | sphinxcontrib-devhelp==2.0.0 23 | sphinxcontrib-htmlhelp==2.1.0 24 | sphinxcontrib-jsmath==1.0.1 25 | sphinxcontrib-qthelp==2.0.0 26 | sphinxcontrib-serializinghtml==2.0.0 27 | sphinxext-rediraffe==0.2.7 28 | urllib3==2.4.0 -------------------------------------------------------------------------------- /docs/_templates/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | ```go 16 | // Minimal code example that reproduces the issue 17 | ``` 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Actual behavior** 23 | What actually happened, including any error messages or stack traces. 24 | 25 | **Environment:** 26 | 27 | - Go version: [e.g. 1.25.0] 28 | - godi version: [e.g. v1.0.0] 29 | - OS: [e.g. Ubuntu 22.04, Windows 11, macOS 14] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Example usage** 19 | Show how the feature would be used: 20 | 21 | ```go 22 | // Example code showing the proposed API 23 | ``` 24 | 25 | **Additional context** 26 | Add any other context or screenshots about the feature request here. 27 | -------------------------------------------------------------------------------- /echo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junioryono/godi/v4/echo 2 | 3 | go 1.24.6 4 | 5 | require ( 6 | github.com/junioryono/godi/v4 v4.0.0 7 | github.com/labstack/echo/v4 v4.13.3 8 | github.com/stretchr/testify v1.11.1 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/labstack/gommon v0.4.2 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/valyala/bytebufferpool v1.0.0 // indirect 18 | github.com/valyala/fasttemplate v1.2.2 // indirect 19 | golang.org/x/crypto v0.31.0 // indirect 20 | golang.org/x/net v0.33.0 // indirect 21 | golang.org/x/sys v0.28.0 // indirect 22 | golang.org/x/text v0.21.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | 26 | replace github.com/junioryono/godi/v4 => ../ 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 6 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /chi/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 6 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /http/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 6 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories 15 | vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | go.work.sum 20 | 21 | # IDE directories 22 | .idea/ 23 | .vscode/ 24 | *.swp 25 | *.swo 26 | *~ 27 | 28 | # OS generated files 29 | .DS_Store 30 | .DS_Store? 31 | ._* 32 | .Spotlight-V100 33 | .Trashes 34 | ehthumbs.db 35 | Thumbs.db 36 | 37 | # Coverage reports 38 | coverage.txt 39 | coverage.html 40 | *.coverprofile 41 | 42 | # Profiling data 43 | *.prof 44 | *.png 45 | 46 | # Benchmark results 47 | benchmark.txt 48 | 49 | # Local environment files 50 | .env 51 | .env.local 52 | 53 | # Build directories 54 | dist/ 55 | build/ 56 | 57 | # Temporary files 58 | *.tmp 59 | *.temp 60 | *.sarif -------------------------------------------------------------------------------- /fiber/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junioryono/godi/v4/fiber 2 | 3 | go 1.24.6 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.52.6 7 | github.com/junioryono/godi/v4 v4.0.0 8 | github.com/stretchr/testify v1.11.1 9 | ) 10 | 11 | require ( 12 | github.com/andybalholm/brotli v1.1.0 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/google/uuid v1.6.0 // indirect 15 | github.com/klauspost/compress v1.17.9 // indirect 16 | github.com/mattn/go-colorable v0.1.13 // indirect 17 | github.com/mattn/go-isatty v0.0.20 // indirect 18 | github.com/mattn/go-runewidth v0.0.16 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | github.com/valyala/bytebufferpool v1.0.0 // indirect 22 | github.com/valyala/fasthttp v1.51.0 // indirect 23 | github.com/valyala/tcplisten v1.0.0 // indirect 24 | golang.org/x/sys v0.28.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | 28 | replace github.com/junioryono/godi/v4 => ../ 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | VENVDIR = _venv 8 | BINDIR = $(VENVDIR)/bin 9 | SPHINXBUILD = $(BINDIR)/sphinx-build 10 | SOURCEDIR = . 11 | BUILDDIR = _build 12 | 13 | # Put it first so that "make" without argument is like "make help". 14 | help: $(SPHINXBUILD) 15 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 16 | 17 | $(SPHINXBUILD): $(VENVDIR) 18 | $(VENVDIR)/bin/pip install -r requirements.txt 19 | 20 | $(VENVDIR): 21 | python3 -m venv $(VENVDIR) 22 | 23 | .PHONY: help Makefile clean 24 | 25 | clean: 26 | rm -rf $(BUILDDIR) 27 | rm -rf $(VENVDIR) 28 | 29 | # Catch-all target: route all unknown targets to Sphinx using the new 30 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 31 | %: Makefile 32 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 junioryono 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/samber/do/v2 v2.0.0 h1:tnunwWaoqSfJ9hxVIaJawIo7JXHQlqT9d9YBXlE9Keg= 6 | github.com/samber/do/v2 v2.0.0/go.mod h1:ZSBCE7Xr6nTNIOVo4DBrkl2+ydUbIOzJjjdV8En5XO4= 7 | github.com/samber/go-type-to-string v1.8.0 h1:5z6tDTjtXxkIAoAuHAZYMYR8mkBZjVgeSH7jcSLqc8w= 8 | github.com/samber/go-type-to-string v1.8.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU= 9 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 10 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 11 | go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= 12 | go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /internal/graph/errors.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // CircularDependencyError represents a circular dependency in the container. 9 | type CircularDependencyError struct { 10 | Node NodeKey 11 | Path []NodeKey 12 | } 13 | 14 | func (e CircularDependencyError) Error() string { 15 | var b strings.Builder 16 | b.WriteString("circular dependency detected:\n\n") 17 | 18 | if len(e.Path) == 0 { 19 | b.WriteString(fmt.Sprintf(" %s\n", e.Node.String())) 20 | b.WriteString(" ↓\n") 21 | b.WriteString(fmt.Sprintf(" %s (cycle)\n", e.Node.String())) 22 | } else { 23 | // Build a visual representation of the cycle 24 | for i, node := range e.Path { 25 | b.WriteString(fmt.Sprintf(" %s\n", node.String())) 26 | if i < len(e.Path)-1 { 27 | b.WriteString(" ↓\n") 28 | } 29 | } 30 | // Show the cycle back to the first node 31 | if len(e.Path) > 0 { 32 | b.WriteString(" ↓\n") 33 | b.WriteString(fmt.Sprintf(" %s (cycle)\n", e.Path[0].String())) 34 | } 35 | } 36 | 37 | b.WriteString("\nTo resolve this:\n") 38 | b.WriteString(" • Use an interface to break the dependency\n") 39 | b.WriteString(" • Use a factory function for lazy initialization\n") 40 | b.WriteString(" • Restructure to remove the circular relationship\n") 41 | 42 | return b.String() 43 | } 44 | -------------------------------------------------------------------------------- /docs/getting-started/01-installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Install godi 4 | 5 | ```bash 6 | go get github.com/junioryono/godi/v4 7 | ``` 8 | 9 | ## Verify It Works 10 | 11 | Create a file called `main.go`: 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "github.com/junioryono/godi/v4" 19 | ) 20 | 21 | func main() { 22 | services := godi.NewCollection() 23 | fmt.Println("godi is ready!") 24 | } 25 | ``` 26 | 27 | Run it: 28 | 29 | ```bash 30 | go run main.go 31 | ``` 32 | 33 | You should see: 34 | 35 | ``` 36 | godi is ready! 37 | ``` 38 | 39 | ## Requirements 40 | 41 | - **Go 1.21+** - godi uses generics for type safety 42 | - **No code generation** - godi works at runtime, no build steps needed 43 | - **No dependencies** - the core library has zero external dependencies 44 | 45 | ## Framework Integrations (Optional) 46 | 47 | If you're using a web framework, install the corresponding integration: 48 | 49 | ```bash 50 | # For Gin 51 | go get github.com/junioryono/godi/v4/gin 52 | 53 | # For Chi 54 | go get github.com/junioryono/godi/v4/chi 55 | 56 | # For Echo 57 | go get github.com/junioryono/godi/v4/echo 58 | 59 | # For Fiber 60 | go get github.com/junioryono/godi/v4/fiber 61 | 62 | # For net/http 63 | go get github.com/junioryono/godi/v4/http 64 | ``` 65 | 66 | --- 67 | 68 | **Next:** [Create your first container](02-first-container.md) 69 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | {{ super() }} 6 | {% endblock %} 7 | 8 | {% block menu %} 9 | {{ super() }} 10 | 11 |

Quick Links

12 | 29 | 30 |

31 | 32 | Community 33 | 34 |

35 | 36 | 46 | {% endblock %} -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: PR Check 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize] 6 | 7 | permissions: 8 | pull-requests: write 9 | statuses: write 10 | 11 | jobs: 12 | conventional-commits: 13 | name: Validate PR Title 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check PR title 17 | uses: amannn/action-semantic-pull-request@v6 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | # Configure allowed types 22 | types: | 23 | feat 24 | fix 25 | docs 26 | style 27 | refactor 28 | perf 29 | test 30 | build 31 | ci 32 | chore 33 | revert 34 | # Configure allowed scopes (optional) 35 | scopes: | 36 | provider 37 | collection 38 | module 39 | lifetime 40 | descriptor 41 | errors 42 | inout 43 | scope 44 | resolver 45 | deps 46 | docs 47 | # Require scope to be provided 48 | requireScope: false 49 | # Subject requirements 50 | subjectPattern: ^(?![A-Z]).+$ 51 | subjectPatternError: | 52 | The subject "{subject}" found in the pull request title "{title}" 53 | didn't match the configured pattern. Please ensure that the subject 54 | doesn't start with an uppercase character. 55 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## PR Title Format 2 | 3 | Your PR title must follow the format: `type(scope): description` 4 | 5 | **Allowed types:** 6 | 7 | - `feat`: New feature 8 | - `fix`: Bug fix 9 | - `docs`: Documentation changes 10 | - `style`: Code style changes (formatting, missing semicolons, etc) 11 | - `refactor`: Code change that neither fixes a bug nor adds a feature 12 | - `perf`: Performance improvements 13 | - `test`: Adding or updating tests 14 | - `build`: Changes to build system or dependencies 15 | - `ci`: Changes to CI configuration files and scripts 16 | - `chore`: Other changes that don't modify src or test files 17 | - `revert`: Reverts a previous commit 18 | 19 | **Example:** `feat(provider): add support for singleton lifetime` 20 | 21 | ## What does this PR do? 22 | 23 | 24 | 25 | ## Type of change 26 | 27 | 28 | 29 | - [ ] 🐛 Bug fix (fixes an issue) 30 | - [ ] ✨ New feature (adds functionality) 31 | - [ ] 🔨 Refactor (code change that neither fixes a bug nor adds a feature) 32 | - [ ] 📝 Documentation (changes to documentation only) 33 | - [ ] 🧪 Test (adding or updating tests) 34 | - [ ] 🏗️ Build/CI (changes to build process or CI) 35 | 36 | ## Related Issue 37 | 38 | 39 | 40 | ## Checklist 41 | 42 | - [ ] My code follows the project style guidelines 43 | - [ ] I have tested my changes 44 | - [ ] I have updated documentation (if needed) 45 | 46 | ## How to test 47 | 48 | 49 | 50 | ## Additional notes 51 | 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | label: 13 | name: Label PR based on type 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Label PR based on title 17 | uses: actions/github-script@v7 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | script: | 21 | const title = context.payload.pull_request.title.toLowerCase(); 22 | const labels = []; 23 | 24 | // Determine type based on conventional commit format 25 | if (title.startsWith('feat')) { 26 | labels.push('type: feature'); 27 | } else if (title.startsWith('fix')) { 28 | labels.push('type: bug'); 29 | } else if (title.startsWith('docs')) { 30 | labels.push('type: documentation'); 31 | } else if (title.match(/^(build|chore|ci|perf|refactor|revert|style|test)/)) { 32 | labels.push('type: maintenance'); 33 | } 34 | 35 | // Check for breaking changes 36 | if (title.includes('!:') || title.includes('breaking')) { 37 | labels.push('type: breaking'); 38 | } 39 | 40 | // Add labels if any were determined 41 | if (labels.length > 0) { 42 | await github.rest.issues.addLabels({ 43 | owner: context.repo.owner, 44 | repo: context.repo.repo, 45 | issue_number: context.issue.number, 46 | labels: labels 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | go: 1.25.0 4 | linters: 5 | enable: 6 | - copyloopvar 7 | - gocritic 8 | - gocyclo 9 | disable: 10 | - exhaustive 11 | - gochecknoglobals 12 | - gochecknoinits 13 | - nestif 14 | settings: 15 | funlen: 16 | lines: 100 17 | statements: 50 18 | gocritic: 19 | enabled-tags: 20 | - diagnostic 21 | - experimental 22 | - opinionated 23 | - performance 24 | - style 25 | gocyclo: 26 | min-complexity: 30 27 | gomodguard: 28 | blocked: 29 | modules: 30 | - github.com/pkg/errors: 31 | recommendations: 32 | - errors 33 | - fmt 34 | reason: use standard library errors package 35 | govet: 36 | enable: 37 | - shadow 38 | exclusions: 39 | generated: lax 40 | presets: 41 | - comments 42 | - common-false-positives 43 | - legacy 44 | - std-error-handling 45 | rules: 46 | - linters: 47 | - errcheck 48 | - funlen 49 | - gochecknoglobals 50 | - goconst 51 | - gocyclo 52 | - gosec 53 | path: _test\.go 54 | - linters: 55 | - unused 56 | path: vendor/ 57 | - linters: 58 | - unused 59 | text: is unused 60 | source: ^\s*\w+\s+interface\s*{ 61 | paths: 62 | - third_party$ 63 | - builtin$ 64 | - examples$ 65 | issues: 66 | max-issues-per-linter: 0 67 | max-same-issues: 0 68 | new: false 69 | formatters: 70 | enable: 71 | - gofmt 72 | exclusions: 73 | generated: lax 74 | paths: 75 | - third_party$ 76 | - builtin$ 77 | - examples$ 78 | -------------------------------------------------------------------------------- /gin/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junioryono/godi/v4/gin 2 | 3 | go 1.24.6 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/junioryono/godi/v4 v4.0.0 8 | github.com/stretchr/testify v1.11.1 9 | ) 10 | 11 | require ( 12 | github.com/bytedance/sonic v1.11.6 // indirect 13 | github.com/bytedance/sonic/loader v0.1.1 // indirect 14 | github.com/cloudwego/base64x v0.1.4 // indirect 15 | github.com/cloudwego/iasm v0.2.0 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 18 | github.com/gin-contrib/sse v0.1.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-playground/validator/v10 v10.20.0 // indirect 22 | github.com/goccy/go-json v0.10.2 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.2.12 // indirect 33 | golang.org/x/arch v0.8.0 // indirect 34 | golang.org/x/crypto v0.23.0 // indirect 35 | golang.org/x/net v0.25.0 // indirect 36 | golang.org/x/sys v0.20.0 // indirect 37 | golang.org/x/text v0.15.0 // indirect 38 | google.golang.org/protobuf v1.34.1 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | 42 | replace github.com/junioryono/godi/v4 => ../ 43 | -------------------------------------------------------------------------------- /docs/_static/customize.css: -------------------------------------------------------------------------------- 1 | /* godi custom styles */ 2 | 3 | .wy-side-nav-search { 4 | background-color: #2980b9; 5 | } 6 | 7 | .wy-side-nav-search img { 8 | padding: 5px 40px !important; 9 | max-width: 200px; 10 | } 11 | 12 | .wy-nav-content { 13 | max-width: 1200px; 14 | } 15 | 16 | /* Better code blocks */ 17 | .highlight { 18 | background: #f5f5f5; 19 | border: 1px solid #e1e4e5; 20 | border-radius: 4px; 21 | } 22 | 23 | .highlight-go { 24 | margin: 1em 0; 25 | } 26 | 27 | /* Banner for announcements */ 28 | #announcement { 29 | text-align: center; 30 | background: #2980b9; 31 | border: 1px solid rgb(52, 49, 49); 32 | color: #f0f0f4; 33 | padding: 10px; 34 | margin-bottom: 1.618em; 35 | } 36 | 37 | #announcement > div > a { 38 | color: #f0f0f4; 39 | text-decoration: underline; 40 | } 41 | 42 | /* Feature boxes */ 43 | .feature-box { 44 | border: 1px solid #e1e4e5; 45 | border-radius: 4px; 46 | padding: 20px; 47 | margin: 20px 0; 48 | background: #f8f8f8; 49 | } 50 | 51 | .feature-box h3 { 52 | margin-top: 0; 53 | color: #2980b9; 54 | } 55 | 56 | /* Warning and note boxes */ 57 | .admonition { 58 | border-radius: 4px; 59 | } 60 | 61 | .admonition.note { 62 | background-color: #e3f2fd; 63 | border-left: 4px solid #2196f3; 64 | } 65 | 66 | .admonition.warning { 67 | background-color: #fff3e0; 68 | border-left: 4px solid #ff9800; 69 | } 70 | 71 | .admonition.tip { 72 | background-color: #e8f5e9; 73 | border-left: 4px solid #4caf50; 74 | } 75 | 76 | /* Improve tables */ 77 | .wy-table-responsive table td, 78 | .wy-table-responsive table th { 79 | white-space: normal; 80 | } 81 | 82 | /* Version selector */ 83 | .version-selector { 84 | margin: 10px 0; 85 | padding: 10px; 86 | background: #f0f0f0; 87 | border-radius: 4px; 88 | } 89 | 90 | /* Copy button styling */ 91 | .copybtn { 92 | transition: opacity 0.3s; 93 | } 94 | 95 | /* Sponsorship section */ 96 | #sponsorship { 97 | margin-top: 20px; 98 | padding: 15px; 99 | background: #f8f8f8; 100 | border-radius: 4px; 101 | text-align: center; 102 | } 103 | 104 | #sponsorship > img { 105 | width: 100%; 106 | max-width: 200px; 107 | } 108 | 109 | -------------------------------------------------------------------------------- /docs/getting-started/index.md: -------------------------------------------------------------------------------- 1 | # Get Started in 5 Minutes 2 | 3 | godi is a dependency injection library that automatically wires up your Go applications. Define your services, specify their lifetimes, and let godi handle the rest. 4 | 5 | **What you'll learn:** 6 | 7 | 1. **Create a container** - Where your services live 8 | 2. **Register services** - Tell godi about your types 9 | 3. **Resolve dependencies** - Let godi wire everything together 10 | 4. **Use lifetimes** - Control when instances are created 11 | 5. **Add HTTP integration** - Build web applications 12 | 13 | ## Before You Start 14 | 15 | You need: 16 | 17 | - Go 1.21 or later 18 | - A text editor 19 | - 5 minutes 20 | 21 | ## Tutorial Overview 22 | 23 | | Page | Time | What You'll Build | 24 | | ------------------------------------------ | ------ | -------------------------- | 25 | | [Installation](01-installation.md) | 30 sec | Install godi | 26 | | [First Container](02-first-container.md) | 60 sec | Create and use a container | 27 | | [Adding Services](03-adding-services.md) | 90 sec | Wire up real services | 28 | | [Using Lifetimes](04-using-lifetimes.md) | 90 sec | Control instance creation | 29 | | [HTTP Integration](05-http-integration.md) | 90 sec | Build a web server | 30 | | [Next Steps](06-next-steps.md) | 30 sec | Where to go from here | 31 | 32 | ## Quick Preview 33 | 34 | Here's what a complete godi application looks like: 35 | 36 | ```go 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | "github.com/junioryono/godi/v4" 42 | ) 43 | 44 | // Your services - normal Go types 45 | type Logger struct{} 46 | func (l *Logger) Log(msg string) { fmt.Println(msg) } 47 | 48 | type UserService struct { 49 | logger *Logger 50 | } 51 | func NewUserService(logger *Logger) *UserService { 52 | return &UserService{logger: logger} 53 | } 54 | 55 | func main() { 56 | // Register services 57 | services := godi.NewCollection() 58 | services.AddSingleton(func() *Logger { return &Logger{} }) 59 | services.AddSingleton(NewUserService) 60 | 61 | // Build and use 62 | provider, _ := services.Build() 63 | defer provider.Close() 64 | 65 | // godi automatically wires Logger into UserService 66 | users := godi.MustResolve[*UserService](provider) 67 | users.logger.Log("Hello, godi!") 68 | } 69 | ``` 70 | 71 | Ready? Let's start with [installation](01-installation.md). 72 | -------------------------------------------------------------------------------- /lifetime.go: -------------------------------------------------------------------------------- 1 | package godi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Lifetime specifies the lifetime of a service in a Collection. 9 | // The lifetime determines when instances are created and how they are cached. 10 | type Lifetime int 11 | 12 | const ( 13 | // Singleton specifies that a single instance of the service will be created. 14 | // The instance is created on first request and cached for the lifetime of the root provider. 15 | // Singleton services must not depend on Scoped services. 16 | Singleton Lifetime = iota 17 | 18 | // Scoped specifies that a new instance of the service will be created for each scope. 19 | // In web applications, this typically means one instance per HTTP request. 20 | // Scoped services are disposed when their scope is disposed. 21 | Scoped 22 | 23 | // Transient specifies that a new instance of the service will be created every time it is requested. 24 | // Transient services are never cached and always create new instances. 25 | Transient 26 | ) 27 | 28 | // String returns the string representation of the ServiceLifetime. 29 | func (sl Lifetime) String() string { 30 | switch sl { 31 | case Singleton: 32 | return "Singleton" 33 | case Scoped: 34 | return "Scoped" 35 | case Transient: 36 | return "Transient" 37 | default: 38 | return fmt.Sprintf("Unknown(%d)", int(sl)) 39 | } 40 | } 41 | 42 | // IsValid checks if the service lifetime is valid. 43 | // Returns true if the lifetime is Singleton, Scoped, or Transient. 44 | func (sl Lifetime) IsValid() bool { 45 | return sl >= Singleton && sl <= Transient 46 | } 47 | 48 | // MarshalText implements encoding.TextMarshaler interface. 49 | // Converts the lifetime to its string representation for text-based serialization. 50 | func (sl Lifetime) MarshalText() ([]byte, error) { 51 | return []byte(sl.String()), nil 52 | } 53 | 54 | // UnmarshalText implements encoding.TextUnmarshaler interface. 55 | // Parses a string representation back into a Lifetime value. 56 | func (sl *Lifetime) UnmarshalText(text []byte) error { 57 | switch string(text) { 58 | case "Singleton", "singleton": 59 | *sl = Singleton 60 | case "Scoped", "scoped": 61 | *sl = Scoped 62 | case "Transient", "transient": 63 | *sl = Transient 64 | default: 65 | return &LifetimeError{Value: string(text)} 66 | } 67 | return nil 68 | } 69 | 70 | // MarshalJSON implements json.Marshaler interface. 71 | // Serializes the lifetime as a JSON string. 72 | func (sl Lifetime) MarshalJSON() ([]byte, error) { 73 | return json.Marshal(sl.String()) 74 | } 75 | 76 | // UnmarshalJSON implements json.Unmarshaler interface. 77 | // Deserializes a JSON string back into a Lifetime value. 78 | func (sl *Lifetime) UnmarshalJSON(data []byte) error { 79 | var s string 80 | if err := json.Unmarshal(data, &s); err != nil { 81 | return err 82 | } 83 | 84 | return sl.UnmarshalText([]byte(s)) 85 | } 86 | -------------------------------------------------------------------------------- /echo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 4 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 5 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 6 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 7 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 8 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 9 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 10 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 11 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 15 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 16 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 17 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 18 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 19 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 20 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 21 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 22 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 23 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 24 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 27 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 28 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 29 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 33 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /fiber/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 2 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= 6 | github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 7 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 8 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 10 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 15 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 17 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 21 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 22 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 23 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 24 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 25 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 26 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 27 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 28 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 29 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 30 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 33 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 37 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | -------------------------------------------------------------------------------- /docs/getting-started/02-first-container.md: -------------------------------------------------------------------------------- 1 | # Your First Container 2 | 3 | A container holds your application's services. You register services with a **collection**, then **build** it into a **provider** that creates instances on demand. 4 | 5 | ## The Pattern 6 | 7 | ``` 8 | Collection (registration) → Build → Provider (resolution) 9 | ``` 10 | 11 | ## Step 1: Create a Collection 12 | 13 | ```go 14 | services := godi.NewCollection() 15 | ``` 16 | 17 | The collection is where you tell godi about your services. 18 | 19 | ## Step 2: Register a Service 20 | 21 | ```go 22 | services.AddSingleton(func() string { 23 | return "Hello, godi!" 24 | }) 25 | ``` 26 | 27 | This registers a `string` service. The function is called when the service is first needed. 28 | 29 | ## Step 3: Build the Provider 30 | 31 | ```go 32 | provider, err := services.Build() 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | defer provider.Close() 37 | ``` 38 | 39 | Building validates your registrations and prepares the dependency graph. Always close the provider when done - it cleans up resources. 40 | 41 | ## Step 4: Resolve the Service 42 | 43 | ```go 44 | message := godi.MustResolve[string](provider) 45 | fmt.Println(message) // Hello, godi! 46 | ``` 47 | 48 | `MustResolve` returns the service or panics. Use `Resolve` if you want to handle errors yourself. 49 | 50 | ## Complete Example 51 | 52 | ```go 53 | package main 54 | 55 | import ( 56 | "fmt" 57 | "log" 58 | "github.com/junioryono/godi/v4" 59 | ) 60 | 61 | func main() { 62 | // 1. Create collection 63 | services := godi.NewCollection() 64 | 65 | // 2. Register service 66 | services.AddSingleton(func() string { 67 | return "Hello, godi!" 68 | }) 69 | 70 | // 3. Build provider 71 | provider, err := services.Build() 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | defer provider.Close() 76 | 77 | // 4. Use service 78 | message := godi.MustResolve[string](provider) 79 | fmt.Println(message) 80 | } 81 | ``` 82 | 83 | ## What Just Happened? 84 | 85 | ``` 86 | ┌─────────────────────────────────────────────────────┐ 87 | │ Collection │ 88 | │ ┌─────────────────────────────────────────────┐ │ 89 | │ │ string → func() string { return "Hello" } │ │ 90 | │ └─────────────────────────────────────────────┘ │ 91 | └────────────────────────┬────────────────────────────┘ 92 | │ Build() 93 | ▼ 94 | ┌─────────────────────────────────────────────────────┐ 95 | │ Provider │ 96 | │ ┌─────────────────────────────────────────────┐ │ 97 | │ │ MustResolve[string]() → "Hello, godi!" │ │ 98 | │ └─────────────────────────────────────────────┘ │ 99 | └─────────────────────────────────────────────────────┘ 100 | ``` 101 | 102 | 1. You registered a `string` service with a factory function 103 | 2. Building created the provider with the dependency graph 104 | 3. Resolving called your factory and returned the result 105 | 106 | ## Key Points 107 | 108 | - **Collection** is for registration (before the app runs) 109 | - **Provider** is for resolution (while the app runs) 110 | - **Build** validates everything upfront - no runtime surprises 111 | - **Close** cleans up resources when you're done 112 | 113 | --- 114 | 115 | **Next:** [Add real services with dependencies](03-adding-services.md) 116 | -------------------------------------------------------------------------------- /inout.go: -------------------------------------------------------------------------------- 1 | package godi 2 | 3 | import ( 4 | "github.com/junioryono/godi/v4/internal/reflection" 5 | ) 6 | 7 | // In embeds godi.In to leverage godi's parameter object functionality. 8 | // When a constructor function accepts a single struct parameter with embedded In, 9 | // godi will automatically populate all exported fields of that struct 10 | // with the corresponding services. 11 | // 12 | // This is a direct wrapper around godi.In, so all godi features are supported: 13 | // - `optional:"true"` - Field is optional and won't cause an error if the service is not found 14 | // - `name:"serviceName"` - Field should be resolved as a keyed/named service 15 | // - `group:"groupName"` - Field should be filled from a value group (slice fields only) 16 | // 17 | // Example: 18 | // 19 | // type ServiceParams struct { 20 | // godi.In 21 | // 22 | // Database *sql.DB 23 | // Logger Logger `optional:"true"` 24 | // Cache Cache `name:"redis"` 25 | // Handlers []http.Handler `group:"routes"` 26 | // } 27 | // 28 | // func NewService(params ServiceParams) *Service { 29 | // return &Service{ 30 | // db: params.Database, 31 | // logger: params.Logger, // might be nil if not registered 32 | // cache: params.Cache, 33 | // handlers: params.Handlers, 34 | // } 35 | // } 36 | // 37 | // The In struct must be embedded anonymously: 38 | // 39 | // type ServiceParams struct { 40 | // godi.In // ✓ Correct - anonymous embedding 41 | // // ... 42 | // } 43 | // 44 | // type ServiceParams struct { 45 | // In godi.In // ✗ Wrong - named field 46 | // // ... 47 | // } 48 | type In = reflection.In 49 | 50 | // Out embeds godi.Out to leverage godi's result object functionality. 51 | // When a constructor returns a struct with embedded Out, each exported field 52 | // of that struct is registered as a separate service in the container. 53 | // 54 | // This is a direct wrapper around godi.Out, so all godi features are supported: 55 | // - `name:"serviceName"` - Field should be registered as a keyed/named service 56 | // - `group:"groupName"` - Field should be added to a value group 57 | // 58 | // Example: 59 | // 60 | // type ServiceResult struct { 61 | // godi.Out 62 | // 63 | // UserService *UserService 64 | // AdminService *AdminService `name:"admin"` 65 | // Handler http.Handler `group:"routes"` 66 | // } 67 | // 68 | // func NewServices(db *sql.DB) ServiceResult { 69 | // userSvc := newUserService(db) 70 | // adminSvc := newAdminService(db) 71 | // 72 | // return ServiceResult{ 73 | // UserService: userSvc, 74 | // AdminService: adminSvc, 75 | // Handler: newAPIHandler(userSvc), 76 | // } 77 | // } 78 | // 79 | // Multiple handlers example with groups: 80 | // 81 | // type Handlers struct { 82 | // godi.Out 83 | // 84 | // UserHandler http.Handler `group:"routes"` 85 | // AdminHandler http.Handler `group:"routes"` 86 | // APIHandler http.Handler `group:"routes"` 87 | // } 88 | // 89 | // The Out struct must be embedded anonymously: 90 | // 91 | // type ServiceResult struct { 92 | // godi.Out // ✓ Correct - anonymous embedding 93 | // // ... 94 | // } 95 | // 96 | // type ServiceResult struct { 97 | // Out godi.Out // ✗ Wrong - named field 98 | // // ... 99 | // } 100 | // 101 | // Result objects are automatically handled by the regular Add* methods: 102 | // 103 | // collection.AddSingleton(NewServices) // Each field in ServiceResult is registered 104 | type Out = reflection.Out 105 | -------------------------------------------------------------------------------- /docs/getting-started/06-next-steps.md: -------------------------------------------------------------------------------- 1 | # Next Steps 2 | 3 | You now understand the fundamentals of godi. Here's where to go next based on what you're building. 4 | 5 | ## Building a Web Application? 6 | 7 | 1. **[Web Applications Guide](../guides/web-applications.md)** - Complete patterns for production web apps 8 | 2. **[Framework Integration](../integrations/)** - Dedicated guides for Gin, Chi, Echo, Fiber, and net/http 9 | 3. **[Scopes & Isolation](../concepts/scopes.md)** - Deep dive into request isolation 10 | 11 | ## Organizing a Large Application? 12 | 13 | 1. **[Modules](../concepts/modules.md)** - Group related services for better organization 14 | 2. **[Keyed Services](../features/keyed-services.md)** - Multiple implementations of the same interface 15 | 3. **[Service Groups](../features/service-groups.md)** - Collect services for batch operations 16 | 17 | ## Simplifying Complex Constructors? 18 | 19 | 1. **[Parameter Objects](../features/parameter-objects.md)** - Automatic field injection with `In` types 20 | 2. **[Result Objects](../features/result-objects.md)** - Register multiple services from one constructor 21 | 22 | ## Testing Your Application? 23 | 24 | 1. **[Testing Guide](../guides/testing.md)** - Strategies for testing with DI 25 | 2. **[Interface Binding](../features/interface-binding.md)** - Mock implementations for testing 26 | 27 | ## Something Went Wrong? 28 | 29 | 1. **[Error Handling Guide](../guides/error-handling.md)** - Debug common issues 30 | 2. **[Lifetimes Deep Dive](../concepts/lifetimes.md)** - Understand lifetime rules 31 | 32 | ## Quick Reference 33 | 34 | ### Core Concepts 35 | 36 | | Topic | Description | 37 | | ------------------------------------------- | -------------------------------------- | 38 | | [How It Works](../concepts/how-it-works.md) | Visual guide to dependency resolution | 39 | | [Lifetimes](../concepts/lifetimes.md) | Singleton, Scoped, Transient explained | 40 | | [Scopes](../concepts/scopes.md) | Request isolation and context | 41 | | [Modules](../concepts/modules.md) | Organizing large applications | 42 | 43 | ### Features 44 | 45 | | Feature | Use Case | 46 | | ----------------------------------------------------- | ------------------------------------- | 47 | | [Keyed Services](../features/keyed-services.md) | Multiple implementations of same type | 48 | | [Service Groups](../features/service-groups.md) | Collect related services | 49 | | [Parameter Objects](../features/parameter-objects.md) | Simplify complex constructors | 50 | | [Result Objects](../features/result-objects.md) | Multi-service registration | 51 | | [Interface Binding](../features/interface-binding.md) | Register concrete as interface | 52 | | [Resource Cleanup](../features/resource-cleanup.md) | Automatic disposal | 53 | 54 | ### Integrations 55 | 56 | | Framework | Guide | 57 | | --------- | --------------------------------------------------- | 58 | | Gin | [Gin Integration](../integrations/gin.md) | 59 | | Chi | [Chi Integration](../integrations/chi.md) | 60 | | Echo | [Echo Integration](../integrations/echo.md) | 61 | | Fiber | [Fiber Integration](../integrations/fiber.md) | 62 | | net/http | [net/http Integration](../integrations/net-http.md) | 63 | 64 | ## Get Help 65 | 66 | - **[GitHub Issues](https://github.com/junioryono/godi/issues)** - Report bugs or request features 67 | - **[API Reference](https://pkg.go.dev/github.com/junioryono/godi/v4)** - Complete API documentation 68 | 69 | --- 70 | 71 | Happy building with godi! 72 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Path setup -------------------------------------------------------------- 7 | 8 | import subprocess 9 | from datetime import datetime 10 | 11 | # -- Project information ----------------------------------------------------- 12 | 13 | project = 'godi' 14 | author = 'junioryono' 15 | copyright = f'{datetime.now().year}, {author}' 16 | 17 | # Read version from git tags 18 | def get_version(): 19 | try: 20 | result = subprocess.run( 21 | ['git', 'describe', '--tags', '--abbrev=0'], 22 | capture_output=True, 23 | text=True, 24 | cwd='..' 25 | ) 26 | if result.returncode == 0: 27 | return result.stdout.strip() 28 | except Exception: 29 | pass 30 | return 'v0.0.0' 31 | 32 | # The full version, including alpha/beta/rc tags 33 | release = get_version() 34 | version = release 35 | 36 | # -- General configuration --------------------------------------------------- 37 | 38 | # Add any Sphinx extension module names here, as strings. 39 | extensions = [ 40 | 'myst_parser', 41 | 'sphinx_rtd_theme', 42 | 'sphinx_favicon', 43 | 'sphinxext.rediraffe', 44 | 'sphinx_copybutton', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | exclude_patterns = ['_build', '_venv', 'Thumbs.db', '.DS_Store'] 53 | 54 | # -- Options for HTML output ------------------------------------------------- 55 | 56 | # The theme to use for HTML and HTML Help pages. 57 | html_theme = 'sphinx_rtd_theme' 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. 61 | html_static_path = ['_static'] 62 | 63 | html_logo = "_static/logo.png" 64 | html_theme_options = { 65 | 'logo_only': True, 66 | 'prev_next_buttons_location': 'both', 67 | 'style_external_links': False, 68 | 'style_nav_header_background': '#2980B9', 69 | # Toc options 70 | 'collapse_navigation': False, 71 | 'sticky_navigation': True, 72 | 'navigation_depth': 4, 73 | 'includehidden': True, 74 | 'titles_only': False 75 | } 76 | 77 | html_context = { 78 | 'display_github': True, 79 | 'github_user': 'junioryono', 80 | 'github_repo': 'godi', 81 | 'github_version': 'main', 82 | 'conf_py_path': '/docs/', 83 | } 84 | 85 | def setup(app): 86 | app.add_css_file('customize.css') 87 | 88 | # Favicons 89 | favicons = [ 90 | "favicon.png", 91 | ] 92 | 93 | # MyST parser configuration 94 | myst_enable_extensions = [ 95 | "attrs_inline", 96 | "colon_fence", 97 | "deflist", 98 | "tasklist", 99 | ] 100 | 101 | # Copy button configuration 102 | copybutton_prompt_text = r"$ |>>> |\.\.\. " 103 | copybutton_prompt_is_regexp = True 104 | 105 | # Rediraffe configuration for redirects 106 | rediraffe_redirects = { 107 | "installation": "getting-started/01-installation", 108 | "core-concepts": "concepts/how-it-works", 109 | "service-lifetimes": "concepts/lifetimes", 110 | "scopes-isolation": "concepts/scopes", 111 | "modules": "concepts/modules", 112 | "keyed-services": "features/keyed-services", 113 | "service-groups": "features/service-groups", 114 | "parameter-objects": "features/parameter-objects", 115 | "result-objects": "features/result-objects", 116 | "interface-registration": "features/interface-binding", 117 | "resource-management": "features/resource-cleanup", 118 | "dependency-resolution": "concepts/how-it-works", 119 | "service-registration": "getting-started/03-adding-services", 120 | } -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: Getting Started 4 | :hidden: 5 | 6 | getting-started/index 7 | getting-started/01-installation 8 | getting-started/02-first-container 9 | getting-started/03-adding-services 10 | getting-started/04-using-lifetimes 11 | getting-started/05-http-integration 12 | getting-started/06-next-steps 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :caption: Concepts 17 | :hidden: 18 | 19 | concepts/how-it-works 20 | concepts/lifetimes 21 | concepts/scopes 22 | concepts/modules 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | :caption: Guides 27 | :hidden: 28 | 29 | guides/web-applications 30 | guides/testing 31 | guides/error-handling 32 | guides/migration 33 | 34 | .. toctree:: 35 | :maxdepth: 2 36 | :caption: Features 37 | :hidden: 38 | 39 | features/keyed-services 40 | features/service-groups 41 | features/parameter-objects 42 | features/result-objects 43 | features/interface-binding 44 | features/resource-cleanup 45 | 46 | .. toctree:: 47 | :maxdepth: 2 48 | :caption: Integrations 49 | :hidden: 50 | 51 | integrations/gin 52 | integrations/chi 53 | integrations/echo 54 | integrations/fiber 55 | integrations/net-http 56 | 57 | .. toctree:: 58 | :maxdepth: 2 59 | :caption: Reference 60 | :hidden: 61 | 62 | GitHub 63 | API Docs 64 | Changelog 65 | 66 | godi 67 | ==== 68 | 69 | **Dependency injection that gets out of your way.** 70 | 71 | godi automatically wires up your Go applications. Define your services, specify their lifetimes, and let godi handle the rest. 72 | 73 | .. code-block:: go 74 | 75 | services := godi.NewCollection() 76 | services.AddSingleton(NewLogger) 77 | services.AddSingleton(NewDatabase) 78 | services.AddScoped(NewUserService) 79 | 80 | provider, _ := services.Build() 81 | defer provider.Close() 82 | 83 | userService := godi.MustResolve[UserService](provider) 84 | 85 | Why godi? 86 | --------- 87 | 88 | .. list-table:: 89 | :widths: 25 75 90 | :header-rows: 1 91 | 92 | * - Feature 93 | - Benefit 94 | * - **Automatic wiring** 95 | - No manual constructor calls 96 | * - **Three lifetimes** 97 | - Singleton, Scoped, Transient 98 | * - **Compile-time safety** 99 | - Generic type resolution 100 | * - **Zero codegen** 101 | - Pure runtime, no build steps 102 | 103 | Get Started in 5 Minutes 104 | ------------------------ 105 | 106 | Install godi: 107 | 108 | .. code-block:: bash 109 | 110 | go get github.com/junioryono/godi/v4 111 | 112 | Create your first container: 113 | 114 | .. code-block:: go 115 | 116 | package main 117 | 118 | import ( 119 | "fmt" 120 | "github.com/junioryono/godi/v4" 121 | ) 122 | 123 | type Logger struct{} 124 | func (l *Logger) Log(msg string) { fmt.Println(msg) } 125 | 126 | type UserService struct { 127 | logger *Logger 128 | } 129 | 130 | func NewUserService(logger *Logger) *UserService { 131 | return &UserService{logger: logger} 132 | } 133 | 134 | func main() { 135 | services := godi.NewCollection() 136 | services.AddSingleton(func() *Logger { return &Logger{} }) 137 | services.AddSingleton(NewUserService) 138 | 139 | provider, _ := services.Build() 140 | defer provider.Close() 141 | 142 | users := godi.MustResolve[*UserService](provider) 143 | users.logger.Log("Hello, godi!") 144 | } 145 | 146 | **Ready for more?** Start the :doc:`getting-started/index`. 147 | 148 | Quick Links 149 | ----------- 150 | 151 | **Learning godi** 152 | 153 | - :doc:`getting-started/index` - Build your first app in 5 minutes 154 | - :doc:`concepts/lifetimes` - Singleton, Scoped, and Transient explained 155 | - :doc:`guides/web-applications` - Complete web app patterns 156 | 157 | **Framework Integrations** 158 | 159 | - :doc:`integrations/gin` - Gin web framework 160 | - :doc:`integrations/chi` - Chi router 161 | - :doc:`integrations/echo` - Echo framework 162 | - :doc:`integrations/fiber` - Fiber framework 163 | - :doc:`integrations/net-http` - Standard library 164 | 165 | **Advanced Features** 166 | 167 | - :doc:`features/keyed-services` - Multiple implementations 168 | - :doc:`features/parameter-objects` - Simplify constructors 169 | - :doc:`concepts/modules` - Organize large apps 170 | 171 | License 172 | ------- 173 | 174 | MIT License - see `LICENSE `_ 175 | -------------------------------------------------------------------------------- /docs/getting-started/03-adding-services.md: -------------------------------------------------------------------------------- 1 | # Adding Services 2 | 3 | Real applications have services that depend on each other. godi automatically wires these dependencies together. 4 | 5 | ## The Magic: Automatic Wiring 6 | 7 | Write your constructors normally. godi figures out what to pass in. 8 | 9 | ```go 10 | // Logger has no dependencies 11 | type Logger struct{} 12 | 13 | func NewLogger() *Logger { 14 | return &Logger{} 15 | } 16 | 17 | // UserService depends on Logger 18 | type UserService struct { 19 | logger *Logger 20 | } 21 | 22 | func NewUserService(logger *Logger) *UserService { 23 | return &UserService{logger: logger} 24 | } 25 | ``` 26 | 27 | Register both: 28 | 29 | ```go 30 | services := godi.NewCollection() 31 | services.AddSingleton(NewLogger) 32 | services.AddSingleton(NewUserService) 33 | ``` 34 | 35 | Resolve: 36 | 37 | ```go 38 | users := godi.MustResolve[*UserService](provider) 39 | // users.logger is already set! 40 | ``` 41 | 42 | godi saw that `NewUserService` needs a `*Logger`, found `NewLogger`, and called it first. 43 | 44 | ## How It Works 45 | 46 | ``` 47 | ┌────────────────────────────────────────────────────────┐ 48 | │ You register: │ 49 | │ NewLogger() → *Logger │ 50 | │ NewUserService() → *UserService (needs *Logger) │ 51 | ├────────────────────────────────────────────────────────┤ 52 | │ godi builds dependency graph: │ 53 | │ │ 54 | │ *UserService │ 55 | │ │ │ 56 | │ └──depends on──▶ *Logger │ 57 | ├────────────────────────────────────────────────────────┤ 58 | │ When you resolve *UserService: │ 59 | │ 1. Create *Logger (no deps) │ 60 | │ 2. Create *UserService (pass *Logger) │ 61 | │ 3. Return *UserService │ 62 | └────────────────────────────────────────────────────────┘ 63 | ``` 64 | 65 | ## A Realistic Example 66 | 67 | ```go 68 | package main 69 | 70 | import ( 71 | "fmt" 72 | "log" 73 | "github.com/junioryono/godi/v4" 74 | ) 75 | 76 | // Logger - no dependencies 77 | type Logger struct { 78 | prefix string 79 | } 80 | 81 | func NewLogger() *Logger { 82 | return &Logger{prefix: "[APP]"} 83 | } 84 | 85 | func (l *Logger) Log(msg string) { 86 | fmt.Printf("%s %s\n", l.prefix, msg) 87 | } 88 | 89 | // Config - no dependencies 90 | type Config struct { 91 | DatabaseURL string 92 | Debug bool 93 | } 94 | 95 | func NewConfig() *Config { 96 | return &Config{ 97 | DatabaseURL: "postgres://localhost/myapp", 98 | Debug: true, 99 | } 100 | } 101 | 102 | // Database - depends on Config and Logger 103 | type Database struct { 104 | config *Config 105 | logger *Logger 106 | } 107 | 108 | func NewDatabase(config *Config, logger *Logger) *Database { 109 | logger.Log("Connecting to database...") 110 | return &Database{config: config, logger: logger} 111 | } 112 | 113 | func (d *Database) Query(sql string) { 114 | d.logger.Log("Executing: " + sql) 115 | } 116 | 117 | // UserService - depends on Database and Logger 118 | type UserService struct { 119 | db *Database 120 | logger *Logger 121 | } 122 | 123 | func NewUserService(db *Database, logger *Logger) *UserService { 124 | return &UserService{db: db, logger: logger} 125 | } 126 | 127 | func (u *UserService) GetUser(id int) { 128 | u.logger.Log(fmt.Sprintf("Getting user %d", id)) 129 | u.db.Query(fmt.Sprintf("SELECT * FROM users WHERE id = %d", id)) 130 | } 131 | 132 | func main() { 133 | services := godi.NewCollection() 134 | 135 | // Register in any order - godi figures out the dependency order 136 | services.AddSingleton(NewUserService) 137 | services.AddSingleton(NewDatabase) 138 | services.AddSingleton(NewLogger) 139 | services.AddSingleton(NewConfig) 140 | 141 | provider, err := services.Build() 142 | if err != nil { 143 | log.Fatal(err) 144 | } 145 | defer provider.Close() 146 | 147 | // Everything is wired up automatically 148 | users := godi.MustResolve[*UserService](provider) 149 | users.GetUser(42) 150 | } 151 | ``` 152 | 153 | Output: 154 | 155 | ``` 156 | [APP] Connecting to database... 157 | [APP] Getting user 42 158 | [APP] Executing: SELECT * FROM users WHERE id = 42 159 | ``` 160 | 161 | ## Constructor Patterns 162 | 163 | godi supports several constructor patterns: 164 | 165 | ```go 166 | // Simple constructor 167 | func NewLogger() *Logger 168 | 169 | // With dependencies 170 | func NewUserService(logger *Logger, db *Database) *UserService 171 | 172 | // With error return 173 | func NewDatabase(config *Config) (*Database, error) 174 | 175 | // Anonymous function 176 | services.AddSingleton(func(logger *Logger) *Cache { 177 | return &Cache{logger: logger} 178 | }) 179 | ``` 180 | 181 | ## Interface Registration 182 | 183 | Register a concrete type to satisfy an interface: 184 | 185 | ```go 186 | type Logger interface { 187 | Log(string) 188 | } 189 | 190 | type consoleLogger struct{} 191 | func (c *consoleLogger) Log(msg string) { fmt.Println(msg) } 192 | 193 | // Register concrete type as interface 194 | services.AddSingleton(func() *consoleLogger { 195 | return &consoleLogger{} 196 | }, godi.As[Logger]()) 197 | 198 | // Resolve by interface 199 | logger := godi.MustResolve[Logger](provider) 200 | ``` 201 | 202 | ## Key Points 203 | 204 | - Write normal Go constructors - godi handles the wiring 205 | - Registration order doesn't matter 206 | - Dependencies are resolved recursively 207 | - Errors during construction are returned from `Build()` or `Resolve()` 208 | 209 | --- 210 | 211 | **Next:** [Control instance creation with lifetimes](04-using-lifetimes.md) 212 | -------------------------------------------------------------------------------- /docs/features/parameter-objects.md: -------------------------------------------------------------------------------- 1 | # Parameter Objects 2 | 3 | Simplify complex constructors with automatic field injection. 4 | 5 | ## The Problem 6 | 7 | Constructors with many dependencies get unwieldy: 8 | 9 | ```go 10 | func NewOrderService( 11 | db Database, 12 | cache Cache, 13 | logger Logger, 14 | emailer EmailService, 15 | payment PaymentGateway, 16 | inventory InventoryService, 17 | shipping ShippingService, 18 | ) *OrderService { 19 | // ... 20 | } 21 | ``` 22 | 23 | ## The Solution: Parameter Objects 24 | 25 | Group dependencies into a struct with `godi.In`: 26 | 27 | ```go 28 | type OrderServiceParams struct { 29 | godi.In 30 | 31 | DB Database 32 | Cache Cache 33 | Logger Logger 34 | Emailer EmailService 35 | Payment PaymentGateway 36 | Inventory InventoryService 37 | Shipping ShippingService 38 | } 39 | 40 | func NewOrderService(params OrderServiceParams) *OrderService { 41 | return &OrderService{ 42 | db: params.DB, 43 | cache: params.Cache, 44 | logger: params.Logger, 45 | // ... 46 | } 47 | } 48 | ``` 49 | 50 | ## Basic Usage 51 | 52 | ```go 53 | // 1. Define struct with embedded godi.In 54 | type ServiceParams struct { 55 | godi.In // Must be embedded anonymously 56 | 57 | Database Database 58 | Logger Logger 59 | Config *Config 60 | } 61 | 62 | // 2. Use in constructor 63 | func NewService(params ServiceParams) *Service { 64 | return &Service{ 65 | db: params.Database, 66 | logger: params.Logger, 67 | config: params.Config, 68 | } 69 | } 70 | 71 | // 3. Register normally 72 | services.AddSingleton(NewService) 73 | ``` 74 | 75 | godi automatically creates the parameter object and fills in the fields. 76 | 77 | ## Field Tags 78 | 79 | ### Optional Dependencies 80 | 81 | ```go 82 | type ServiceParams struct { 83 | godi.In 84 | 85 | // Required (default) 86 | Database Database 87 | Logger Logger 88 | 89 | // Optional - nil if not registered 90 | Cache Cache `optional:"true"` 91 | Metrics Metrics `optional:"true"` 92 | } 93 | 94 | func NewService(params ServiceParams) *Service { 95 | svc := &Service{ 96 | db: params.Database, 97 | logger: params.Logger, 98 | } 99 | 100 | if params.Cache != nil { 101 | svc.cache = params.Cache 102 | } 103 | 104 | return svc 105 | } 106 | ``` 107 | 108 | ### Named Dependencies 109 | 110 | ```go 111 | type ServiceParams struct { 112 | godi.In 113 | 114 | PrimaryDB Database `name:"primary"` 115 | ReplicaDB Database `name:"replica"` 116 | RedisCache Cache `name:"redis"` 117 | } 118 | ``` 119 | 120 | ### Group Dependencies 121 | 122 | ```go 123 | type ServiceParams struct { 124 | godi.In 125 | 126 | Validators []Validator `group:"validators"` 127 | Middlewares []Middleware `group:"middleware"` 128 | } 129 | ``` 130 | 131 | ### Combining Tags 132 | 133 | ```go 134 | type ServiceParams struct { 135 | godi.In 136 | 137 | // Required named dependency 138 | PrimaryDB Database `name:"primary"` 139 | 140 | // Optional named dependency 141 | CacheDB Database `name:"cache" optional:"true"` 142 | 143 | // Optional group 144 | Plugins []Plugin `group:"plugins" optional:"true"` 145 | } 146 | ``` 147 | 148 | ## Benefits 149 | 150 | ### 1. Cleaner Signatures 151 | 152 | ```go 153 | // Before: 10 parameters 154 | func NewService(a, b, c, d, e, f, g, h, i, j SomeType) *Service 155 | 156 | // After: 1 parameter object 157 | func NewService(params ServiceParams) *Service 158 | ``` 159 | 160 | ### 2. Easier Refactoring 161 | 162 | ```go 163 | // Adding a dependency: just add a field 164 | type ServiceParams struct { 165 | godi.In 166 | 167 | Database Database 168 | Logger Logger 169 | Cache Cache 170 | Metrics Metrics // New - no signature change! 171 | } 172 | ``` 173 | 174 | ### 3. Self-Documenting 175 | 176 | ```go 177 | type EmailServiceParams struct { 178 | godi.In 179 | 180 | // Core dependencies 181 | SMTPClient SMTPClient 182 | TemplateEngine TemplateEngine 183 | 184 | // Optional features 185 | RateLimiter RateLimiter `optional:"true"` 186 | Analytics Analytics `optional:"true"` 187 | 188 | // Configuration 189 | Config *EmailConfig 190 | } 191 | ``` 192 | 193 | ## Testing 194 | 195 | ### Direct Construction 196 | 197 | ```go 198 | func TestService(t *testing.T) { 199 | params := ServiceParams{ 200 | Database: &MockDatabase{}, 201 | Logger: &TestLogger{}, 202 | Cache: &MemoryCache{}, 203 | } 204 | 205 | service := NewService(params) 206 | // Test service... 207 | } 208 | ``` 209 | 210 | ### With Provider 211 | 212 | ```go 213 | func TestServiceIntegration(t *testing.T) { 214 | services := godi.NewCollection() 215 | services.AddSingleton(NewMockDatabase) 216 | services.AddSingleton(NewTestLogger) 217 | services.AddScoped(NewService) 218 | 219 | provider, _ := services.Build() 220 | 221 | service := godi.MustResolve[*Service](provider) 222 | // Test... 223 | } 224 | ``` 225 | 226 | ## Common Mistakes 227 | 228 | ### Named Embedding 229 | 230 | ```go 231 | // Wrong: named field 232 | type BadParams struct { 233 | In godi.In // Won't work! 234 | Database Database 235 | } 236 | 237 | // Correct: anonymous 238 | type GoodParams struct { 239 | godi.In // Anonymous embedding 240 | Database Database 241 | } 242 | ``` 243 | 244 | ### Unexported Fields 245 | 246 | ```go 247 | // Wrong: unexported 248 | type BadParams struct { 249 | godi.In 250 | database Database // lowercase - not injected 251 | } 252 | 253 | // Correct: exported 254 | type GoodParams struct { 255 | godi.In 256 | Database Database // Uppercase - injected 257 | } 258 | ``` 259 | 260 | --- 261 | 262 | **See also:** [Result Objects](result-objects.md) | [Keyed Services](keyed-services.md) 263 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_call: 9 | 10 | env: 11 | GO_VERSION: "1.25" 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | go: ["1.22", "1.23", "1.24", "1.25"] 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v5 25 | 26 | - name: Setup Go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: ${{ matrix.go }} 30 | cache: true 31 | 32 | - name: Get dependencies 33 | run: go mod download 34 | 35 | - name: Run tests 36 | run: go test -v -race -coverprofile coverage.txt -covermode atomic ./... 37 | 38 | - name: Upload coverage to Codecov 39 | if: matrix.os == 'ubuntu-latest' && matrix.go == env.GO_VERSION 40 | uses: codecov/codecov-action@v5 41 | env: 42 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 43 | 44 | lint: 45 | name: Lint 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v5 51 | 52 | - name: Setup Go 53 | uses: actions/setup-go@v5 54 | with: 55 | go-version: ${{ env.GO_VERSION }} 56 | cache: true 57 | 58 | - name: Run golangci-lint 59 | uses: golangci/golangci-lint-action@v8 60 | with: 61 | version: latest 62 | 63 | build: 64 | name: Build 65 | runs-on: ubuntu-latest 66 | 67 | steps: 68 | - name: Checkout code 69 | uses: actions/checkout@v5 70 | 71 | - name: Setup Go 72 | uses: actions/setup-go@v5 73 | with: 74 | go-version: ${{ env.GO_VERSION }} 75 | cache: true 76 | 77 | - name: Verify module 78 | run: | 79 | go mod verify 80 | go mod tidy 81 | git diff --exit-code go.mod go.sum 82 | 83 | - name: Build 84 | run: go build -v ./... 85 | 86 | - name: Check formatting 87 | run: | 88 | if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then 89 | echo "Code is not formatted. Please run 'gofmt -s -w .'" 90 | gofmt -s -d . 91 | exit 1 92 | fi 93 | 94 | benchmark: 95 | name: Benchmark 96 | runs-on: ubuntu-latest 97 | 98 | steps: 99 | - name: Checkout code 100 | uses: actions/checkout@v5 101 | 102 | - name: Setup Go 103 | uses: actions/setup-go@v5 104 | with: 105 | go-version: ${{ env.GO_VERSION }} 106 | cache: true 107 | 108 | - name: Run benchmarks 109 | run: go test -bench=. -benchmem -run=^$ ./... | tee benchmark.txt 110 | 111 | - name: Upload benchmark results 112 | uses: actions/upload-artifact@v4 113 | with: 114 | name: benchmark-results 115 | path: benchmark.txt 116 | 117 | security: 118 | name: Security Scan 119 | runs-on: ubuntu-latest 120 | permissions: 121 | contents: read 122 | security-events: write 123 | 124 | steps: 125 | - name: Checkout code 126 | uses: actions/checkout@v5 127 | 128 | - name: Setup Go 129 | uses: actions/setup-go@v5 130 | with: 131 | go-version: ${{ env.GO_VERSION }} 132 | cache: true 133 | 134 | - name: Run gosec 135 | uses: securego/gosec@master 136 | with: 137 | args: "-exclude-dir=fiber -exclude-dir=echo -exclude-dir=gin -exclude-dir=chi -fmt sarif -out gosec-results.sarif ./..." 138 | 139 | - name: Upload SARIF file 140 | uses: github/codeql-action/upload-sarif@v3 141 | with: 142 | sarif_file: gosec-results.sarif 143 | 144 | coverage: 145 | name: Coverage Report 146 | runs-on: ubuntu-latest 147 | needs: test 148 | 149 | steps: 150 | - name: Checkout code 151 | uses: actions/checkout@v5 152 | 153 | - name: Setup Go 154 | uses: actions/setup-go@v5 155 | with: 156 | go-version: ${{ env.GO_VERSION }} 157 | cache: true 158 | 159 | - name: Generate coverage report 160 | run: | 161 | go test -coverprofile=coverage.out ./... 162 | go tool cover -html=coverage.out -o coverage.html 163 | 164 | - name: Upload coverage report 165 | uses: actions/upload-artifact@v4 166 | with: 167 | name: coverage-report 168 | path: | 169 | coverage.out 170 | coverage.html 171 | 172 | - name: Check coverage threshold 173 | run: | 174 | COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') 175 | echo "Total coverage: $COVERAGE%" 176 | if (( $(echo "$COVERAGE < 80" | bc -l) )); then 177 | echo "Coverage is below 80% threshold" 178 | exit 1 179 | fi 180 | 181 | all-checks: 182 | name: All checks passed 183 | runs-on: ubuntu-latest 184 | needs: [test, lint, build, security] 185 | if: always() 186 | 187 | steps: 188 | - name: Verify all checks passed 189 | run: | 190 | if [[ "${{ needs.test.result }}" != "success" || "${{ needs.lint.result }}" != "success" || "${{ needs.build.result }}" != "success" || "${{ needs.security.result }}" != "success" ]]; then 191 | echo "One or more checks failed" 192 | echo "Test: ${{ needs.test.result }}" 193 | echo "Lint: ${{ needs.lint.result }}" 194 | echo "Build: ${{ needs.build.result }}" 195 | echo "Security: ${{ needs.security.result }}" 196 | exit 1 197 | fi 198 | echo "All checks passed successfully" 199 | -------------------------------------------------------------------------------- /docs/features/keyed-services.md: -------------------------------------------------------------------------------- 1 | # Keyed Services 2 | 3 | Register and resolve multiple implementations of the same type using keys. 4 | 5 | ## The Problem 6 | 7 | You need multiple database connections, cache implementations, or payment gateways - all of the same type: 8 | 9 | ```go 10 | // Can't do this - same type registered twice 11 | services.AddSingleton(NewPrimaryDB) // Database 12 | services.AddSingleton(NewReplicaDB) // Also Database - conflict! 13 | ``` 14 | 15 | ## The Solution: Keys 16 | 17 | ```go 18 | services.AddSingleton(NewPrimaryDB, godi.Name("primary")) 19 | services.AddSingleton(NewReplicaDB, godi.Name("replica")) 20 | 21 | // Resolve by key 22 | primary := godi.MustResolveKeyed[Database](provider, "primary") 23 | replica := godi.MustResolveKeyed[Database](provider, "replica") 24 | ``` 25 | 26 | ## Registration 27 | 28 | Use `godi.Name()` to assign a key: 29 | 30 | ```go 31 | // Database connections 32 | services.AddSingleton(NewPrimaryDB, godi.Name("primary")) 33 | services.AddSingleton(NewReplicaDB, godi.Name("replica")) 34 | services.AddSingleton(NewAnalyticsDB, godi.Name("analytics")) 35 | 36 | // Cache implementations 37 | services.AddSingleton(NewRedisCache, godi.Name("redis")) 38 | services.AddSingleton(NewMemoryCache, godi.Name("memory")) 39 | ``` 40 | 41 | ## Resolution 42 | 43 | ```go 44 | // By key 45 | primary := godi.MustResolveKeyed[Database](provider, "primary") 46 | replica := godi.MustResolveKeyed[Database](provider, "replica") 47 | 48 | // With error handling 49 | analytics, err := godi.ResolveKeyed[Database](provider, "analytics") 50 | if err != nil { 51 | // Key not found or resolution error 52 | } 53 | ``` 54 | 55 | ## Use Cases 56 | 57 | ### Multiple Database Connections 58 | 59 | ```go 60 | type DatabaseManager struct { 61 | primary Database 62 | replica Database 63 | analytics Database 64 | } 65 | 66 | func NewDatabaseManager(provider godi.Provider) *DatabaseManager { 67 | return &DatabaseManager{ 68 | primary: godi.MustResolveKeyed[Database](provider, "primary"), 69 | replica: godi.MustResolveKeyed[Database](provider, "replica"), 70 | analytics: godi.MustResolveKeyed[Database](provider, "analytics"), 71 | } 72 | } 73 | 74 | func (m *DatabaseManager) Read(query string) Result { 75 | return m.replica.Query(query) // Use replica for reads 76 | } 77 | 78 | func (m *DatabaseManager) Write(query string) Result { 79 | return m.primary.Exec(query) // Use primary for writes 80 | } 81 | ``` 82 | 83 | ### Strategy Pattern 84 | 85 | ```go 86 | type PaymentStrategy interface { 87 | Process(amount float64) error 88 | } 89 | 90 | // Register strategies 91 | services.AddSingleton(NewStripeStrategy, godi.Name("stripe"), godi.As[PaymentStrategy]()) 92 | services.AddSingleton(NewPayPalStrategy, godi.Name("paypal"), godi.As[PaymentStrategy]()) 93 | services.AddSingleton(NewSquareStrategy, godi.Name("square"), godi.As[PaymentStrategy]()) 94 | 95 | // Select at runtime 96 | func (s *PaymentService) Process(method string, amount float64) error { 97 | strategy := godi.MustResolveKeyed[PaymentStrategy](s.provider, method) 98 | return strategy.Process(amount) 99 | } 100 | ``` 101 | 102 | ### Environment-Specific Services 103 | 104 | ```go 105 | func RegisterEmailService(services *godi.ServiceCollection, env string) { 106 | switch env { 107 | case "development": 108 | services.AddSingleton(NewMockEmailer, godi.Name("email")) 109 | case "staging": 110 | services.AddSingleton(NewSandboxEmailer, godi.Name("email")) 111 | case "production": 112 | services.AddSingleton(NewSESEmailer, godi.Name("email")) 113 | } 114 | } 115 | 116 | // Same resolution everywhere 117 | emailer := godi.MustResolveKeyed[Emailer](provider, "email") 118 | ``` 119 | 120 | ## With Parameter Objects 121 | 122 | Use the `name` tag to inject keyed services: 123 | 124 | ```go 125 | type ServiceParams struct { 126 | godi.In 127 | 128 | PrimaryDB Database `name:"primary"` 129 | ReplicaDB Database `name:"replica"` 130 | RedisCache Cache `name:"redis"` 131 | MemoryCache Cache `name:"memory"` 132 | } 133 | 134 | func NewService(params ServiceParams) *Service { 135 | return &Service{ 136 | primary: params.PrimaryDB, 137 | replica: params.ReplicaDB, 138 | redis: params.RedisCache, 139 | memory: params.MemoryCache, 140 | } 141 | } 142 | ``` 143 | 144 | ## Best Practices 145 | 146 | ### Use Constants for Keys 147 | 148 | ```go 149 | const ( 150 | PrimaryDB = "primary" 151 | ReplicaDB = "replica" 152 | RedisCache = "redis" 153 | MemoryCache = "memory" 154 | ) 155 | 156 | // Registration 157 | services.AddSingleton(NewPrimaryDB, godi.Name(PrimaryDB)) 158 | 159 | // Resolution 160 | db := godi.MustResolveKeyed[Database](provider, PrimaryDB) 161 | ``` 162 | 163 | ### Fallback Pattern 164 | 165 | ```go 166 | func GetCache(provider godi.Provider) Cache { 167 | // Try primary 168 | cache, err := godi.ResolveKeyed[Cache](provider, "redis") 169 | if err == nil { 170 | return cache 171 | } 172 | 173 | // Fallback 174 | cache, err = godi.ResolveKeyed[Cache](provider, "memory") 175 | if err == nil { 176 | return cache 177 | } 178 | 179 | // Default 180 | return NewDefaultCache() 181 | } 182 | ``` 183 | 184 | ## Common Mistakes 185 | 186 | ### Duplicate Keys 187 | 188 | ```go 189 | // Error: same key twice 190 | services.AddSingleton(NewServiceA, godi.Name("service")) 191 | services.AddSingleton(NewServiceB, godi.Name("service")) // Conflict! 192 | 193 | // Correct: unique keys 194 | services.AddSingleton(NewServiceA, godi.Name("serviceA")) 195 | services.AddSingleton(NewServiceB, godi.Name("serviceB")) 196 | ``` 197 | 198 | ### Wrong Type 199 | 200 | ```go 201 | services.AddSingleton(NewLogger, godi.Name("logger")) 202 | 203 | // Error: wrong type 204 | cache := godi.MustResolveKeyed[Cache](provider, "logger") // Panic! 205 | 206 | // Correct: matching type 207 | logger := godi.MustResolveKeyed[Logger](provider, "logger") 208 | ``` 209 | 210 | --- 211 | 212 | **See also:** [Service Groups](service-groups.md) | [Parameter Objects](parameter-objects.md) 213 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version_bump: 7 | description: "Version bump type" 8 | required: true 9 | type: choice 10 | options: 11 | - Patch 12 | - Minor 13 | - Major 14 | 15 | jobs: 16 | run-tests: 17 | name: Run Tests 18 | uses: ./.github/workflows/test.yml 19 | permissions: 20 | contents: read 21 | security-events: write 22 | 23 | tag: 24 | name: Create Tag 25 | runs-on: ubuntu-latest 26 | needs: run-tests 27 | if: success() 28 | 29 | permissions: 30 | contents: write 31 | 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v5 35 | with: 36 | fetch-depth: 0 37 | token: ${{ secrets.RELEASE_TOKEN }} 38 | 39 | - name: Configure Git 40 | run: | 41 | git config user.name 'github-actions[bot]' 42 | git config user.email 'github-actions[bot]@users.noreply.github.com' 43 | 44 | - name: Get current version 45 | id: current_version 46 | run: | 47 | # Get the highest semantic version tag (not just reachable from HEAD) 48 | LATEST_TAG=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -t. -k1,1Vr -k2,2nr -k3,3nr | head -n1) 49 | LATEST_TAG=${LATEST_TAG:-v0.0.0} 50 | echo "Latest tag: $LATEST_TAG" 51 | 52 | # Extract version numbers 53 | VERSION=${LATEST_TAG#v} 54 | IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" 55 | 56 | # Default to 0 if empty 57 | MAJOR=${MAJOR:-0} 58 | MINOR=${MINOR:-0} 59 | PATCH=${PATCH:-0} 60 | 61 | echo "Current version: $MAJOR.$MINOR.$PATCH" 62 | echo "major=$MAJOR" >> $GITHUB_OUTPUT 63 | echo "minor=$MINOR" >> $GITHUB_OUTPUT 64 | echo "patch=$PATCH" >> $GITHUB_OUTPUT 65 | echo "current_tag=$LATEST_TAG" >> $GITHUB_OUTPUT 66 | 67 | - name: Calculate new version 68 | id: new_version 69 | run: | 70 | MAJOR=${{ steps.current_version.outputs.major }} 71 | MINOR=${{ steps.current_version.outputs.minor }} 72 | PATCH=${{ steps.current_version.outputs.patch }} 73 | 74 | VERSION_BUMP="${{ github.event.inputs.version_bump }}" 75 | 76 | case "$VERSION_BUMP" in 77 | Major) 78 | MAJOR=$((MAJOR + 1)) 79 | MINOR=0 80 | PATCH=0 81 | ;; 82 | Minor) 83 | MINOR=$((MINOR + 1)) 84 | PATCH=0 85 | ;; 86 | Patch) 87 | PATCH=$((PATCH + 1)) 88 | ;; 89 | *) 90 | echo "Invalid version bump type: $VERSION_BUMP" 91 | exit 1 92 | ;; 93 | esac 94 | 95 | NEW_VERSION="v$MAJOR.$MINOR.$PATCH" 96 | echo "New version: $NEW_VERSION" 97 | echo "new_tag=$NEW_VERSION" >> $GITHUB_OUTPUT 98 | echo "version=$MAJOR.$MINOR.$PATCH" >> $GITHUB_OUTPUT 99 | 100 | - name: Create and push tags 101 | run: | 102 | NEW_TAG="${{ steps.new_version.outputs.new_tag }}" 103 | 104 | # Check if main tag already exists 105 | if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then 106 | echo "❌ Error: Tag $NEW_TAG already exists!" 107 | echo "This may indicate a version calculation issue. Please check existing tags." 108 | git tag -l 'v*' | sort -V | tail -5 109 | exit 1 110 | fi 111 | 112 | # Define submodules that need their own tags 113 | SUBMODULES="gin chi echo fiber http" 114 | 115 | # Check if any submodule tag already exists 116 | for submod in $SUBMODULES; do 117 | SUBMOD_TAG="${submod}/${NEW_TAG}" 118 | if git rev-parse "$SUBMOD_TAG" >/dev/null 2>&1; then 119 | echo "❌ Error: Tag $SUBMOD_TAG already exists!" 120 | exit 1 121 | fi 122 | done 123 | 124 | # Create annotated tag for main module 125 | git tag -a "$NEW_TAG" -m "Release $NEW_TAG - ${{ github.event.inputs.version_bump }} version bump" 126 | echo "✅ Created main module tag: $NEW_TAG" 127 | 128 | # Create annotated tags for each submodule 129 | for submod in $SUBMODULES; do 130 | SUBMOD_TAG="${submod}/${NEW_TAG}" 131 | git tag -a "$SUBMOD_TAG" -m "Release $SUBMOD_TAG - ${{ github.event.inputs.version_bump }} version bump" 132 | echo "✅ Created submodule tag: $SUBMOD_TAG" 133 | done 134 | 135 | # Push all tags at once 136 | git push origin "$NEW_TAG" 137 | for submod in $SUBMODULES; do 138 | git push origin "${submod}/${NEW_TAG}" 139 | done 140 | 141 | echo "✅ Successfully pushed all tags" 142 | 143 | - name: Summary 144 | run: | 145 | NEW_TAG="${{ steps.new_version.outputs.new_tag }}" 146 | SUBMODULES="gin chi echo fiber http" 147 | 148 | echo "## Tag Creation Summary" >> $GITHUB_STEP_SUMMARY 149 | echo "" >> $GITHUB_STEP_SUMMARY 150 | echo "- **Version Bump Type:** ${{ github.event.inputs.version_bump }}" >> $GITHUB_STEP_SUMMARY 151 | echo "- **Previous Tag:** ${{ steps.current_version.outputs.current_tag }}" >> $GITHUB_STEP_SUMMARY 152 | echo "" >> $GITHUB_STEP_SUMMARY 153 | echo "### Created Tags" >> $GITHUB_STEP_SUMMARY 154 | echo "" >> $GITHUB_STEP_SUMMARY 155 | echo "| Module | Tag | Go Get Command |" >> $GITHUB_STEP_SUMMARY 156 | echo "|--------|-----|----------------|" >> $GITHUB_STEP_SUMMARY 157 | echo "| Main (godi/v4) | \`$NEW_TAG\` | \`go get github.com/junioryono/godi/v4@$NEW_TAG\` |" >> $GITHUB_STEP_SUMMARY 158 | for submod in $SUBMODULES; do 159 | echo "| $submod | \`${submod}/$NEW_TAG\` | \`go get github.com/junioryono/godi/v4/${submod}@$NEW_TAG\` |" >> $GITHUB_STEP_SUMMARY 160 | done 161 | echo "" >> $GITHUB_STEP_SUMMARY 162 | echo "✅ All tests passed and all tags have been created successfully!" >> $GITHUB_STEP_SUMMARY 163 | -------------------------------------------------------------------------------- /lifetime_test.go: -------------------------------------------------------------------------------- 1 | package godi 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestLifetime(t *testing.T) { 13 | t.Parallel() 14 | 15 | // All valid lifetimes for reuse in tests 16 | validLifetimes := []Lifetime{Singleton, Scoped, Transient} 17 | 18 | t.Run("String", func(t *testing.T) { 19 | t.Parallel() 20 | cases := []struct { 21 | lt Lifetime 22 | want string 23 | }{ 24 | {Singleton, "Singleton"}, 25 | {Scoped, "Scoped"}, 26 | {Transient, "Transient"}, 27 | {Lifetime(-1), "Unknown(-1)"}, 28 | {Lifetime(999), "Unknown(999)"}, 29 | } 30 | for _, tc := range cases { 31 | assert.Equal(t, tc.want, tc.lt.String()) 32 | } 33 | }) 34 | 35 | t.Run("IsValid", func(t *testing.T) { 36 | t.Parallel() 37 | for _, lt := range validLifetimes { 38 | assert.True(t, lt.IsValid(), "%s should be valid", lt) 39 | } 40 | assert.False(t, Lifetime(-1).IsValid()) 41 | assert.False(t, Lifetime(3).IsValid()) 42 | assert.False(t, Lifetime(999).IsValid()) 43 | }) 44 | 45 | t.Run("Constants", func(t *testing.T) { 46 | t.Parallel() 47 | // Verify constant values (important for serialization compatibility) 48 | assert.Equal(t, Lifetime(0), Singleton) 49 | assert.Equal(t, Lifetime(1), Scoped) 50 | assert.Equal(t, Lifetime(2), Transient) 51 | 52 | // Zero value should be Singleton 53 | var zero Lifetime 54 | assert.Equal(t, Singleton, zero) 55 | }) 56 | 57 | t.Run("TextRoundTrip", func(t *testing.T) { 58 | t.Parallel() 59 | for _, lt := range validLifetimes { 60 | data, err := lt.MarshalText() 61 | require.NoError(t, err) 62 | 63 | var got Lifetime 64 | require.NoError(t, got.UnmarshalText(data)) 65 | assert.Equal(t, lt, got) 66 | } 67 | }) 68 | 69 | t.Run("UnmarshalText", func(t *testing.T) { 70 | t.Parallel() 71 | 72 | t.Run("valid_inputs", func(t *testing.T) { 73 | cases := []struct { 74 | input string 75 | want Lifetime 76 | }{ 77 | {"Singleton", Singleton}, 78 | {"singleton", Singleton}, 79 | {"Scoped", Scoped}, 80 | {"scoped", Scoped}, 81 | {"Transient", Transient}, 82 | {"transient", Transient}, 83 | } 84 | for _, tc := range cases { 85 | var got Lifetime 86 | err := got.UnmarshalText([]byte(tc.input)) 87 | require.NoError(t, err, "input: %s", tc.input) 88 | assert.Equal(t, tc.want, got) 89 | } 90 | }) 91 | 92 | t.Run("invalid_inputs", func(t *testing.T) { 93 | inputs := []string{"", "Invalid", "random", " Singleton ", "SiNgLeToN"} 94 | for _, input := range inputs { 95 | var got Lifetime 96 | err := got.UnmarshalText([]byte(input)) 97 | assert.Error(t, err, "input: %q should error", input) 98 | var ltErr *LifetimeError 99 | assert.IsType(t, ltErr, err) 100 | } 101 | }) 102 | }) 103 | 104 | t.Run("JSONRoundTrip", func(t *testing.T) { 105 | t.Parallel() 106 | for _, lt := range validLifetimes { 107 | data, err := json.Marshal(lt) 108 | require.NoError(t, err) 109 | 110 | var got Lifetime 111 | require.NoError(t, json.Unmarshal(data, &got)) 112 | assert.Equal(t, lt, got) 113 | } 114 | }) 115 | 116 | t.Run("UnmarshalJSON", func(t *testing.T) { 117 | t.Parallel() 118 | 119 | t.Run("valid", func(t *testing.T) { 120 | cases := []struct { 121 | input string 122 | want Lifetime 123 | }{ 124 | {`"Singleton"`, Singleton}, 125 | {`"singleton"`, Singleton}, 126 | {`"Scoped"`, Scoped}, 127 | {`"scoped"`, Scoped}, 128 | {`"Transient"`, Transient}, 129 | {`"transient"`, Transient}, 130 | } 131 | for _, tc := range cases { 132 | var got Lifetime 133 | err := json.Unmarshal([]byte(tc.input), &got) 134 | require.NoError(t, err, "input: %s", tc.input) 135 | assert.Equal(t, tc.want, got) 136 | } 137 | }) 138 | 139 | t.Run("invalid", func(t *testing.T) { 140 | inputs := []string{ 141 | `"Invalid"`, `""`, `null`, `0`, 142 | `Singleton`, `["Singleton"]`, `{"lifetime":"Singleton"}`, 143 | } 144 | for _, input := range inputs { 145 | var got Lifetime 146 | err := json.Unmarshal([]byte(input), &got) 147 | assert.Error(t, err, "input: %s should error", input) 148 | } 149 | }) 150 | }) 151 | 152 | t.Run("JSONInStruct", func(t *testing.T) { 153 | t.Parallel() 154 | type Config struct { 155 | Lifetime Lifetime `json:"lifetime"` 156 | Name string `json:"name"` 157 | } 158 | 159 | // Marshal 160 | cfg := Config{Lifetime: Singleton, Name: "test"} 161 | data, err := json.Marshal(cfg) 162 | require.NoError(t, err) 163 | assert.JSONEq(t, `{"lifetime":"Singleton","name":"test"}`, string(data)) 164 | 165 | // Unmarshal 166 | var got Config 167 | require.NoError(t, json.Unmarshal([]byte(`{"lifetime":"Scoped","name":"svc"}`), &got)) 168 | assert.Equal(t, Scoped, got.Lifetime) 169 | assert.Equal(t, "svc", got.Name) 170 | 171 | // Invalid 172 | err = json.Unmarshal([]byte(`{"lifetime":"Invalid"}`), &got) 173 | assert.Error(t, err) 174 | }) 175 | 176 | t.Run("JSONSlice", func(t *testing.T) { 177 | t.Parallel() 178 | lifetimes := []Lifetime{Singleton, Scoped, Transient} 179 | 180 | data, err := json.Marshal(lifetimes) 181 | require.NoError(t, err) 182 | assert.JSONEq(t, `["Singleton","Scoped","Transient"]`, string(data)) 183 | 184 | var got []Lifetime 185 | require.NoError(t, json.Unmarshal(data, &got)) 186 | assert.Equal(t, lifetimes, got) 187 | }) 188 | 189 | t.Run("JSONMap", func(t *testing.T) { 190 | t.Parallel() 191 | m := map[string]Lifetime{"a": Singleton, "b": Scoped} 192 | 193 | data, err := json.Marshal(m) 194 | require.NoError(t, err) 195 | 196 | var got map[string]Lifetime 197 | require.NoError(t, json.Unmarshal(data, &got)) 198 | assert.Equal(t, m, got) 199 | }) 200 | 201 | t.Run("NilPointerUnmarshal", func(t *testing.T) { 202 | t.Parallel() 203 | var ptr *Lifetime 204 | assert.Panics(t, func() { 205 | _ = ptr.UnmarshalText([]byte("Singleton")) 206 | }) 207 | }) 208 | 209 | t.Run("ConcurrentAccess", func(t *testing.T) { 210 | t.Parallel() 211 | lt := Singleton 212 | var wg sync.WaitGroup 213 | for i := 0; i < 100; i++ { 214 | wg.Add(1) 215 | go func() { 216 | defer wg.Done() 217 | _ = lt.String() 218 | _ = lt.IsValid() 219 | _, _ = lt.MarshalText() 220 | _, _ = lt.MarshalJSON() 221 | }() 222 | } 223 | wg.Wait() 224 | }) 225 | } 226 | -------------------------------------------------------------------------------- /docs/getting-started/04-using-lifetimes.md: -------------------------------------------------------------------------------- 1 | # Using Lifetimes 2 | 3 | When should a database connection be shared? When should a request context be unique? Lifetimes answer these questions. 4 | 5 | ## The Three Lifetimes 6 | 7 | ``` 8 | ┌────────────────────────────────────────────────────────┐ 9 | │ Application Lifetime │ 10 | │ ┌───────────────────────────────────────────────────┐ │ 11 | │ │ SINGLETON: Logger, Database, Config │ │ 12 | │ │ Created once, shared everywhere │ │ 13 | │ └───────────────────────────────────────────────────┘ │ 14 | │ │ 15 | │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 16 | │ │ Request 1 │ │ Request 2 │ │ Request 3 │ │ 17 | │ │ │ │ │ │ │ │ 18 | │ │ SCOPED: │ │ SCOPED: │ │ SCOPED: │ │ 19 | │ │ UserSession │ │ UserSession │ │ UserSession │ │ 20 | │ │ Transaction │ │ Transaction │ │ Transaction │ │ 21 | │ │ │ │ │ │ │ │ 22 | │ │ TRANSIENT: │ │ TRANSIENT: │ │ TRANSIENT: │ │ 23 | │ │ new instance │ │ new instance │ │ new instance │ │ 24 | │ │ every time │ │ every time │ │ every time │ │ 25 | │ └──────────────┘ └──────────────┘ └──────────────┘ │ 26 | └────────────────────────────────────────────────────────┘ 27 | ``` 28 | 29 | ## Singleton - One Instance Forever 30 | 31 | Created once when first requested. Shared by everyone. 32 | 33 | ```go 34 | services.AddSingleton(NewDatabasePool) 35 | 36 | // Same instance everywhere 37 | db1 := godi.MustResolve[*DatabasePool](provider) 38 | db2 := godi.MustResolve[*DatabasePool](provider) 39 | // db1 == db2 ✓ 40 | ``` 41 | 42 | **Use for:** Database connections, configuration, loggers, HTTP clients, caches 43 | 44 | ## Scoped - One Instance Per Scope 45 | 46 | Created once per scope. Different scopes get different instances. 47 | 48 | ```go 49 | services.AddScoped(NewRequestContext) 50 | 51 | // Create a scope (typically per HTTP request) 52 | scope1, _ := provider.CreateScope(ctx) 53 | defer scope1.Close() 54 | 55 | // Same within scope 56 | ctx1 := godi.MustResolve[*RequestContext](scope1) 57 | ctx2 := godi.MustResolve[*RequestContext](scope1) 58 | // ctx1 == ctx2 ✓ 59 | 60 | // Different scope = different instance 61 | scope2, _ := provider.CreateScope(ctx) 62 | defer scope2.Close() 63 | ctx3 := godi.MustResolve[*RequestContext](scope2) 64 | // ctx1 == ctx3 ✗ 65 | ``` 66 | 67 | **Use for:** Request context, database transactions, user sessions, per-request caches 68 | 69 | ## Transient - New Instance Every Time 70 | 71 | Created fresh on every resolution. 72 | 73 | ```go 74 | services.AddTransient(NewEmailBuilder) 75 | 76 | // Always new 77 | builder1 := godi.MustResolve[*EmailBuilder](provider) 78 | builder2 := godi.MustResolve[*EmailBuilder](provider) 79 | // builder1 == builder2 ✗ 80 | ``` 81 | 82 | **Use for:** Builders, temporary objects, stateful utilities 83 | 84 | ## Complete Example 85 | 86 | ```go 87 | package main 88 | 89 | import ( 90 | "context" 91 | "fmt" 92 | "log" 93 | "github.com/junioryono/godi/v4" 94 | ) 95 | 96 | // Singleton - shared everywhere 97 | type Logger struct { 98 | id int 99 | } 100 | var loggerCount = 0 101 | func NewLogger() *Logger { 102 | loggerCount++ 103 | return &Logger{id: loggerCount} 104 | } 105 | 106 | // Scoped - one per scope 107 | type RequestID struct { 108 | value int 109 | } 110 | var requestCount = 0 111 | func NewRequestID() *RequestID { 112 | requestCount++ 113 | return &RequestID{value: requestCount} 114 | } 115 | 116 | // Transient - always new 117 | type TempFile struct { 118 | name string 119 | } 120 | var fileCount = 0 121 | func NewTempFile() *TempFile { 122 | fileCount++ 123 | return &TempFile{name: fmt.Sprintf("temp_%d.txt", fileCount)} 124 | } 125 | 126 | func main() { 127 | services := godi.NewCollection() 128 | services.AddSingleton(NewLogger) 129 | services.AddScoped(NewRequestID) 130 | services.AddTransient(NewTempFile) 131 | 132 | provider, err := services.Build() 133 | if err != nil { 134 | log.Fatal(err) 135 | } 136 | defer provider.Close() 137 | 138 | // Simulate two HTTP requests 139 | for i := 1; i <= 2; i++ { 140 | fmt.Printf("\n--- Request %d ---\n", i) 141 | 142 | scope, _ := provider.CreateScope(context.Background()) 143 | 144 | // Singleton: same logger 145 | logger := godi.MustResolve[*Logger](scope) 146 | fmt.Printf("Logger ID: %d\n", logger.id) 147 | 148 | // Scoped: same within request 149 | reqID1 := godi.MustResolve[*RequestID](scope) 150 | reqID2 := godi.MustResolve[*RequestID](scope) 151 | fmt.Printf("RequestID (same scope): %d == %d? %v\n", 152 | reqID1.value, reqID2.value, reqID1 == reqID2) 153 | 154 | // Transient: different every time 155 | file1 := godi.MustResolve[*TempFile](scope) 156 | file2 := godi.MustResolve[*TempFile](scope) 157 | fmt.Printf("TempFile: %s, %s\n", file1.name, file2.name) 158 | 159 | scope.Close() 160 | } 161 | } 162 | ``` 163 | 164 | Output: 165 | 166 | ``` 167 | --- Request 1 --- 168 | Logger ID: 1 169 | RequestID (same scope): 1 == 1? true 170 | TempFile: temp_1.txt, temp_2.txt 171 | 172 | --- Request 2 --- 173 | Logger ID: 1 174 | RequestID (same scope): 2 == 2? true 175 | TempFile: temp_3.txt, temp_4.txt 176 | ``` 177 | 178 | ## The Golden Rule 179 | 180 | **A service can only depend on services with the same or longer lifetime.** 181 | 182 | ```go 183 | // ✓ OK: Scoped can depend on Singleton 184 | services.AddSingleton(NewLogger) 185 | services.AddScoped(func(logger *Logger) *UserService { 186 | return &UserService{logger: logger} 187 | }) 188 | 189 | // ✗ ERROR: Singleton cannot depend on Scoped 190 | services.AddScoped(NewRequestContext) 191 | services.AddSingleton(func(ctx *RequestContext) *Cache { // Build error! 192 | return &Cache{ctx: ctx} 193 | }) 194 | ``` 195 | 196 | Why? A singleton lives forever, but scoped services are destroyed when the scope closes. The singleton would hold a reference to something that no longer exists. 197 | 198 | ## Quick Reference 199 | 200 | | Lifetime | Created | Shared | Destroyed | Use Case | 201 | | --------- | ---------- | ------------ | ---------------- | ----------------------------- | 202 | | Singleton | Once | App-wide | Provider.Close() | DB pools, config | 203 | | Scoped | Per scope | Within scope | Scope.Close() | Request context, transactions | 204 | | Transient | Every time | Never | Scope.Close() | Builders, temp objects | 205 | 206 | --- 207 | 208 | **Next:** [Build a web application](05-http-integration.md) 209 | -------------------------------------------------------------------------------- /docs/features/interface-binding.md: -------------------------------------------------------------------------------- 1 | # Interface Binding 2 | 3 | Register concrete types to satisfy interfaces. 4 | 5 | ## The Problem 6 | 7 | You have a concrete type but want to resolve by interface: 8 | 9 | ```go 10 | type consoleLogger struct{} 11 | func (c *consoleLogger) Log(msg string) { fmt.Println(msg) } 12 | 13 | type Logger interface { 14 | Log(string) 15 | } 16 | 17 | // Register concrete 18 | services.AddSingleton(NewConsoleLogger) // Returns *consoleLogger 19 | 20 | // Want to resolve by interface 21 | logger := godi.MustResolve[Logger](provider) // Error: Logger not registered 22 | ``` 23 | 24 | ## The Solution: As Option 25 | 26 | Use `godi.As[T]()` to register a concrete type as an interface: 27 | 28 | ```go 29 | services.AddSingleton(NewConsoleLogger, godi.As[Logger]()) 30 | 31 | // Now resolvable by interface 32 | logger := godi.MustResolve[Logger](provider) 33 | ``` 34 | 35 | ## Basic Usage 36 | 37 | ```go 38 | // Interface 39 | type Cache interface { 40 | Get(key string) (any, bool) 41 | Set(key string, value any) 42 | } 43 | 44 | // Concrete implementation 45 | type redisCache struct { 46 | client *redis.Client 47 | } 48 | 49 | func NewRedisCache(config *Config) *redisCache { 50 | return &redisCache{ 51 | client: redis.NewClient(&redis.Options{Addr: config.RedisAddr}), 52 | } 53 | } 54 | 55 | // Register as interface 56 | services.AddSingleton(NewRedisCache, godi.As[Cache]()) 57 | 58 | // Resolve by interface 59 | cache := godi.MustResolve[Cache](provider) 60 | ``` 61 | 62 | ## Multiple Interfaces 63 | 64 | A type can implement and be registered as multiple interfaces: 65 | 66 | ```go 67 | type userStore struct { 68 | db *sql.DB 69 | } 70 | 71 | // Implements multiple interfaces 72 | type UserReader interface { GetUser(id int) *User } 73 | type UserWriter interface { SaveUser(user *User) error } 74 | type UserRepository interface { 75 | GetUser(id int) *User 76 | SaveUser(user *User) error 77 | } 78 | 79 | services.AddSingleton(NewUserStore, 80 | godi.As[UserReader](), 81 | godi.As[UserWriter](), 82 | godi.As[UserRepository](), 83 | ) 84 | 85 | // Resolve by any interface 86 | reader := godi.MustResolve[UserReader](provider) 87 | writer := godi.MustResolve[UserWriter](provider) 88 | repo := godi.MustResolve[UserRepository](provider) 89 | // All return the same *userStore instance (singleton) 90 | ``` 91 | 92 | ## With Keys and Groups 93 | 94 | Combine with other options: 95 | 96 | ```go 97 | // Named interface 98 | services.AddSingleton(NewFileLogger, 99 | godi.Name("file"), 100 | godi.As[Logger](), 101 | ) 102 | 103 | // Resolve by key and interface 104 | fileLogger := godi.MustResolveKeyed[Logger](provider, "file") 105 | 106 | // Interface in group 107 | services.AddSingleton(NewEmailValidator, 108 | godi.Group("validators"), 109 | godi.As[Validator](), 110 | ) 111 | 112 | validators := godi.MustResolveGroup[Validator](provider, "validators") 113 | ``` 114 | 115 | ## Use Cases 116 | 117 | ### Swappable Implementations 118 | 119 | ```go 120 | // Production 121 | services.AddSingleton(NewProductionEmailer, godi.As[Emailer]()) 122 | 123 | // Testing 124 | services.AddSingleton(NewMockEmailer, godi.As[Emailer]()) 125 | 126 | // Code uses interface 127 | type NotificationService struct { 128 | emailer Emailer // Interface 129 | } 130 | ``` 131 | 132 | ### Repository Pattern 133 | 134 | ```go 135 | type UserRepository interface { 136 | FindByID(id int) (*User, error) 137 | FindByEmail(email string) (*User, error) 138 | Save(user *User) error 139 | } 140 | 141 | // PostgreSQL implementation 142 | type postgresUserRepository struct { 143 | db *sql.DB 144 | } 145 | 146 | func NewUserRepository(db *sql.DB) *postgresUserRepository { 147 | return &postgresUserRepository{db: db} 148 | } 149 | 150 | // Register implementation as interface 151 | services.AddScoped(NewUserRepository, godi.As[UserRepository]()) 152 | 153 | // Service depends on interface 154 | type UserService struct { 155 | repo UserRepository 156 | } 157 | 158 | func NewUserService(repo UserRepository) *UserService { 159 | return &UserService{repo: repo} 160 | } 161 | ``` 162 | 163 | ### Dependency Inversion 164 | 165 | ```go 166 | // Domain layer defines interface 167 | type OrderPlacer interface { 168 | PlaceOrder(order *Order) error 169 | } 170 | 171 | // Infrastructure implements it 172 | type stripeOrderPlacer struct { 173 | client *stripe.Client 174 | } 175 | 176 | func NewStripeOrderPlacer(config *Config) *stripeOrderPlacer { 177 | return &stripeOrderPlacer{ 178 | client: stripe.NewClient(config.StripeKey), 179 | } 180 | } 181 | 182 | // Register infrastructure as domain interface 183 | services.AddSingleton(NewStripeOrderPlacer, godi.As[OrderPlacer]()) 184 | 185 | // Domain service uses interface 186 | type CheckoutService struct { 187 | placer OrderPlacer 188 | } 189 | ``` 190 | 191 | ## With Parameter Objects 192 | 193 | Reference interfaces in parameter objects: 194 | 195 | ```go 196 | type ServiceParams struct { 197 | godi.In 198 | 199 | Logger Logger // Interface 200 | Cache Cache // Interface 201 | Repo Repository // Interface 202 | } 203 | 204 | func NewService(params ServiceParams) *Service { 205 | return &Service{ 206 | logger: params.Logger, 207 | cache: params.Cache, 208 | repo: params.Repo, 209 | } 210 | } 211 | ``` 212 | 213 | ## Testing 214 | 215 | Easy to swap implementations for testing: 216 | 217 | ```go 218 | // Production setup 219 | func ProductionModule() godi.Module { 220 | return func(services *godi.ServiceCollection) { 221 | services.AddSingleton(NewProductionDB, godi.As[Database]()) 222 | services.AddSingleton(NewProductionCache, godi.As[Cache]()) 223 | } 224 | } 225 | 226 | // Test setup 227 | func TestModule() godi.Module { 228 | return func(services *godi.ServiceCollection) { 229 | services.AddSingleton(NewMockDB, godi.As[Database]()) 230 | services.AddSingleton(NewMockCache, godi.As[Cache]()) 231 | } 232 | } 233 | 234 | // In tests 235 | services := godi.NewCollection() 236 | services.AddModule(TestModule()) // Use mocks 237 | ``` 238 | 239 | ## Common Mistakes 240 | 241 | ### Resolving Concrete When Registered as Interface 242 | 243 | ```go 244 | services.AddSingleton(NewConsoleLogger, godi.As[Logger]()) 245 | 246 | // Error: *consoleLogger not registered directly 247 | logger := godi.MustResolve[*consoleLogger](provider) 248 | 249 | // Correct: resolve by interface 250 | logger := godi.MustResolve[Logger](provider) 251 | ``` 252 | 253 | ### Forgetting As Option 254 | 255 | ```go 256 | // Only registers *consoleLogger 257 | services.AddSingleton(NewConsoleLogger) 258 | 259 | // Error: Logger interface not registered 260 | logger := godi.MustResolve[Logger](provider) 261 | ``` 262 | 263 | --- 264 | 265 | **See also:** [Keyed Services](keyed-services.md) | [Parameter Objects](parameter-objects.md) 266 | -------------------------------------------------------------------------------- /docs/features/resource-cleanup.md: -------------------------------------------------------------------------------- 1 | # Resource Cleanup 2 | 3 | Automatic disposal of resources when scopes and providers close. 4 | 5 | ## How It Works 6 | 7 | Services implementing `Close() error` are automatically cleaned up: 8 | 9 | ```go 10 | type Database struct { 11 | conn *sql.DB 12 | } 13 | 14 | func (d *Database) Close() error { 15 | return d.conn.Close() 16 | } 17 | 18 | services.AddSingleton(NewDatabase) 19 | 20 | provider, _ := services.Build() 21 | // ... use database ... 22 | 23 | provider.Close() // Database.Close() called automatically 24 | ``` 25 | 26 | ## The Disposable Pattern 27 | 28 | Any type with a `Close() error` method is disposable: 29 | 30 | ```go 31 | // Automatically disposed 32 | type FileHandler struct { 33 | file *os.File 34 | } 35 | 36 | func (f *FileHandler) Close() error { 37 | return f.file.Close() 38 | } 39 | 40 | // Also automatically disposed 41 | type Connection struct { 42 | conn net.Conn 43 | } 44 | 45 | func (c *Connection) Close() error { 46 | return c.conn.Close() 47 | } 48 | ``` 49 | 50 | ## Disposal by Lifetime 51 | 52 | ### Singleton Disposal 53 | 54 | Disposed when the provider closes: 55 | 56 | ```go 57 | services.AddSingleton(NewDatabase) 58 | 59 | provider, _ := services.Build() 60 | db := godi.MustResolve[*Database](provider) 61 | // ... use throughout app ... 62 | 63 | provider.Close() // Database.Close() called here 64 | ``` 65 | 66 | ### Scoped Disposal 67 | 68 | Disposed when the scope closes: 69 | 70 | ```go 71 | services.AddScoped(NewTransaction) 72 | 73 | scope, _ := provider.CreateScope(ctx) 74 | tx := godi.MustResolve[*Transaction](scope) 75 | // ... use transaction ... 76 | 77 | scope.Close() // Transaction.Close() called here 78 | ``` 79 | 80 | ### Transient Disposal 81 | 82 | Disposed when the scope they were created in closes: 83 | 84 | ```go 85 | services.AddTransient(NewTempFile) 86 | 87 | scope, _ := provider.CreateScope(ctx) 88 | file1 := godi.MustResolve[*TempFile](scope) // Created 89 | file2 := godi.MustResolve[*TempFile](scope) // Created 90 | // Each resolution creates new instance 91 | 92 | scope.Close() // Both file1.Close() and file2.Close() called 93 | ``` 94 | 95 | ## Disposal Order 96 | 97 | Resources are disposed in reverse creation order: 98 | 99 | ``` 100 | Created: Database → Cache → UserService 101 | Disposed: UserService → Cache → Database 102 | ``` 103 | 104 | This ensures dependencies are still available during disposal. 105 | 106 | ## Error Handling 107 | 108 | Disposal errors are collected but don't stop other disposals: 109 | 110 | ```go 111 | // Custom close error handler 112 | godihttp.ScopeMiddleware(provider, 113 | godihttp.WithCloseErrorHandler(func(err error) { 114 | log.Printf("Cleanup error: %v", err) 115 | // Still continues closing other resources 116 | }), 117 | ) 118 | ``` 119 | 120 | ## Practical Examples 121 | 122 | ### Database Connection 123 | 124 | ```go 125 | type Database struct { 126 | pool *sql.DB 127 | } 128 | 129 | func NewDatabase(config *Config) (*Database, error) { 130 | pool, err := sql.Open("postgres", config.DatabaseURL) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | pool.SetMaxOpenConns(25) 136 | pool.SetMaxIdleConns(5) 137 | 138 | return &Database{pool: pool}, nil 139 | } 140 | 141 | func (d *Database) Close() error { 142 | return d.pool.Close() 143 | } 144 | ``` 145 | 146 | ### Database Transaction 147 | 148 | ```go 149 | type Transaction struct { 150 | tx *sql.Tx 151 | } 152 | 153 | func NewTransaction(db *Database) (*Transaction, error) { 154 | tx, err := db.pool.Begin() 155 | if err != nil { 156 | return nil, err 157 | } 158 | return &Transaction{tx: tx}, nil 159 | } 160 | 161 | func (t *Transaction) Close() error { 162 | // Commit on successful close, or rollback 163 | return t.tx.Commit() 164 | } 165 | 166 | // Register as scoped - one per request 167 | services.AddScoped(NewTransaction) 168 | ``` 169 | 170 | ### File Handler 171 | 172 | ```go 173 | type FileHandler struct { 174 | file *os.File 175 | } 176 | 177 | func NewFileHandler() (*FileHandler, error) { 178 | f, err := os.CreateTemp("", "app-*") 179 | if err != nil { 180 | return nil, err 181 | } 182 | return &FileHandler{file: f}, nil 183 | } 184 | 185 | func (f *FileHandler) Close() error { 186 | f.file.Close() 187 | return os.Remove(f.file.Name()) // Clean up temp file 188 | } 189 | ``` 190 | 191 | ### HTTP Client with Keep-Alive 192 | 193 | ```go 194 | type HTTPClient struct { 195 | client *http.Client 196 | } 197 | 198 | func NewHTTPClient() *HTTPClient { 199 | return &HTTPClient{ 200 | client: &http.Client{ 201 | Transport: &http.Transport{ 202 | MaxIdleConns: 100, 203 | MaxIdleConnsPerHost: 10, 204 | IdleConnTimeout: 90 * time.Second, 205 | }, 206 | }, 207 | } 208 | } 209 | 210 | func (c *HTTPClient) Close() error { 211 | c.client.CloseIdleConnections() 212 | return nil 213 | } 214 | ``` 215 | 216 | ## Web Application Pattern 217 | 218 | ```go 219 | func main() { 220 | services := godi.NewCollection() 221 | 222 | // Singletons - closed on app shutdown 223 | services.AddSingleton(NewDatabase) 224 | services.AddSingleton(NewRedisClient) 225 | 226 | // Scoped - closed per request 227 | services.AddScoped(NewTransaction) 228 | services.AddScoped(NewRequestContext) 229 | 230 | provider, _ := services.Build() 231 | defer provider.Close() // Closes singletons on shutdown 232 | 233 | mux := http.NewServeMux() 234 | handler := godihttp.ScopeMiddleware(provider)(mux) 235 | // Middleware creates/closes scopes automatically 236 | 237 | server := &http.Server{Handler: handler} 238 | 239 | // Graceful shutdown 240 | go func() { 241 | <-signalChan 242 | server.Shutdown(ctx) 243 | }() 244 | 245 | server.ListenAndServe() 246 | } 247 | ``` 248 | 249 | ## Manual Disposal 250 | 251 | You can check if a service is disposable: 252 | 253 | ```go 254 | service := godi.MustResolve[SomeService](scope) 255 | 256 | // If you need manual disposal 257 | if closer, ok := service.(godi.Disposable); ok { 258 | defer closer.Close() 259 | } 260 | ``` 261 | 262 | ## Best Practices 263 | 264 | 1. **Always defer Close()** for providers and scopes 265 | 2. **Handle close errors** with custom handlers in production 266 | 3. **Keep disposal fast** - don't do heavy work in Close() 267 | 4. **Log disposal errors** for debugging 268 | 5. **Use scoped lifetime** for per-request resources like transactions 269 | 270 | ## Common Resources to Dispose 271 | 272 | | Resource | Lifetime | Close Action | 273 | | --------------- | --------- | ---------------------- | 274 | | Database pool | Singleton | Close connections | 275 | | Redis client | Singleton | Close connections | 276 | | HTTP client | Singleton | Close idle connections | 277 | | File handle | Transient | Close and delete | 278 | | DB transaction | Scoped | Commit/rollback | 279 | | gRPC connection | Singleton | Close connection | 280 | | WebSocket | Scoped | Close connection | 281 | 282 | --- 283 | 284 | **See also:** [Service Lifetimes](../concepts/lifetimes.md) | [Scopes](../concepts/scopes.md) 285 | -------------------------------------------------------------------------------- /docs/features/result-objects.md: -------------------------------------------------------------------------------- 1 | # Result Objects 2 | 3 | Register multiple services from a single constructor. 4 | 5 | ## The Problem 6 | 7 | One constructor creates multiple related services: 8 | 9 | ```go 10 | // Creates both a Database and a HealthChecker 11 | func NewDatabaseConnection(config *Config) (*Database, *HealthChecker) { 12 | db := connectDB(config) 13 | health := &HealthChecker{db: db} 14 | return db, health 15 | } 16 | 17 | // How to register both? 18 | ``` 19 | 20 | ## The Solution: Result Objects 21 | 22 | Use `godi.Out` to return multiple services: 23 | 24 | ```go 25 | type DatabaseResult struct { 26 | godi.Out 27 | 28 | Database *Database 29 | HealthChecker *HealthChecker 30 | } 31 | 32 | func NewDatabaseConnection(config *Config) DatabaseResult { 33 | db := connectDB(config) 34 | return DatabaseResult{ 35 | Database: db, 36 | HealthChecker: &HealthChecker{db: db}, 37 | } 38 | } 39 | 40 | // Register once, get both services 41 | services.AddSingleton(NewDatabaseConnection) 42 | 43 | // Resolve each separately 44 | db := godi.MustResolve[*Database](provider) 45 | health := godi.MustResolve[*HealthChecker](provider) 46 | ``` 47 | 48 | ## Basic Usage 49 | 50 | ```go 51 | // 1. Define result struct with embedded godi.Out 52 | type Result struct { 53 | godi.Out // Must be embedded anonymously 54 | 55 | Service1 *Service1 56 | Service2 *Service2 57 | Service3 *Service3 58 | } 59 | 60 | // 2. Return from constructor 61 | func NewServices(deps Dependencies) Result { 62 | return Result{ 63 | Service1: NewService1(deps), 64 | Service2: NewService2(deps), 65 | Service3: NewService3(deps), 66 | } 67 | } 68 | 69 | // 3. Register once 70 | services.AddSingleton(NewServices) 71 | 72 | // 4. Resolve individually 73 | s1 := godi.MustResolve[*Service1](provider) 74 | s2 := godi.MustResolve[*Service2](provider) 75 | s3 := godi.MustResolve[*Service3](provider) 76 | ``` 77 | 78 | ## Field Tags 79 | 80 | ### Named Services 81 | 82 | ```go 83 | type CacheResult struct { 84 | godi.Out 85 | 86 | RedisCache Cache `name:"redis"` 87 | MemoryCache Cache `name:"memory"` 88 | } 89 | 90 | func NewCaches(config *Config) CacheResult { 91 | return CacheResult{ 92 | RedisCache: NewRedisCache(config.RedisURL), 93 | MemoryCache: NewMemoryCache(config.CacheSize), 94 | } 95 | } 96 | 97 | // Resolve by name 98 | redis := godi.MustResolveKeyed[Cache](provider, "redis") 99 | memory := godi.MustResolveKeyed[Cache](provider, "memory") 100 | ``` 101 | 102 | ### Group Membership 103 | 104 | ```go 105 | type ValidatorResult struct { 106 | godi.Out 107 | 108 | EmailValidator Validator `group:"validators"` 109 | PhoneValidator Validator `group:"validators"` 110 | AddressValidator Validator `group:"validators"` 111 | } 112 | 113 | func NewValidators() ValidatorResult { 114 | return ValidatorResult{ 115 | EmailValidator: &EmailValidator{}, 116 | PhoneValidator: &PhoneValidator{}, 117 | AddressValidator: &AddressValidator{}, 118 | } 119 | } 120 | 121 | // Resolve as group 122 | validators := godi.MustResolveGroup[Validator](provider, "validators") 123 | ``` 124 | 125 | ### Interface Binding 126 | 127 | ```go 128 | type RepositoryResult struct { 129 | godi.Out 130 | 131 | UserRepo UserRepository `as:"UserRepository"` 132 | OrderRepo OrderRepository `as:"OrderRepository"` 133 | } 134 | ``` 135 | 136 | ## Use Cases 137 | 138 | ### Database with Health Checker 139 | 140 | ```go 141 | type DatabaseResult struct { 142 | godi.Out 143 | 144 | DB *Database 145 | Health *HealthChecker 146 | Migrations *MigrationRunner 147 | } 148 | 149 | func NewDatabase(config *Config, logger *Logger) (DatabaseResult, error) { 150 | db, err := sql.Open("postgres", config.DatabaseURL) 151 | if err != nil { 152 | return DatabaseResult{}, err 153 | } 154 | 155 | return DatabaseResult{ 156 | DB: &Database{db}, 157 | Health: &HealthChecker{db}, 158 | Migrations: &MigrationRunner{db, logger}, 159 | }, nil 160 | } 161 | ``` 162 | 163 | ### Cache Layer 164 | 165 | ```go 166 | type CacheResult struct { 167 | godi.Out 168 | 169 | LocalCache Cache `name:"local"` 170 | RemoteCache Cache `name:"remote"` 171 | TieredCache Cache `name:"tiered"` 172 | } 173 | 174 | func NewCacheLayer(config *Config) CacheResult { 175 | local := NewMemoryCache(config.LocalCacheSize) 176 | remote := NewRedisCache(config.RedisURL) 177 | tiered := NewTieredCache(local, remote) 178 | 179 | return CacheResult{ 180 | LocalCache: local, 181 | RemoteCache: remote, 182 | TieredCache: tiered, 183 | } 184 | } 185 | ``` 186 | 187 | ### HTTP Client Suite 188 | 189 | ```go 190 | type HTTPClientResult struct { 191 | godi.Out 192 | 193 | DefaultClient *http.Client `name:"default"` 194 | TimeoutClient *http.Client `name:"timeout"` 195 | RetryingClient *http.Client `name:"retrying"` 196 | } 197 | 198 | func NewHTTPClients(config *Config) HTTPClientResult { 199 | return HTTPClientResult{ 200 | DefaultClient: &http.Client{}, 201 | TimeoutClient: &http.Client{Timeout: config.HTTPTimeout}, 202 | RetryingClient: NewRetryingClient(config.MaxRetries), 203 | } 204 | } 205 | ``` 206 | 207 | ## Combining In and Out 208 | 209 | Use both parameter and result objects: 210 | 211 | ```go 212 | type ServiceParams struct { 213 | godi.In 214 | 215 | Config *Config 216 | Logger *Logger 217 | Database *Database 218 | } 219 | 220 | type ServiceResult struct { 221 | godi.Out 222 | 223 | UserService *UserService 224 | OrderService *OrderService 225 | AdminService *AdminService 226 | } 227 | 228 | func NewServices(params ServiceParams) ServiceResult { 229 | return ServiceResult{ 230 | UserService: NewUserService(params.Database, params.Logger), 231 | OrderService: NewOrderService(params.Database, params.Logger), 232 | AdminService: NewAdminService(params.Database, params.Logger, params.Config), 233 | } 234 | } 235 | ``` 236 | 237 | ## With Errors 238 | 239 | Result objects work with error returns: 240 | 241 | ```go 242 | func NewServices(config *Config) (ServiceResult, error) { 243 | db, err := connectDB(config) 244 | if err != nil { 245 | return ServiceResult{}, err 246 | } 247 | 248 | return ServiceResult{ 249 | Database: db, 250 | Health: &HealthChecker{db}, 251 | }, nil 252 | } 253 | ``` 254 | 255 | ## Common Mistakes 256 | 257 | ### Named Embedding 258 | 259 | ```go 260 | // Wrong 261 | type BadResult struct { 262 | Out godi.Out // Named - won't work 263 | Service *Service 264 | } 265 | 266 | // Correct 267 | type GoodResult struct { 268 | godi.Out // Anonymous 269 | Service *Service 270 | } 271 | ``` 272 | 273 | ### Unexported Fields 274 | 275 | ```go 276 | // Wrong 277 | type BadResult struct { 278 | godi.Out 279 | service *Service // lowercase - not registered 280 | } 281 | 282 | // Correct 283 | type GoodResult struct { 284 | godi.Out 285 | Service *Service // Uppercase - registered 286 | } 287 | ``` 288 | 289 | --- 290 | 291 | **See also:** [Parameter Objects](parameter-objects.md) | [Interface Binding](interface-binding.md) 292 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to godi 2 | 3 | Thank you for your interest in contributing to godi! This document provides guidelines and instructions for contributing. 4 | 5 | ## Table of Contents 6 | 7 | - [Getting Started](#getting-started) 8 | - [Development Setup](#development-setup) 9 | - [Making Changes](#making-changes) 10 | - [Commit Message Convention](#commit-message-convention) 11 | - [Pull Request Process](#pull-request-process) 12 | - [Testing](#testing) 13 | - [Release Process](#release-process) 14 | - [Areas for Contribution](#areas-for-contribution) 15 | 16 | ## Getting Started 17 | 18 | 1. Fork the repository on GitHub 19 | 2. Clone your fork locally: 20 | ```bash 21 | git clone https://github.com/your-username/godi.git 22 | cd godi 23 | ``` 24 | 3. Add the upstream repository: 25 | ```bash 26 | git remote add upstream https://github.com/junioryono/godi.git 27 | ``` 28 | 4. Create a new branch for your feature or fix: 29 | ```bash 30 | git checkout -b feat/my-new-feature 31 | # or 32 | git checkout -b fix/issue-123 33 | ``` 34 | 35 | ## Development Setup 36 | 37 | 1. Ensure you have Go 1.23 or later installed 38 | 2. Install dependencies: 39 | ```bash 40 | go mod download 41 | ``` 42 | 3. Run tests: 43 | ```bash 44 | make test 45 | # or with coverage 46 | make test-cover 47 | ``` 48 | 4. Run linter: 49 | ```bash 50 | make lint 51 | ``` 52 | 53 | ## Making Changes 54 | 55 | ### Code Style 56 | 57 | - Follow standard Go conventions and idioms 58 | - Use `gofmt` to format your code 59 | - Run `make lint` before committing 60 | - Use meaningful variable and function names 61 | - Add comments for exported types, functions, and methods 62 | - Keep line length reasonable (around 120 characters) 63 | 64 | ### Testing 65 | 66 | - Write tests for new functionality 67 | - Ensure all tests pass before submitting 68 | - Aim for high test coverage (run `make test-cover`) 69 | - Include both unit tests and integration tests where appropriate 70 | - Test edge cases and error conditions 71 | 72 | ### Documentation 73 | 74 | - Update godoc comments for any API changes 75 | - Add examples in documentation where helpful 76 | - Update README.md if adding new features 77 | - No need to update CHANGELOG.md - it's automated! 78 | 79 | ## Commit Message Convention 80 | 81 | **This project uses [Conventional Commits](https://www.conventionalcommits.org/) for automated versioning and changelog generation.** 82 | 83 | ### Format 84 | 85 | ``` 86 | (): 87 | 88 | 89 | 90 |