This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Iris provides first-class support for dependency injection through request handlers and server replies based on return value(s).
Dependency Injection
With Iris you get truly safe bindings. It is blazing-fast, near to raw handlers performance because we pre-allocates necessary information before the server even goes online!
A dependency can be either a function or a static value. A function dependency can accept a previously registered dependency as its input argument too.
Example Code:
func printFromTo(from, to string) string { return "message" }
// [...]
app.ConfigureContainer(func (api *iris.APIContainer){
api.Get("/{from}/{to}", printFromTo)
})
As you've seen above the iris.Context
input argument is totally optional. Iris is smart enough to bind it as well without any hassle.
Overview
The most common scenario from a route to handle is to:
- accept one or more path parameters and request data, a payload
- send back a response, a payload (JSON, XML,...)
The new Iris Dependency Injection feature is about 33.2% faster than its predecessor on the above case. This drops down even more the performance cost between native handlers and handlers with dependencies. This reason itself brings us, with safety and performance-wise, to the new Party.ConfigureContainer(builder ...func(*iris.APIContainer)) *APIContainer
method which returns methods such as Handle(method, relativePath string, handlersFn ...interface{}) *Route
and RegisterDependency
.
Look how clean your codebase can be when using Iris':
package main
import "github.com/kataras/iris/v12"
type (
testInput struct {
Email string `json:"email"`
}
testOutput struct {
ID int `json:"id"`
Name string `json:"name"`
}
)
func handler(id int, in testInput) testOutput {
return testOutput{
ID: id,
Name: in.Email,
}
}
func main() {
app := iris.New()
app.ConfigureContainer(func(api *iris.APIContainer) {
api.Post("/{id:int}", handler)
})
app.Listen(":5000", iris.WithOptimizations)
}
Your eyes don't lie you. You read well, no ctx.ReadJSON(&v)
and ctx.JSON(send)
neither error
handling are presented. It is a huge relief but if you ever need, you still have the control over those, even errors from dependencies. Here is a quick list of the new Party.ConfigureContainer
()'s fields and methods:
// Container holds the DI Container of this Party featured Dependency Injection.
// Use it to manually convert functions or structs(controllers) to a Handler.
Container *hero.Container
// OnError adds an error handler for this Party's DI Hero Container and its handlers (or controllers).
// The "errorHandler" handles any error may occurred and returned
// during dependencies injection of the Party's hero handlers or from the handlers themselves.
OnError(errorHandler func(iris.Context, error))
// RegisterDependency adds a dependency.
// The value can be a single struct value or a function.
// Follow the rules:
// * <T> {structValue}
// * func(accepts <T>) returns <D> or (<D>, error)
// * func(accepts iris.Context) returns <D> or (<D>, error)
//
// A Dependency can accept a previous registered dependency and return a new one or the same updated.
// * func(accepts1 <D>, accepts2 <T>) returns <E> or (<E>, error) or error
// * func(acceptsPathParameter1 string, id uint64) returns <T> or (<T>, error)
//
// Usage:
//
// - RegisterDependency(loggerService{prefix: "dev"})
// - RegisterDependency(func(ctx iris.Context) User {...})
// - RegisterDependency(func(User) OtherResponse {...})
RegisterDependency(dependency interface{})
// UseResultHandler adds a result handler to the Container.
// A result handler can be used to inject the returned struct value
// from a request handler or to replace the default renderer.
UseResultHandler(handler func(next iris.ResultHandler) iris.ResultHandler)
ResultHandler
type ResultHandler func(ctx iris.Context, v interface{}) error
// Use same as a common Party's "Use" but it accepts dynamic functions as its "handlersFn" input.
Use(handlersFn ...interface{})
// Done same as a common Party's but it accepts dynamic functions as its "handlersFn" input.
Done(handlersFn ...interface{})
// Handle same as a common Party's `Handle` but it accepts one or more "handlersFn" functions which each one of them
// can accept any input arguments that match with the Party's registered Container's `Dependencies` and
// any output result; like custom structs <T>, string, []byte, int, error,
// a combination of the above, hero.Result(hero.View | hero.Response) and more.
//
// It's common from a hero handler to not even need to accept a `Context`, for that reason,
// the "handlersFn" will call `ctx.Next()` automatically when not called manually.
// To stop the execution and not continue to the next "handlersFn"
// the end-developer should output an error and return `iris.ErrStopExecution`.
Handle(method, relativePath string, handlersFn ...interface{}) *Route
// Get registers a GET route, same as `Handle("GET", relativePath, handlersFn....)`.
Get(relativePath string, handlersFn ...interface{}) *Route
// and so on...
Here is a list of the built-in dependencies that can be used right away as input parameters:
Type | Maps To |
---|---|
*mvc.Application | Current MVC Application |
iris.Context | Current Iris Context |
*sessions.Session | Current Iris Session |
context.Context | ctx.Request().Context() |
*http.Request | ctx.Request() |
http.ResponseWriter | ctx.ResponseWriter() |
http.Header | ctx.Request().Header |
time.Time | time.Now() |
string , |
|
int, int8, int16, int32, int64 , |
|
uint, uint8, uint16, uint32, uint64 , |
|
float, float32, float64 , |
|
bool , |
|
slice |
Path Parameter |
Struct | Request Body of JSON , XML , YAML , Form , URL Query , Protobuf , MsgPack |
Request & Response & Path Parameters
1. Declare Go types for client's request body and a server's response.
type (
request struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
}
response struct {
ID uint64 `json:"id"`
Message string `json:"message"`
}
)
2. Create the route handler.
Path parameters and request body are binded automatically.
- id uint64 binds to "id:uint64"
- input request binds to client request data such as JSON
func updateUser(id uint64, input request) response {
return response{
ID: id,
Message: "User updated successfully",
}
}
3. Configure the container per group and register the route.
app.Party("/user").ConfigureContainer(container)
func container(api *iris.APIContainer) {
api.Put("/{id:uint64}", updateUser)
}
4. Simulate a client request which sends data to the server and displays the response.
curl --request PUT -d '{"firstanme":"John","lastname":"Doe"}' http://localhost:8080/user/42
{
"id": 42,
"message": "User updated successfully"
}
Custom Preflight
Before we continue to the next section, register dependencies, you may want to learn how a response can be customized through the iris.Context
right before sent to the client.
The server will automatically execute the Preflight(iris.Context) error
method of a function's output struct value right before send the response to the client.
Take for example that you want to fire different HTTP status codes depending on the custom logic inside your handler and also modify the value(response body) itself before sent to the client. Your response type should contain a Preflight
method like below.
type response struct {
ID uint64 `json:"id,omitempty"`
Message string `json:"message"`
Code int `json:"code"`
Timestamp int64 `json:"timestamp,omitempty"`
}
func (r *response) Preflight(ctx iris.Context) error {
if r.ID > 0 {
r.Timestamp = time.Now().Unix()
}
ctx.StatusCode(r.Code)
return nil
}
Now, each handler that returns a *response
value will call the response.Preflight
method automatically.
func deleteUser(db *sql.DB, id uint64) *response {
// [...custom logic]
return &response{
Message: "User has been marked for deletion",
Code: iris.StatusAccepted,
}
}
If you register the route and fire a request you should see an output like this, the timestamp is filled and the HTTP status code of the response that the client will receive is 202 (Status Accepted).
{
"message": "User has been marked for deletion",
"code": 202,
"timestamp": 1583313026
}
Register Dependencies
1. Import packages to interact with a database. The go-sqlite3 package is a database driver for SQLite.
import "database/sql"
import _ "github.com/mattn/go-sqlite3"
2. Configure the container (see above), register your dependencies. Handler expects an *sql.DB instance.
localDB, _ := sql.Open("sqlite3", "./foo.db")
api.RegisterDependency(localDB)
3. Register a route to create a user.
api.Post("/{id:uint64}", createUser)
4. The create user Handler.
The handler accepts a database and some client request data such as JSON, Protobuf, Form, URL Query and e.t.c. It Returns a response.
func createUser(db *sql.DB, user request) *response {
// [custom logic using the db]
userID, err := db.CreateUser(user)
if err != nil {
return &response{
Message: err.Error(),
Code: iris.StatusInternalServerError,
}
}
return &response{
ID: userID,
Message: "User created",
Code: iris.StatusCreated,
}
}
5. Simulate a client to create a user.
# JSON
curl --request POST -d '{"firstname":"John","lastname":"Doe"}' \
--header 'Content-Type: application/json' \
http://localhost:8080/user
# Form (multipart)
curl --request POST 'http://localhost:8080/users' \
--header 'Content-Type: multipart/form-data' \
--form 'firstname=John' \
--form 'lastname=Doe'
# Form (URL-encoded)
curl --request POST 'http://localhost:8080/users' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'firstname=John' \
--data-urlencode 'lastname=Doe'
# URL Query
curl --request POST 'http://localhost:8080/users?firstname=John&lastname=Doe'
Response:
{
"id": 42,
"message": "User created",
"code": 201,
"timestamp": 1583313026
}
Return values
- if the return value is
string
then it will send that string as the response's body. - If it's an
int
then it will send it as a status code. - If it's an
error
then it will set a bad request with that error as its reason. - If it's an
error
and anint
then the error code is the output integer instead of 400(bad request). - If it's a custom
struct
then it sent as a JSON, when a Content-Type header is not already set. - If it's a custom
struct
and astring
then the second output value, string, it will be the Content-Type and so on.
Type | Replies to |
---|---|
string | body |
string, string | content-type, body |
string, int | body, status code |
int | status code |
int, string | status code, body |
error | if not nil, bad request |
any, bool | if false then fires not found |
<Τ> | JSON body |
<Τ>, string | body, content-type |
<Τ>, error | JSON body or bad request |
<Τ>, int | JSON body, status code |
Result | calls its Dispatch method |
PreflightResult | calls its Preflight method |
Where
<T>
means any struct value.
type response struct {
ID uint64 `json:"id,omitempty"`
Message string `json:"message"`
Code int `json:"code"`
Timestamp int64 `json:"timestamp,omitempty"`
}
func (r *response) Preflight(ctx iris.Context) error {
if r.ID > 0 {
r.Timestamp = time.Now().Unix()
}
ctx.StatusCode(r.Code)
return nil
}
func deleteUser(db *sql.DB, id uint64) *response {
// [...custom logic]
return &response{
Message: "User has been marked for deletion",
Code: iris.StatusAccepted,
}
}
Later on you'll see how this knowledge will help you to craft an application using the MVC architectural pattern which Iris provides wonderful API for it.
- What is Iris
- 📌Getting Started
- Host
- Configuration
- Routing
- HTTP Method Override
- API Versioning
- Content Negotiation
- Response Recorder
- HTTP Referrer
- Request Authentication
- URL Query Parameters
- Forms
- Model Validation
- Cache
- View
- Cookies
- Sessions
- Websockets
- Dependency Injection
- MVC
- gRPC
- Sitemap
- Localization
- Testing
- 🤓Resources
Home | Project | Quick Start | Technical Docs | Copyright © 2019-2020 Gerasimos Maropoulos. Documentation terms of use.