Kod
Kod stands for Killer Of Dependency, a generics based dependency injection framework for Go.
Although it seems that most Go enthusiasts dislike dependency injection, many companies that widely use Go for their development projects have open-sourced their own dependency injection frameworks. For example, Google has open-sourced Wire, Uber has open-sourced Fx, and Facebook has open-sourced Inject. This is truly a strange phenomenon.
Feature
- Component Based: Kod is a component-based framework. Components are the building blocks of a Kod application.
- Configurable: Kod can use TOML/YAML/JSON files to configure how applications are run.
- Testing: Kod includes a Test function that you can use to test your Kod applications.
- Logging: Kod provides a logging API,
kod.L
. Kod also integrates the logs into the environment where your application is deployed. - OpenTelemetry: Kod relies on OpenTelemetry to collect trace and metrics from your application.
- Hooks: Kod provides a way to run code when a component start or stop.
- Interceptors: Kod has built-in common interceptors, and components can implement the following methods to inject these interceptors into component methods.
- Interface Generation: Kod provides a way to generate interface from structure.
- Code Generation: Kod provides a way to generate kod related codes for your kod application.
Components
Kod’s core abstraction is the component. A component is like an actor, and a Kod application is implemented as a set of components. Concretely, a component is represented with a regular Go interface, and components interact with each other by calling the methods defined by these interfaces.
In this section, we’ll define a simple hello component that just prints a string and returns. First, run go mod init hello
to create a go module.
mkdir hello/
cd hello/
go mod init hello
Then, create a file called main.go
with the following contents:
package main
import (
"context"
"fmt"
"log"
"github.com/go-kod/kod"
)
func main() {
if err := kod.Run(context.Background(), serve); err != nil {
log.Fatal(err)
}
}
// app is the main component of the application. kod.Run creates
// it and passes it to serve.
type app struct{
kod.Implements[kod.Main]
}
// serve is called by kod.Run and contains the body of the application.
func serve(context.Context, *app) error {
fmt.Println("Hello")
return nil
}
kod.Run(...)
initializes and runs the Kod application. In particular, kod.Run
finds the main component, creates it, and passes it to a supplied function. In this example, app
is the main component since it contains a kod.Implements[kod.Main]
field.
go mod tidy
kod generate .
go run .
Hello
FUNDAMENTALS
Components
Components are Kod’s core abstraction. Concretely, a component is represented as a Go interface and corresponding implementation of that interface. Consider the following Adder
component for example:
type Adder interface {
Add(context.Context, int, int) (int, error)
}
type adder struct {
kod.Implements[Adder]
}
func (*adder) Add(_ context.Context, x, y int) (int, error) {
return x + y, nil
}
Adder defines the component’s interface, and adder defines the component’s implementation. The two are linked with the embedded kod.Implements[Adder]
field. You can call kod.Ref[Adder].Get()
to get a caller to the Adder component.
Implementation
A component implementation must be a struct that looks like:
type foo struct{
kod.Implements[Foo]
// ...
}
It must be a struct.
It must embed a kod.Implements[T]
field where T is the component interface it implements.
If a component implementation implements an Init(context.Context) error
method, it will be called when an instance of the component is created.
func (f *foo) Init(context.Context) error {
// ...
}
func (f *foo) Shutdown(context.Context) error {
// ...
}
Lazy Initialization
Components can be lazily initialized by embedding a kod.LazyInit
field in the component implementation,
which will be initialized when the component is first used, instead of when the application starts.
Simple demo below:
type foo struct {
kod.Implements[Foo]
kod.LazyInit
}
Interceptors
Kod has built-in common interceptors, and components can implement the following methods to inject these interceptors into component methods:
func (f *foo) Interceptors() []interceptor.Interceptor {
return []interceptor.Interceptor{
kmetric.New(),
ktrace.New(),
}
}
Interfaces
Interface can be generated automatically by kod tool.
//go:generate kod struct2interface .
Config
Kod uses config files, written in TOML, to configure how applications are run. A minimal config file, for example, simply lists the application name:
[kod]
name = "hello"
A config file may also contain component-specific configuration sections, which allow you configuring the components in your application. For example, consider the following Greeter
component.
type Greeter interface {
Greet(context.Context, string) (string, error)
}
type greeter struct {
kod.Implements[Greeter]
}
func (g *greeter) Greet(_ context.Context, name string) (string, error) {
return fmt.Sprintf("Hello, %s!", name), nil
}
Rather than hard-coding the greeting “Hello”, we can provide a greeting in a config file. First, we define an options
struct.
type greeterOptions struct {
Greeting string
}
Next, we associate the options
struct with the greeter implementation by embedding the kod.WithConfig[T]
struct.
type greeter struct {
kod.Implements[Greeter]
kod.WithConfig[greeterOptions]
}
Now, we can add a Greeter section to the config file. The section is keyed by the full path-prefixed name of the component.
["example.com/mypkg/Greeter"]
Greeting = "Bonjour"
When the Greeter component is created, Kod will automatically parse the Greeter section of the config file into a greeterOptions
struct. You can access the populated struct via the Config
method of the embedded WithConfig
struct. For example:
func (g *greeter) Greet(_ context.Context, name string) (string, error) {
greeting := g.Config().Greeting
if greeting == "" {
greeting = "Hello"
}
return fmt.Sprintf("%s, %s!", greeting, name), nil
}
You can use TOML struct tags to specify the name that should be used for a field in a config file. For example, we can change the greeterOptions
struct to the following.
type greeterOptions struct {
Greeting string `toml:"my_custom_name"`
}
Testing
Unit Test
Kod includes a Test
function that you can use to test your Kod applications. For example, create an adder_test.go
file with the following contents.
package main
import (
"context"
"testing"
"github.com/go-kod/kod"
)
func TestAdd(t *testing.T) {
kod.RunTest(t, func(ctx context.Context, adder Adder) {
got, err := adder.Add(ctx, 1, 2)
if err != nil {
t.Fatal(err)
}
if want := 3; got != want {
t.Fatalf("got %q, want %q", got, want)
}
})
}
Run go test to run the test. kod.RunTest
will create a sub-test and within it will create an Adder component and pass it to the supplied function. If you want to test the implementation of a component, rather than its interface, specify a pointer to the implementing struct as an argument. For example, if the adderImpl
struct implemented the Adder interface, we could write the following:
kod.RunTest(t, func(ctx context.Context, adder *adderImpl) {
// Test adder...
})
Benchmark
You can also use kod.RunTest
to benchmark your application. For example, create an adder_benchmark.go file with the following contents.
package main
import (
"context"
"testing"
"github.com/go-kod/kod"
)
func BenchmarkAdd(b *testing.B) {
kod.RunTest(b, func(ctx context.Context, adder Adder) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := adder.Add(ctx, 1, 2)
if err != nil {
b.Fatal(err)
}
}
})
}
Fake
You can replace a component implementation with a fake implementation in a test using kod.Fake
. Here’s an example where we replace the real implementation of a Clock component with a fake implementation that always returns a fixed time.
// fakeClock is a fake implementation of the Clock component.
type fakeClock struct {
now int64
}
// Now implements the Clock component interface. It returns the current time, in
// microseconds, since the unix epoch.
func (f *fakeClock) Now(context.Context) (int64, error) {
return f.now, nil
}
func TestClock(t *testing.T) {
t.Run("fake", func(t *testing.T) {
// Register a fake Clock implementation with the runner.
fake := kod.Fake[Clock](&fakeClock{100})
// When a fake is registered for a component, all instances of that
// component dispatch to the fake.
kod.RunTest(t, func(ctx context.Context, clock Clock) {
now, err := clock.UnixMicro(ctx)
if err != nil {
t.Fatal(err)
}
if now != 100 {
t.Fatalf("bad time: got %d, want %d", now, 100)
}
fake.now = 200
now, err = clock.UnixMicro(ctx)
if err != nil {
t.Fatal(err)
}
if now != 200 {
t.Fatalf("bad time: got %d, want %d", now, 200)
}
}, kod.WithFakes(fake))
})
}
Config
You can also provide the contents of a config file to a runner by setting the Runner.Config
field:
func TestArithmetic(t *testing.T) {
kod.RunTest(t, func(ctx context.Context, adder Adder) {
// ...
}, kod.WithConfigFile("testdata/config.toml"))
}
Logging
Kod provides a logging API, kod.L
. Kod also integrates the logs into the environment where your application is deployed.
Use the Logger method of a component implementation to get a logger scoped to the component. For example:
type Adder interface {
Add(context.Context, int, int) (int, error)
}
type adder struct {
kod.Implements[Adder]
}
func (a *adder) Add(ctx context.Context, x, y int) (int, error) {
// adder embeds kod.Implements[Adder] which provides the L method.
logger := a.L(ctx)
logger.DebugContext(ctx, "A debug log.")
logger.InfoContext(ctx, "An info log.")
logger.ErrorContext(ctx, "An error log.", fmt.Errorf("an error"))
return x + y, nil
}
Acknowledge
This project was heavily inspired by ServiceWeaver.
Star History
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=go-kod/kod&type=Timeline&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=go-kod/kod&type=Timeline" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=go-kod/kod&type=Timeline" />