├── 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 |