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: errors: generalize As to all types #33473

Closed
bhenderson opened this issue Aug 5, 2019 · 8 comments
Closed

proposal: errors: generalize As to all types #33473

bhenderson opened this issue Aug 5, 2019 · 8 comments

Comments

@bhenderson
Copy link

@bhenderson bhenderson commented Aug 5, 2019

The errors proposal introduces a new paradigm for inspecting wrapped errors.

var target *Type
if errors.As(err, &target) {
   ...
}

I love this new paradigm and I think it should be extended to any interface. The one that immediately comes to my mind that would benefit is http.ResponseWrapper.
http.ResponseWriter interface has 3 methods, but there are 5 other methods that the underlying type can implement:
http.CloseNotifier, http.Flusher, http.Hijacker, io.ReaderFrom, http.Pusher

There are 2 "easy" ways to address this: a wrapping type can either expose them directly, and if the underlying type doesn't implement them, the method becomes a no-op; or the wrapping type can expose the original ResponseWriter, but this may leak functionality.

func (wrapped http.ResponseWriter, r *http.Request) {
  if w, ok := wrapped.(interface{ ResponseWriter() http.ResponseWriter }); ok {
    if f, ok := w.ResponseWriter().(http.Flusher); ok {
      f.Flush()
    }
    // w.ResponseWriter().Write() can now also be called, which might break the purpose of the wrapper in the first place.
  }
}

This nested wrapping looks very similar to the errors problem. But what if the wrapper was written like this?

type MyLoggingWriter struct {
  http.ResponseWriter
  length int
}

func (w *MyLoggingWriter) As(type interface{}) bool {
  return As(w.ResponseWriter, type)
}

// also would override Write() method to record num bytes written
...

func (wrapped http.RepsonseWriter, r *http.Request) {
  if h := new(http.Hijacker); As(wrapped, &h) {
    // now you can hijack the session
  }
}

Some unknowns about this proposal:

  • the new As method needs a package. I originally thought of reflect but that would mean pulling in reflect pkg a lot more.
  • This introduces another way of doing type assertions, which in general, the go community frowns upon.

I think the real power in the As method in how it allows the input type to define it's own As(interface{}) method. We could do without a generalized method and just have the paradigm that wrapping interfaces implement an As unwrapper. An implementation would look something like this:

func (w *MyLoggingWriter) As(v interface{}) bool {
  switch x := v.(type) {
  case **http.Hijacker:
    if h, ok := w.ResponseWriter.(http.Hijacker); ok {
      *x = &h
      return true
    }
    // ... repeat for all the others you want to implement
}

This is tedious and not future proof. However, a generalized method depends on reflection.
Maybe a hybrid approach could be take where libraries that don't want to pull in reflection can implement it manually like above. I would note that the current errors proposal depends on reflection and that it would probably be used just as often. I think that this also allows for optimizations in the future where reflection could potentially be removed and the interface be unchanged.

Some corollaries:

  • I think the Is and Unwrap functions are superfluous, but I haven't totally thought through that.

Isn't errors.Is(err, syscall.EADDRNOTAVAIL) the same as: As(err, syscall.EADDRNOTAVAIL) if err sufficiently implements As(type interface)?

@gopherbot gopherbot added this to the Proposal milestone Aug 5, 2019
@gopherbot gopherbot added the Proposal label Aug 5, 2019
@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 5, 2019

func (wrapped http.ResponseWriter, r *http.Request) {

You wrote that twice but I don't understand what this means. There is no name for this function/method. What does this mean? How is it called?

@bhenderson
Copy link
Author

@bhenderson bhenderson commented Aug 5, 2019

it's a stand-in for an http.HandlerFunc. A common use case is I want to log the request time for an http Handler. I would usually do something like:

http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
  f, ok := w.(http.Flusher) // this doesn't work because it's wrapped
  if f := new(http.Flusher); As(w, &f) { // this would work because MyLoggingWriter implements As(interface{})
})

http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  l := MyLoggingWriter{ResponseWriter: w}
  http.DefaultServeMux.ServeHTTP(&l, r)
  // log l.length
}

thanks for your time.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 5, 2019

I see, thanks.

The new errors.As function is only meaningful with a clear definition of how to wrap and unwrap values. I think that would be the first thing to address.

@bhenderson
Copy link
Author

@bhenderson bhenderson commented Aug 6, 2019

I was thinking that errors would wrap other errors by implementing As(interface{}) bool

prototype implementation: https://play.golang.org/p/xhQF8aMcHkv

@rsc
Copy link
Contributor

@rsc rsc commented Aug 6, 2019

There's a fair amount that we have yet to learn about errors.As.
It would be good to take the time to understand it better before we try to generalize to other types.

@rsc rsc changed the title Proposal: Extend As() unwrapper paradigm to any type proposal: errors: generalize As to all types Aug 6, 2019
@carlmjohnson
Copy link
Contributor

@carlmjohnson carlmjohnson commented Aug 7, 2019

I made the same proposal in my feeedback on the xerrors package: https://gist.github.com/carlmjohnson/d06cd8d10e0aef65f565a55cc57cd986

I think @rsc is correct that we can revisit the question of generalizing this after xerrors.Is/As has been in wider use.

@rsc
Copy link
Contributor

@rsc rsc commented Aug 20, 2019

Based on the discussion above, I think we should probably decline this proposal and encourage filing a new proposal in a year or two based on our experience with errors.As. Putting this proposal on hold for a year or two would not make sense because the right thing to do in a year or two is likely to differ in the details - better to write a new proposal.

Thoughts?

@bhenderson
Copy link
Author

@bhenderson bhenderson commented Aug 21, 2019

sounds good to me. thank you for the discussion. I realized too that it's unclear what it means to expose underlying types.

@bhenderson bhenderson closed this Aug 21, 2019
@golang golang locked and limited conversation to collaborators Aug 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
5 participants
You can’t perform that action at this time.