labstack / echo

High performance, minimalist Go web framework
https://echo.labstack.com
MIT License
29.46k stars 2.21k forks source link

Proposal: Application state #2670

Open behnambm opened 1 month ago

behnambm commented 1 month ago

Application state

This proposal seeks to introduce an Application State Management feature in Echo. This feature will enable the ability to access a store application wide. Mmiddlewares and request handlers will be able to access this store because they both have access to Echo struct. The state's data will not change from request to request.

Similar functionality is available in other frameworks, such as FastAPI.

API

Set(key string, val any) Get(key string) (any, bool)

We need to discuss about some cases that needs to be considered in the API design. For example, should we override the already existing data? Or return an error?

Sample

package main

import (
    "fmt"
    "github.com/labstack/echo/v4"
    "net/http"
)

func main() {
    e := echo.New()

    testClient := http.Client{}
    testClient.Timeout = 42
    e.State.Set("myClient", &testClient)

    e.GET("/", hello)

    e.Logger.Fatal(e.Start(":1323"))
}

func hello(c echo.Context) error {
    client, ok := c.Echo().State.Get("myClient")
    httpClient, ok := client.(*http.Client)
    if ok {
        fmt.Println("Timeout :", httpClient.Timeout)
    }

    return c.String(http.StatusOK, "Hello, World!")
}
behnambm commented 1 month ago

@aldas Would you please take a look at this?

aldas commented 1 month ago

@behnambm this seems a lot like dependency injection containers. DI is hard to test/mock. Consider this approach

package main

import (
    "fmt"
    "github.com/labstack/echo/v4"
    "net/http"
)

func main() {
    e := echo.New()

    g := e.Group("/users")
    RegisterUserRoutes(g)  // users.RegisterUserRoutes(g) <-- if it would be moved to separate package

    e.Logger.Fatal(e.Start(":1323"))
}

///// in bigger application you would divide code by domains. so this is example for user related domain
// package users

type userCtrl struct {
    // here you could add things that you want to access from handlers
    httpClient *http.Client 
}

func RegisterUserRoutes(g *echo.Group) {
    userCtrl := userCtrl{httpClient: &http.Client{Timeout: 42}}

    g.GET("/", userCtrl.index)
}

func (u *userCtrl) index(c echo.Context) error {
    fmt.Println("Timeout :", u.httpClient.Timeout)

    return c.String(http.StatusOK, "Hello, World!")
}

with this approach it is easy to test these handler methods and code is cleaner etc.

aldas commented 1 month ago

Also please see this comment https://github.com/labstack/echo/issues/2075#issuecomment-1016819041

aldas commented 1 month ago

a little bit off-topic but https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/ from Mat has excellent ideas for designing around testability

behnambm commented 1 month ago

@aldas Thanks for the response and also for sharing the link to the article by Mat Ryer.

I appreciate the alternative approach you’ve suggested, especially in terms of keeping the code modular and easy to test. However, I believe the proposed application state management feature provides value in specific scenarios where a centralized, application-wide state is necessary or advantageous.

Here are a few points to consider: