Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
liamylian opened this issue Jan 21, 2020 · 10 comments
Closed

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

liamylian opened this issue Jan 21, 2020 · 10 comments
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal Proposal-FinalCommentPeriod v2 An incompatible library change
Milestone

Comments

@liamylian
Copy link

liamylian commented Jan 21, 2020

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)
}
@gopherbot gopherbot added this to the Proposal milestone Jan 21, 2020
@beoran
Copy link

beoran commented Jan 21, 2020

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
Copy link
Author

liamylian commented Jan 21, 2020

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 ianlancetaylor added v2 An incompatible library change LanguageChange Suggested changes to the Go language labels Jan 21, 2020
@ianlancetaylor
Copy link
Contributor

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
Copy link
Author

liamylian commented Jan 21, 2020

@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
Copy link
Contributor

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
Copy link

beoran commented Jan 21, 2020

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
Copy link
Contributor

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!

@gopherbot gopherbot added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jan 21, 2020
@ianlancetaylor ianlancetaylor added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jan 21, 2020
@liamylian
Copy link
Author

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.

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    Experienced

  • What other languages do you have experience with?
    PHP, Python, JAVA

  • Would this change make Go easier or harder to learn, and why?
    Not much, it's just a simple decorator of function.

  • Has this idea, or one like it, been proposed before? If so, how does this proposal differ?
    Not found.

  • Who does this proposal help, and why?
    Framework developers and developers using framework.
    Like SprintBoot annotations, decorator is a very concise way to add extra behavior to a given function.

  • Is this change backward compatible?
    Nope

  • Show example code before and after the change.

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)
}
  • What is the cost of this proposal?
    How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
    No.
    What is the compile time cost?
    Barely none.
    What is the run time cost?
    Barely none.

  • Can you describe a possible implementation?
    Trigger decorators when package initialization.

  • Do you have a prototype? (This is not required.)
    No

  • How would the language spec change?
    Not sure.

  • Orthogonality: how does this change interact or overlap with existing features?
    None.

  • Is the goal of this change a performance improvement?
    Nope.

  • Does this affect error handling?
    Nope.

@gopherbot please remove label WaitingForInfo

@gopherbot gopherbot removed the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jan 22, 2020
@ianlancetaylor
Copy link
Contributor

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
Copy link
Contributor

No further comments.

@golang golang locked and limited conversation to collaborators Feb 24, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal Proposal-FinalCommentPeriod v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests

4 participants