TE
科技回声
首页24小时热榜最新最佳问答展示工作
GitHubTwitter
首页

科技回声

基于 Next.js 构建的科技新闻平台,提供全球科技新闻和讨论内容。

GitHubTwitter

首页

首页最新最佳问答展示工作

资源链接

HackerNews API原版 HackerNewsNext.js

© 2025 科技回声. 版权所有。

The problem with Go’s default HTTP handlers

192 点作者 p5v将近 3 年前

39 条评论

jamal-kumar将近 3 年前
Well yeah, the default stdlib HTTP handlers are very basic. The cool thing is that the standard library is replete enough to build tons on top of that, though. CORS headers using nothing but stdlib for example:<p><pre><code> origin := http.StripPrefix(&quot;&#x2F;&quot;, http.FileServer(http.Dir(&quot;www&quot;))) wrapped := http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { writer.Header().Set(&quot;Access-Control-Allow-Origin&quot;, &quot;whatever.com&quot;) writer.Header().Set(&quot;Access-Control-Allow-Methods&quot;, &quot;POST, GET, OPTIONS, PUT, DELETE&quot;) writer.Header(). Set(&quot;Access-Control-Allow-Headers&quot;, &quot;Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization&quot;) origin.ServeHTTP(writer, req) }) http.Handle(&quot;&#x2F;&quot;, wrapped) </code></pre> You end up writing a ton of boilerplate like this if you&#x27;re sticking with the standard library, but having a ton of awareness and control over every aspect of what you&#x27;re doing is really just so easy in this language. If I don&#x27;t understand anything in the standard library I just go to the definition of it in the golang source code and usually end up figuring out what I need. It&#x27;s really impressive how much you can get done without using any packages if you&#x27;re writing anything net-facing that just needs to do one thing and do it well.
评论 #32406535 未加载
dralley将近 3 年前
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.<p>async fn create_user( Json(payload): Json&lt;CreateUser&gt;, ) -&gt; impl IntoResponse {<p><pre><code> let user = User { id: 1337, username: payload.username, }; &#x2F;&#x2F; this will be converted into a JSON response &#x2F;&#x2F; with a status code of `201 Created` (StatusCode::CREATED, Json(user))</code></pre> }<p>If the handler is complex enough that branches can&#x27;t all return the same type, you can just add .into_response() to each return site, and now you&#x27;re just returning a response, which also implements IntoResponse (as a no-op).<p>This avoids the problem discussed in this article while also avoiding much of the boilerplate of manually building responses.
评论 #32399893 未加载
评论 #32403211 未加载
评论 #32407068 未加载
评论 #32400349 未加载
评论 #32402385 未加载
vrnvu将近 3 年前
I dislike the example.<p>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.<p>You could take a more functional approach and separate the main logic and the side effect. Which is writing the response.<p><pre><code> func aMoreAdvancedHandler(w http.ResponseWriter, r *http.Request) { res, err := helper() if err { http.Error(w, err.Error(), err.Status()) } w.write(res) } func helper() ([]byte, err error) { &#x2F;&#x2F; todo } </code></pre> Where now the helper function has the problem of dealing with multiple operations that can potentially fail. Which is exactly the problem in the example. Not the HTTP handler itself...<p>For example, we could use the defer function call with a named error to check if something fails and guarantee that we always return an appropriate error. Similar to the pattern used to free resources... I don&#x27;t know how is this commonly called. But again, the problem is the helper function not the handler.<p>I don&#x27;t want to use the FP terminology, probably most people are familiar with this pseudo-code:<p><pre><code> func helper() (res, err) { return foo().bar().baz() } </code></pre> Where the chain foo, bar, baz will continue to be executed if all the calls are success, but early terminate if an error occurs.<p>So, where is the problem?
评论 #32418046 未加载
评论 #32405464 未加载
topicseed将近 3 年前
I agree, I wouldn&#x27;t have been against a required return, but the mitigation is relatively straightforward by using custom handlers like mentioned at the end, if needed.<p>I really like Go&#x27;s default HTTP handlers because it offers that straightforward flexibility. Nothing rocket-sciencey.
评论 #32401568 未加载
评论 #32401095 未加载
评论 #32400904 未加载
donatj将近 3 年前
So the problem with these &quot;good&quot; examples is that they expose the end user to <i>Error</i>s, messages likely not written by you or your team and moreso not intended for general consumption - you need to be really careful about doing so because an unexpected error could expose lots of troubling things like credentials and general information about your system you&#x27;d rather not expose.<p>We&#x27;ve got a general rule of thumb never to expose the message of an Error to the end user.
评论 #32402924 未加载
mrweasel将近 3 年前
It&#x27;s not something I even thought about, but I guess I see the point, I just don&#x27;t agree. In the HTTP handlers it makes sense that you don&#x27;t have return values, because: What would you do with that value exactly?<p>The HTTP handler should always ensure that &quot;something&quot; is returned, at the very least a return code. Once something has been returned to the client, can you really argue that there&#x27;s an error?<p>There&#x27;s a point to the risk of missing return value, you just run the risk of complicating the request flow by adding it. I&#x27;d argue that most people would view the code path as having ended, once the client receives a response. The request shouldn&#x27;t really continue on the server end after that. There can be background processing, go routines or something like that, but that&#x27;s a bit different, as it doesn&#x27;t involve the original requests-response.
评论 #32399748 未加载
评论 #32401215 未加载
jayd16将近 3 年前
Maybe this is a subtlety of Go I&#x27;m not aware of, but at least in most languages, the issue with the explicit return value is that you must first return before you can send the value.<p>By being able to call into the response object, the handler is able to send to the client immediately, and then the handler writer can do other bookkeeping before exiting the handler.<p>With a return value, work would need to be fire off asynchronously before the return value is sent.
badhombres将近 3 年前
To me this reads as a positive to the default handlers, as it shows how easy it was to make it work into a format that works for the programmer&#x27;s preference. I appreciated the solution to the complaint
silentprog将近 3 年前
I was in the same camp as you a few years ago and also wrote an utility to handle this. Might be of any interest to you.<p><a href="https:&#x2F;&#x2F;github.com&#x2F;barthr&#x2F;web-util" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;barthr&#x2F;web-util</a><p>And the blog post explaining it: <a href="https:&#x2F;&#x2F;bartfokker.com&#x2F;posts&#x2F;clean-handlers" rel="nofollow">https:&#x2F;&#x2F;bartfokker.com&#x2F;posts&#x2F;clean-handlers</a>
alectic将近 3 年前
Streamed-response use cases get weird&#x2F;complicated if you have to return the response.
评论 #32401051 未加载
评论 #32400298 未加载
binwiederhier将近 3 年前
I agree that they are awkward. I&#x27;ve solved it similarly by defining a custom handleFunc [1] and a custom errHTTP struct [2] which contains the HTTP response code and even more detailed error codes. It is very nice to work with.<p>Like so:<p><pre><code> if err == util.ErrLimitReached { return errHTTPEntityTooLargeAttachmentTooLarge } </code></pre> Or so:<p><pre><code> if err != nil { return wrapErrHTTP(errHTTPBadRequestActionsInvalid, err.Error()) } </code></pre> [1] <a href="https:&#x2F;&#x2F;github.com&#x2F;binwiederhier&#x2F;ntfy&#x2F;blob&#x2F;main&#x2F;server&#x2F;server.go#L56" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;binwiederhier&#x2F;ntfy&#x2F;blob&#x2F;main&#x2F;server&#x2F;serve...</a><p>[2] <a href="https:&#x2F;&#x2F;github.com&#x2F;binwiederhier&#x2F;ntfy&#x2F;blob&#x2F;main&#x2F;server&#x2F;errors.go" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;binwiederhier&#x2F;ntfy&#x2F;blob&#x2F;main&#x2F;server&#x2F;error...</a>
lhorie将近 3 年前
IMHO this boils down to purity vs side effects. Given a black box function, it&#x27;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.<p>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 &quot;real world&quot;. But it&#x27;s entirely up to you how you manage the data therein. You can continue to procedurally consume these input&#x2F;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.
oppositelock将近 3 年前
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&#x27;t obvious about that Go http design is that it&#x27;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.<p>I&#x27;m opinionated here, because I also think all API&#x27;s should be built starting from a spec, such as OpenAPI, and to that end, I&#x27;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]<p>1: <a href="https:&#x2F;&#x2F;github.com&#x2F;deepmap&#x2F;oapi-codegen" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;deepmap&#x2F;oapi-codegen</a>
halfmatthalfcat将近 3 年前
This isn&#x27;t so much a critique of the HTTP handlers themselves but relying on (and stuffing a ton of shit into) context as a way of dealing with complex request&#x2F;response patterns was a major headache.<p>Trying to implement a circuit breaker against multiple upstreams for a single request in Go was a nightmare.
评论 #32402824 未加载
ainar-g将近 3 年前
Another possible extension of that design is using error types for HTTP codes. Something like:<p><pre><code> type HTTPError struct { Err error Code int } </code></pre> And then, with a couple of wrappers like this:<p><pre><code> func errorNotFound(err error) (wrapped error) { return &amp;HTTPError{ Err: err, Code: http.StatusNotFound, } } </code></pre> you could do something like this:<p><pre><code> return errorNotFound(err) </code></pre> or this:<p><pre><code> return errorInternal(err) &#x2F;&#x2F; 500 Internal Server Error</code></pre>
评论 #32400238 未加载
评论 #32400703 未加载
carom将近 3 年前
I am writing a large Go web application right now and I disagree with this. I do not want to return an error. I want to write an error page or an error JSON message depending on the handler. By the time you are deep in a web application you are copy and pasting all your error blocks anyway. This [1] is a sample of one of my functions. I honestly love Go for how consistent it is.<p>I wrote a rudimentary static checker [2] to make sure that any function does not call both the jsonError() and errorPage() functions. This was quite a problem given how much I reuse code. I disagree with the author&#x27;s take because it is incredibly obvious and quite easy to track down when you forget a return. You will have a big block of garbage in the middle of your page and JSON, and your unique error message should lead you directly to the line.<p>My problem is actually in executing the templates at the end of the functions [1], when you finally write out the page. If that has an error, you can&#x27;t really back track. I&#x27;ve been thinking about changing that to write to a temporary buffer which then writes to the http.ResponseWriter but haven&#x27;t made the change yet.<p>1. <a href="https:&#x2F;&#x2F;gist.github.com&#x2F;TACIXAT&#x2F;24621013248e2d91bebd16c5a02a99f9" rel="nofollow">https:&#x2F;&#x2F;gist.github.com&#x2F;TACIXAT&#x2F;24621013248e2d91bebd16c5a02a...</a><p>2. <a href="https:&#x2F;&#x2F;gist.github.com&#x2F;TACIXAT&#x2F;1000e7d873f0a8a738b3e720b4a7684c" rel="nofollow">https:&#x2F;&#x2F;gist.github.com&#x2F;TACIXAT&#x2F;1000e7d873f0a8a738b3e720b4a7...</a>
tastysandwich将近 3 年前
I can see the problem, but, I don&#x27;t know, it&#x27;s just not something that&#x27;s ever bothered me. How big are his handlers anyway!? They should be pretty small IMO...<p>I&#x27;ve managed to write some pretty complicated REST services with just the standard HTTP library and Mux for routing.<p>In fact I&#x27;ve never used any of the bigger libraries like Gin or Echo.<p>While all of my Node JS colleagues are drowning in dependabot PRs we&#x27;re just cruising by in the Golang team with our small handful of dependencies. Heaven!
Matthias247将近 3 年前
The article points out a „pro“ of returning a response in handlers - but fails to miss any cons.<p>However there are a few of them. One particularly important one is that the „return response“ variants lack extremely on the observability front - which then in the end leads to services which are hard to run in production.<p>Eg let’s say I want to measure in my handler what „last byte latency“ is - or at least when the handler pushed the last byte into a tcp socket. With the Go approach it’s not a problem - we can wrap the handler in another handler which does emit metrics.<p>With the „return response object“ it won’t work easily anymore. At the time the handler returns nothing actually has happened.<p>The same thing applies for eg tracking the amount of concurrently executed requests to understand resource utilization (and starvation). No issue with the Go variant - hard to do with „function as a service“.<p>It’s actually rather unfortunate that all Rust frameworks went down the „response object“ route and made it hard to add proper observability.
Someone将近 3 年前
So, the issue is that there’s an implicit contract that says something like “a handler must write to the <i>ResponseWriter</i> or call <i>http.Error</i> before returning”, but the compiler doesn’t enforce that contract.<p>The proposed improvement makes some ways to accidentally not adhere to the contract more obvious to humans, but still doesn’t enforce the contract.<p>I wonder whether there are languages that allow one to write a library that enforces such contracts at compile time.<p>(One way to do that would be to pass the Request and the ResponseWriter together with a state machine that is hard-coded in the API as a single object to the handler, with the equivalents of <i>w.Write</i> and <i>http.Error</i> declaring their state transitions, and having a language functionality that requires such object to be in an end state of that state machine whenever the function returns)
评论 #32401451 未加载
boredumb将近 3 年前
<p><pre><code> func (api \*App) HandleOnError(w http.ResponseWriter, err error, status int, context string) bool { if err != nil { api.logging.Error(err, context) http.Error(w, err.Error(), http.StatusInternalServerError) return true } return false } </code></pre> and then it&#x27;s used in the handler funcs<p><pre><code> if api.HandleOnError(w, err, http.StatusInternalServerError, &quot;Getting stuff for things&quot;) { return } </code></pre> Makes the logging verbose and handling always properly taken care of, makes the error handling in handlers verbose enough from the caller but abstract enough to not be a majority of the handlers body.
chrsig将近 3 年前
I dont necessarily agree they should return a value, but I do have some issues with the api:<p>- by default, you can&#x27;t tell if a handler has already written to the body or emitted a status code.<p>- by default, you can&#x27;t read the body more than once<p>- r.PostForm vs r.Form ...that they both exist, and that they&#x27;re only available after a manual call to r.ParseForm<p>- the http server accepts an unbounded number of connections. it&#x27;ll just keep making goroutines as they come in. there&#x27;s no way that I know of to apply back pressure.<p>there&#x27;s probably more that I grump about, but those are the ones at the top of my mind. for the most part it works well, and i can work around my gripes.
评论 #32403012 未加载
skybrian将近 3 年前
There&#x27;s a subtle problem in the proposed solution: if an error happens after you already started writing the response, you can&#x27;t change the http status code.<p>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&#x27;s no good way to report an error to the client because the network connection is likely broken.<p>If you&#x27;re not going to keep statistics, dropping errors from writing an http response is reasonable and arguably correct.
throwaddzuzxd将近 3 年前
In my team we used Gin and migrated to Echo (because Gin didn&#x27;t support conflicting routes at the time, like &#x2F;foo&#x2F;{variable} and &#x2F;foo&#x2F;bar) and we got to the same conclusion. Forgetting to return with Gin (and with net&#x2F;http) is an issue that actually occurs, and we&#x27;ve been bitten by it more times than we care to admit.<p>Of course it&#x27;s not worth migrating to Echo just for that but it&#x27;s good to know that some routers implement it differently, if it&#x27;s something that bothers you.
adontz将近 3 年前
To me it seems like a problem induced by the way Go handles errors. With exceptions it would be something like<p><pre><code> try { &#x2F;&#x2F; Some dangerous code which may throw&#x2F;raise w.Write([]byte(&quot;Hello, World!&quot;)) } catch( FileException ) { w.Write([]byte(&quot;Screw your file!&quot;)) } catch( ... ) { w.Write([]byte(&quot;Screw you in general!&quot;)) } </code></pre> and no such problem
评论 #32401933 未加载
评论 #32403330 未加载
crabmusket将近 3 年前
For fairness&#x27;s sake, I&#x27;d like to point out that Nodejs has the same behaviour. Helping a new developer create a web server, we fell into the bugs the author is talking about a few times. I was always quick to spot the problem with the benefit of experience, but a newbie debugging a &quot;response was already sent&quot; error was not the simplest thing in the world.
timvisee将近 3 年前
With Rust you could write a function that consumes the HTTP client, not allowing you use it after, resulting in a compiler error for your example.<p>In Go you could write a function taking the HTTP client reference and setting it to null. This would at least produce a runtime error if it ever comes across the path rather than doing something else that is unexpected.
AtNightWeCode将近 3 年前
Surely one can make the exact same mistake with Echo?<p>if id == &quot;&quot; { fmt.Errorf(&quot;get album: missing id&quot;) }
weltensturm将近 3 年前
Why is it http.Error(w, ...) and not w.Write(http.Error(...))? The latter would be way more clear in showing that nothing magical happens and a return (if wanted) is still necessary (I don&#x27;t write Go, so I don&#x27;t know the conventions)
评论 #32402961 未加载
LordHeini将近 3 年前
Returning errors from a http handler is not a good idea imho.<p>Because http errors&#x2F;responses can be too complex for the limited Golang error objects.<p>You usually want an appropriate error code, a body with a message or set some header.
Gnouc将近 3 年前
You may want to use <a href="https:&#x2F;&#x2F;github.com&#x2F;orijtech&#x2F;httperroryzer" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;orijtech&#x2F;httperroryzer</a> for your CI.
n4jm4将近 3 年前
I&#x27;ve made that same mistake before. And not just in HTTP handlers.<p>There is a case to be made for a linter rule that warns when a conditional evaluates an error value, without presenting a return path.
didip将近 3 年前
This is such a tiny problem to quibble about. Just create a helper function that indeed force you to return values.<p>The default leaves you room to do harder things like custom chunk streaming.
评论 #32400723 未加载
tomohawk将近 3 年前
Errors important to the server should not be conflated with http responses to the client. There is sometimes a correlation between the two, but often not.
m00x将近 3 年前
Editorialized title. The real title is: &quot;I Don’t Like Go’s Default HTTP Handlers&quot;.<p>Please edit the HN title to reflect the blog post.
alingse将近 3 年前
we have already do this in prod.<p>type HandlerFuncErr func(w http.ResponseWriter, r <i>http.Request) error<p>func HandleErr(h HandlerFuncErr) http.HandlerFunc { return func(w http.ResponseWriter, r </i>http.Request) { ... } }
blablabla123将近 3 年前
Seems a rather theoretical problem to me because usually an external library is used. Also in the underlying http response first the return code is written before the body. This matters a lot when for instance copying a buffer into the response
asp_hornet将近 3 年前
I thought a “naked return” was when the return types were named?
zxcvbn4038将近 3 年前
Who cares? Rando with a blog, maybe he&#x27;ll like nodejs better.
JustSomeNobody将近 3 年前
They&#x27;re not functions, they&#x27;re procedures. Procedures don&#x27;t return values. Write your code accordingly.