golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.98k stars 17.67k forks source link

proposal: Go 2: decorator support, like python decorator #36669

Closed liamylian closed 4 years ago

liamylian commented 4 years ago

Decorator may make writing GoLang projects more efficient. I hope GoLang support some kind of decorator.

// decorator `Default` sets default value to a int variable
func (intVar *int) @Default(value int) {
    if intVar == nil || *intVar == 0 {
        intVar = &value
    }
}

// decorator `Route` adds route to a `HandlerFunc`
func (handler http.HandlerFunc) @Route(method, uri string) {
    http.HandleFunc(uri, handler)
}

@Route(method = http.MethodGet, uri = "/books/:id") // decorator `Route` run right after function `getBook` is defined. That is, `getBook.Route(http.MethodGet, "/books/:id")` will be called right after function `getBook` is defined. 
func getBook(w http.ResponseWriter, r *http.Request) error {
    return nil
}

func main() {
    @Default(value = 80) // `httpPort.Default(80)` will be called right after variable `httpPort` is initialized 
    var httpPort int

    http.ListenAndServe(fmt.Sprintf(":%d", httpPort), nil)
}
beoran commented 4 years ago

You can do something similar by using higher order functions or simply pointers. For example:

package main

import (
    "fmt"
    "net/http"
)

// `Default` sets default value to a int variable
func Default(intVar *int, value int) {
    if intVar == nil || *intVar == 0 {
        intVar = &value
    }
}

func getBookHandler(w http.ResponseWriter, r *http.Request) {
}

// Route wraps a route into a handler function
func Route(handler http.HandlerFunc, method, uri string) http.HandlerFunc  {
    return func (w http.ResponseWriter, r *http.Request) {       
        http.Handle(uri, handler)
    }
}

var getBook = Route(http.HandlerFunc(getBookHandler), http.MethodGet, "/books/:id")

func main() {
    var httpPort int
    Default(&httpPort, 80)
    fmt.Printf("httpPort %d", httpPort)

    http.ListenAndServe(fmt.Sprintf(":%d", httpPort), getBook)
}

https://play.golang.org/p/dsolvSezkqf (It does give an error since http doesn't work on the playground.) . IMO; decorators are a misfeature of Pyton, in Go we generally don't need such a confusing way to call a function before another though magic syntax.

liamylian commented 4 years ago

Yep, the previous example can be achieved by higher order functions. But in the case of auto dependency injection, you have to do it manually if without decorator.

type BookRepository interface {
     GetBook(id int) (*Book, error)
     CreateBook(book *Book) error
}

type bookService struct {
     @di.Inject // `di.Inject(bookRepository)` will be called right after `bookService` instance is created
     bookRepository BookRepository
}

func (s *bookService) CreateBook(title, author string) error {
     book := NewBook(title, author)
     return s.boookRepository.CreateBook(book)
}

func main() {
    bookRepository := NewBookRepository()
    di.Provide(bookRepository)

    bookService := new(bookService) // `di.Inject(bookService.bookRepository)`will be called right after `new`, so bookService.bookRepository is not nil here now.
    bookService.CreateBook("Jane Eyre", "Charlotte Bronte") // won't panic
}
// package di

var container map[reflect.Type]interface{}

func Provide(obj interface{}) {
    typ := reflect.TypeOf(obj)
    container[typ] = obj
}

// decorator `Inject` injects a previous provided instance to obj, which might be nil before
func (obj interface{}) @Inject() {
    typ := reflect.TypeOf(obj)
    obj = container[typ]
}

As far as this feature is concerned, maybe there is a balance between confusing, beauty, productivity, etc.

ianlancetaylor commented 4 years ago

Like @beoran , I can't see the point of @Default at all. There's no reason to add a new syntax to do something that we can already do.

I don't know what @di.Inject is supposed to mean.

liamylian commented 4 years ago

@ianlancetaylor I've updated the previous comment with the code of package di. The point of @Default is a simple demo, too simple to have another point. May be @Env will look a little more convenient, but it's still not complex enough to show the power of decorator.

// Decorator `@Env` sets value to a int variable from System Environment
func (intVar *int) @Env(name string, defaultValue int) {
    if intVar != nil && *intVar != 0 {
        return
    }

    val, ok := os.LookUpEnv(name)
    if !ok {
        *intVar = defaultValue
        return
    }

    intVal, err := strconv.Atoi(val)
    if err != nil {
        *intVar = defaultValue
        return
    }

    *intVar = intVal
}

