Live data from Hacker News

The problem with Go’s default HTTP handlers

preslav.me

61–70 of 97 posts

Re: The problem with Go’s default HTTP handlers

#61

Earlier quoted context omitted.

> In the HTTP handlers it makes sense that you don't have return values, because: What would you do with that value exactly? I think that approach used by clojure's ring shows an elegant way to represent http responses https://github.com/ring-clojure/ring/wiki/Concepts#responses . They are essentially structs with the following fields: status := number headers := map of string->string body := stream | string | seq |…

There are major performance advantages, especially once you're past HTTP/1.1, to passing a mutable response (i.e. in Go a ResponseWriter) to the handler rather than expecting the handler to return something created whole-cloth.

What advantages are you thinking of? The obvious one is that you might want to stream the response rather than construct it in-memory at once. But as I read the parent (and as I've used similar interfaces in Rust), you don't have to have constructed the whole response with this interface. You can return something whose body is a stream.

Re: The problem with Go’s default HTTP handlers

#62
post #61

Earlier quoted context omitted.

There are major performance advantages, especially once you're past HTTP/1.1, to passing a mutable response (i.e. in Go a ResponseWriter) to the handler rather than expecting the handler to return something created whole-cloth.

What advantages are you thinking of? The obvious one is that you might want to stream the response rather than construct it in-memory at once. But as I read the parent (and as I've used similar interfaces in Rust), you don't have to have constructed the whole response with this interface. You can return something whose body is a stream.

If you return something whose body is a stream you still have to construct that thing, including the stream. And if you return something whose body is a stream you didn't fill in yet, you need to create entire async thunks or threads to fill that data in. You have also gained ~nothing.

Re: The problem with Go’s default HTTP handlers

#63
IMHO this boils down to purity vs side effects. Given a black box function, it's a lot easier to reason about what it did if it behaves in a pure manner returning some data structure that represents some form of state, than passing some mutable complex real world thing into it and then trying to inspect what the thing did after the fact.

But the matter of purity vs side effects is completely orthogonal to the default HTTP handler design. The handler provides the inputs and the output sinks that represent the "real world". But it's entirely up to you how you manage the data therein. You can continue to procedurally consume these input/output entities turtles all the way down, or you can work in a functional style until you are actually ready to flush the result of the computation down the writer.

Re: The problem with Go’s default HTTP handlers

#64
There's a subtle problem in the proposed solution: if an error happens after you already started writing the response, you can't change the http status code.

So, what should you do with an error that happens while writing the HTTP response itself? Maybe keep statistics on how often it happens, in case happens often enough that it seems relevant for debugging. But there's no good way to report an error to the client because the network connection is likely broken.

If you're not going to keep statistics, dropping errors from writing an http response is reasonable and arguably correct.

Re: The problem with Go’s default HTTP handlers

#66
post #19
post #4

I like the way Axum for Rust handles this. All response handlers return something that implements IntoResponse. Lots of types implement IntoResponse, thus making the simple cases really streamlined. async fn create_user( Json(payload): Json , ) -> impl IntoResponse { let user = User { id: 1337, username: payload.username, }; // this will be converted into a JSON response // with a status code of `201 Created` (Status…

That is what Gin does.

Gin is excellent. It's the right balance of batteries included, sensible defaults, and black magic

Re: The problem with Go’s default HTTP handlers

#67
post #61

Earlier quoted context omitted.

What advantages are you thinking of? The obvious one is that you might want to stream the response rather than construct it in-memory at once. But as I read the parent (and as I've used similar interfaces in Rust), you don't have to have constructed the whole response with this interface. You can return something whose body is a stream.

If you return something whose body is a stream you still have to construct that thing, including the stream. And if you return something whose body is a stream you didn't fill in yet, you need to create entire async thunks or threads to fill that data in. You have also gained ~nothing.

> There are major performance advantages

You didn't outline the performance advantage. You've got "you need to create entire async thunks or threads to fill that data in", but that absolutely isn't true.

Let's look at how we might stream a file in response in go with the current model vs this new model, and you can point out where the performance difference is:

    // current
    func responseHandler(rw http.ResponseWriter, req *http.Request) {
        fi, err := os.OpenFile("/tmp/file")
        if err != nil {
            rw.WriteHeader(500)
            rw.Write([]byte("error opening file"))
        }
        io.Copy(rw, fi)
        fi.Close()
    }

    // returning a streaming Response object
    func responseHandler(req *http.Request) *http.HandlerResponse {
        fi, err := os.OpenFile("/tmp/file")
        if err != nil {
            return &http.HandlerResponse{Status: 500, Body: ioutil.NopCloser(bytes.NewReader([]byte("error opening file"))}
        }
        return &http.HandlerResponse{Status: 200, Body: fi}
    }

In the happy path, there's no difference in number of goroutines or anything, right? In both cases, this all runs in the goroutine or thread or whatever that the caller _already_ created for the request handler. What difference does it make if the request handler calls `io.Copy(response, body)` or if your function does, in both case it's the same goroutine/thread.

> You have also gained ~nothing.

You were the one that claimed you lose massive performance, and that's what we're asking about. The ergonomics are a separate thing from performance

Re: The problem with Go’s default HTTP handlers

#68
post #57

I dislike the example. The author says that because we have a complex function, and the HTTP handler is designed the way it is, we cannot avoid running into problems. I disagree. This are two separate things. You could take a more functional approach and separate the main logic and the side effect. Which is writing the response. func aMoreAdvancedHandler(w http.ResponseWriter, r *http.Request) { res, err := helper()…

I very much prefer this approach as well. Way better than endless chaining of this().that().repeat().million().times().

Re: The problem with Go’s default HTTP handlers

#69
The low level HTTP library is phenomenal for building more friendly packages on top, think of it as assembly language for HTTP in go. What isn't obvious about that Go http design is that it's very parallel. Your callback gets invoked in a goroutine for each request, so you can write easy to understand code, linearly, without worrying about things like promises or callbacks or message queues.

I'm opinionated here, because I also think all API's should be built starting from a spec, such as OpenAPI, and to that end, I've written a library to do just that, generate models and echo handlers from API specs, and other contributors have added gin, chi, etc [1]

1: https://github.com/deepmap/oapi-codegen

Re: The problem with Go’s default HTTP handlers

#70
post #19
post #4

I like the way Axum for Rust handles this. All response handlers return something that implements IntoResponse. Lots of types implement IntoResponse, thus making the simple cases really streamlined. async fn create_user( Json(payload): Json , ) -> impl IntoResponse { let user = User { id: 1337, username: payload.username, }; // this will be converted into a JSON response // with a status code of `201 Created` (Status…

That is what Gin does.

You don't return anything in Gin.
Post reply on HN