type Config struct {
    @Env(/*name=*/ "HTTP_PORT", /*defaultValue=*/ 80) // `httpPort.Env("HTTP_PORT", 80)` will be called right after a new instance of `Config` is created.
    httpPort int
}{}

var config Config // config.httpPort now equals to 80, because of decorator `@Env` is executed right after variable `config` initialization.

Of course, things can be done without decorators. But the decorator syntax allows us to more conveniently alter variables, functions, etc.

Refers:

  1. https://www.python.org/dev/peps/pep-0318/
  2. https://www.python-course.eu/python3_decorators.php
ianlancetaylor commented 4 years ago

Go favors an explicit programming style. If you write out what the program does, everybody reading the program understands what happens. It's true that this leads to more verbose code than in some other programming languages. Go considers the benefit in readability to be worth the extra code.

If we take that as a given, I have no idea what benefit Go gets from adding decorators. As far as I can tell, a decorator is a way to concisely invoke a function. Go already has ways to invoke a function. In your Config example with an @Env decorator, the advantage of the decorator is that it is concise, and it applies to every instance of Config. The disadvantage is that when somebody in some other package far away writes var c pkg.Config, the value of the httpPort field is unexpectedly set. If they write c := pkg.NewConfig(), then it is clear that some fields may be changed.

beoran commented 4 years ago

There are ways to do dependency injection in Go as well, either using reflection, or using code generation, such as this tool/library: https://github.com/elliotchance/dingo In Go "less is exponentially more".

ianlancetaylor commented 4 years ago

For language change proposals, please fill out the template at https://go.googlesource.com/proposal/+/bd3ac287ccbebb2d12a386f1f1447876dd74b54d/go2-language-changes.md .

When you are done, please reply to the issue with @gopherbot please remove label WaitingForInfo.

Thanks!

liamylian commented 4 years ago

Proposal: Go 2: function decorator support

After above discussion, maybe only function decorator is a simple and useful sugar. Other decorators, like variable decorator is not so predictable that disobey GoLang's design principle.

Before:

package main

import (
    "fmt"
    "log"
    "time"
)

type Handler func(string) error

func Recover(h Handler) Handler {
    return func(arg string) error {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("recoved from: %v", r)
            }
        }()

        return h(arg)
    }
}

func Async(h Handler) Handler {
    return func(arg string) error {
        go h(arg)
        return nil
    }
}

func Log(h Handler) Handler {
    return func(arg string) error {
        err := h(arg)
        log.Printf("called f with %s, returns %v", arg, err)
        return err
    }
}

func hello(name string) error {
    fmt.Printf("Hello, %s\n", name)
    panic("Ouch")
}

var Hello = Async(Log(Recover(hello)))

func main() {
    Hello("Beoran")
    time.Sleep(time.Second)
}

After:

package main

import (
    "fmt"
    "log"
    "time"
)

type Handler func(string) error

func (h *Handler) @Recover() {
    wrapped := func(arg string) error {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("recoved from: %v", r)
            }
        }()

        return h(arg)
    }

    *h = wrapped
}

func (h *Handler) @Async() {
    wrapped := func(arg string) error {
        go h(arg)
        return nil
    }

    *h = wrapped
}

func (h *Handler) @Log() {
    wrapped := func(arg string) error {
        err := h(arg)
        log.Printf("called f with %s, returns %v", arg, err)
        return err
    }

    *h = wrapped
}

@Async
@Log
@Recover
func Hello(name string) error {
    fmt.Printf("Hello, %s\n", name)
    panic("Ouch")
}

func main() {
    Hello("Beoran")
    time.Sleep(time.Second)
}

@gopherbot please remove label WaitingForInfo

ianlancetaylor commented 4 years ago

Thanks for filling out the template.

This proposal introduces a new way to do something that the language can already do. Perhaps if we had used this style initially this would be a good fit. But we have years of code that does not use this new approach. This new approach does not seem to provide enough advantage over the existing approach to warrant inclusion.

Also, there is little support for this based on emoji voting.

For these reasons this is a likely decline. Leaving open for four weeks for final comments.

ianlancetaylor commented 4 years ago

No further comments.