Why is it a good idea to load an record from database in a 'middleware', and put it in the context, and then retrieve it back in the handler as pressly/chi example does?

blov · · 422 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>I am referring to <a href="https://github.com/pressly/chi/blob/master/_examples/rest/main.go#L122-L137">this</a>:</p> <pre><code>func ArticleCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { articleID := chi.URLParam(r, &#34;articleID&#34;) article, err := dbGetArticle(articleID) if err != nil { //... } ctx := context.WithValue(r.Context(), &#34;article&#34;, article) next.ServeHTTP(w, r.WithContext(ctx)) }) } func GetArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value(&#34;article&#34;).(*Article) render.JSON(w, r, article) } </code></pre> <p>We are losing type safety here. If this is about code sharing, why not simply calling a shared function?</p> <hr/>**评论:**<br/><br/>grutoc: <pre><p>Here is another way: <a href="https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39#.9022wvms6" rel="nofollow">https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39#.9022wvms6</a></p></pre>srikanthegdee: <pre><blockquote> <p>We are losing type safety here</p> </blockquote> <p>Actually you dont. Just modify your code like this</p> <pre><code>article, ok := r.Context().Value(&#34;article&#34;).(*Article) if ok == false { //Log and http redirect or somethingelse!!} render.JSON(w, r, article) </code></pre></pre>tv64738: <pre><p>That&#39;s still a runtime error instead of compile time error.</p></pre>epiris: <pre><p>What do you mean by runtime error? The above code will not produce a panic under any circumstance.</p></pre>tv64738: <pre><p>No, but it will &#34;somethingelse&#34; in a way that compile-time checks can prevent.</p></pre>epiris: <pre><p>Your follow up response is much less eloquent (and correct) than saying &#34;I didn&#39;t realize the above code wouldn&#39;t panic.&#34;, but to each their own.</p></pre>tv64738: <pre><p>You seem to think the only runtime errors are panics. Also, great tone, here&#39;s your downvote too.</p></pre>epiris: <pre><p>You replied to a counter argument about losing type safety.</p> <blockquote> <p>That&#39;s still a runtime error instead of compile time error.</p> </blockquote> <p>You seem to find referring to a demonstration of conditional control flow within legal boundaries of the languages type system appropriate. Runtime error has a distinct meaning, reinforced in this context by being directly after referring to a compiler halting error. </p> <p>This is different than errors that occur during runtime, which in Go are just values, not runtime errors. Feel free to be butt hurt and down vote me but it doesn&#39;t change he fact you&#39;re wrong, sorry.</p> <p>We can go further and I can make you show me a example of doing the same thing the op posted that doesn&#39;t fall with your own unique definition of a runtime error. Equality check of if article does not equal nil after fetching it? Which uses... conditional control flow within legal boundaries of the program?</p> <p>I won&#39;t down vote you, have an up vote and a good afternoon, may your butt fully repair.</p></pre>tv64738: <pre><p>That comma-ok there is an indicator of a problem situation, and you have a whole code branch for that wouldn&#39;t exist in a solution that doesn&#39;t use such &#34;middleware&#34;.</p></pre>epiris: <pre><p>I&#39;m sure you&#39;re probably a pretty good engineer and already know that what you said above makes very little sense.</p> <blockquote> <p>That comma-ok there is an indicator of a problem situation</p> </blockquote> <pre><code>recvValue, isChClosed := &lt;-ch mapValue, isKeyPresent := m[mapKey] typValue, isTypeOfGivenT := v.(T) myValue, err := inGoWeAreUseToThis() </code></pre> <blockquote> <p>and you have a whole code branch for that wouldn&#39;t exist in a solution that doesn&#39;t use such &#34;middleware&#34;.</p> </blockquote> <pre><code>article, isArticle := ctx.Value(&#34;article&#34;).(*Article) article, err := fetchArticle(id) </code></pre> <p>Which causes me to circle back around to my original point:</p> <blockquote> <p>Your follow up response is much less eloquent (and correct) than saying &#34;I didn&#39;t realize the above code wouldn&#39;t panic.&#34;, but to each their own.</p> </blockquote> <p>I don&#39;t know why I bother with these sorts of replies, but I guess if even one person learns something it was worth it, we can agree to disagree. Have a good evening man.</p></pre>tv64738: <pre><pre><code>article, err := fetchArticle(id) </code></pre> <p>That bit of code would exist in the straight-forward version. The other line, and its code branch, wouldn&#39;t.</p></pre>dchapes: <pre><p>That&#39;s largely because it doesn&#39;t follow the advice and example given in the <a href="https://golang.org/pkg/context#Context" rel="nofollow"><code>context</code> package documentation</a>:</p> <blockquote> <p>Packages that define a Context key should provide type-safe accessors for the values stored using that key:</p> </blockquote> <p>For the lazy, the example given is:</p> <pre><code>// Package user defines a User type that&#39;s stored in Contexts. package user import &#34;context&#34; // User is the type of value stored in the Contexts. type User struct {...} // key is an unexported type for keys defined in this package. // This prevents collisions with keys defined in other packages. type key int // userKey is the key for user.User values in Contexts. It is // unexported; clients use user.NewContext and user.FromContext // instead of using this key directly. var userKey key = 0 // NewContext returns a new Context that carries value u. func NewContext(ctx context.Context, u *User) context.Context { return context.WithValue(ctx, userKey, u) } // FromContext returns the User value stored in ctx, if any. func FromContext(ctx context.Context) (*User, bool) { u, ok := ctx.Value(userKey).(*User) return u, ok } </code></pre> <p>This is always type safe (type assertions are type safe).</p> <p>Also, since the key passed to <code>context.Value</code> is a non-exported type it can only be set or retrieved via the exported functions which take and return types the compiler can verify for you.</p> <p>Also, note that although the example <code>FromContext</code> returns the boolean from a type assertion that will never fail due to a value of the wrong type. It&#39;s needed because the context may not have any stored value at all. In this specific example it may be better to just <code>return ctx.Value(userKey).(*User)</code> and document that if the value wasn&#39;t set the return will be <code>nil</code>.</p></pre>tv64738: <pre><blockquote> <p>It&#39;s needed because the context may not have any stored value at all.</p> </blockquote> <p>Yes, that&#39;s why there&#39;s a whole new code branch in that scenario, that isn&#39;t present without it.</p></pre>xy8_t_rel_: <pre><blockquote> <p>We are losing type safety here. If this is about code sharing, why not simply calling a shared function?</p> </blockquote> <p>How would you pass data from middleware to the actual controller while staying type safe ? </p></pre>m3wm3wm3wm: <pre><p>The question is not about how to pass data in a request-scope context in a type safe manner.</p> <p>The question is, why using middleware when sharing a code, loading a record from database, between handlers? Why not simply calling a function that loads the record and returns it?</p></pre>pkieltyka: <pre><p>its a trivialized version for a single handler, the pattern is more useful when your have a much larger amount of routes, and different middlewares to handle different logical parts, with the goal of composability across the request path </p></pre>xy8_t_rel_: <pre><blockquote> <p>The question is not about how to pass data in a request-scope context in a type safe manner.</p> </blockquote> <p>Yes it is, imagine you have an authentication system, the authentication is done through the auth middleware, a *User value is created when the user is fully authenticated (inside the middleware). Why should you fetch the user again in your controller when you already fetched the user once ? </p> <p>you are basically saying middleware and controllers shouldn&#39;t share any data. What&#39;s the point of a middleware queue then? Do everything in your controller.</p> <p>My point is, no matter how hard you try to come up with a reusable middleware queue system, you cannot share informations between middlewares and controllers in a typesafe way while sticking to the http.HandlerFunc signature. Why ? because Go doesn&#39;t have generics. Sooner or later you&#39;ll have to do a type assertion. </p></pre>m3wm3wm3wm: <pre><blockquote> <p>you are basically saying middleware and controllers shouldn&#39;t share any data.</p> </blockquote> <p>I am not. My question was, why do they have to share loading the Article object by middleware? Why not simply calling a function?</p> <p>Yes, it can be shared via middleware. But should it? Where to draw the line when it comes to sharing via middleware? The Authentication example makes sense, but the Article loading sounds like to be on the border to me.</p></pre>xy8_t_rel_: <pre><blockquote> <p>But should it? Where to draw the line when it comes to sharing via middleware? </p> </blockquote> <p>it&#39;s just an example to demonstrate the use of context in chi. if you think the example isn&#39;t relevant make a pull request.</p></pre>tv64738: <pre><p>It&#39;s almost as if a lot of &#34;middleware&#34; was just cargo cult programming...</p></pre>machete143: <pre><p>tbh this looks like a bad use for context. Context was initially established (correct me if I&#39;m wrong) to cancel subroutines when the parent was canceled, e.g. due to a timeout. It shouldn&#39;t be a way to pass around data structs.</p></pre>xy8_t_rel_: <pre><blockquote> <p>It shouldn&#39;t be a way to pass around data structs.</p> </blockquote> <p>So why the <code>Context.Value(interface{})interface{}</code> method? why is it here and why is it basically untyped? Can you enlighten us on why is that method here?</p></pre>machete143: <pre><p>Only because there&#39;s a function for something doesn&#39;t mean you should put type assertions everywhere, maybe resort to javascript if you don&#39;t want a type system.</p></pre>xy8_t_rel_: <pre><blockquote> <p>Only because there&#39;s a function for something doesn&#39;t mean you should put type assertions everywhere, maybe resort to javascript if you don&#39;t want a type system.</p> </blockquote> <p>It&#39;s there, are you suggesting people shouldn&#39;t be using it? or that Go designers shoved Javascript in Go ? because they obviously didn&#39;t want &#34;a type system&#34; here if they wrote a method returning an untyped value. You think you&#39;re making fun of me but you&#39;re insulting Go type system itself, labelling it as &#34;Javascript&#34;.</p></pre>thisIsExactly20Chars: <pre><p>It&#39;s a great use for context! Passing big data structs is not a good idea, but passing along data relevant to the context is awesome (user or session id).</p> <blockquote> <p>Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes. Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.</p> </blockquote></pre>karma_vacuum123: <pre><p>why is the alternative &#34;passing big structs&#34;? pointers?</p></pre>nhooyr: <pre><p><a href="/u/pkieltyka" rel="nofollow">/u/pkieltyka</a></p></pre>8bitcow: <pre><p>for standalone apps/services, I think the Context().Value stuff is pretty useful for storing request scoped data.</p> <p>but in a micro-services world, you probably want to pass around the context to other services as well (for example to propagate deadlines). putting structs in there that you only use in one service is not the right thing to do, imho. you&#39;ll end up with clutter in the context that other services cannot handle...</p></pre>Akkifokkusu: <pre><p>I&#39;m curious what you mean by &#34;micro-services&#34;. Contexts aren&#39;t passed between separate applications in any implementation that I know of. Even with GRPC, you have to opt in to passing any &#34;arbitrary&#34; data by using the <code>metadata</code> wrapper, and even that&#39;s limited to strings (or slices of strings). And it&#39;s explicitly serialized on one end and deserialized on the other by the GRPC libraries. GRPC&#39;s context might be similar to Go&#39;s context (and I&#39;m guessing one influenced the design of the other), but they&#39;re definitely distinct things and only one is really relevant in the case of this question, which is about an HTTP server.</p></pre>pkieltyka: <pre><p>It is a pattern that I came to through writing a large REST api with &gt;2000 handlers. It&#39;s worked very well for us - our code is maintainable, we&#39;re productive as a team, the architecture scales as our codebase grows, it works for us. However, if I were to do it all over again I&#39;d probably write it with gRPC :) grpc-web will be coming out soon too (its in active development by Google folks) and it&#39;ll provide a simple bridge between gRPC servers to JS clients in the browser (and even generate the client for you!)</p> <p>going back to the middleware question.. a middleware approach to composing a larger service has been debated many times, and there are certainly pros/cons like in any system design. </p> <p>I named the router &#34;chi&#34; because I think of each request going through a uni-directional flow, to produce a response at any point.</p> <p>The routes and middleware compose a tree of layers that a request passes through until the leaf (handler) is reached. Each layer is easier to reason because you can think of it in isolation from the rest of the flow, you just consider the input state and output state. A layer can be a middleware, an entire subrouter or just an endpoint handler.</p> <p>Type-safety is maintained by asserting the type off the context at runtime, but that doesn&#39;t apply to complete compile-time checking. You could improve this by having a single struct for all context values and have a function to get the app context (for example).</p> <p>The patterns are yours to discover and choose for what works to help deliver your software. There is no single correct answer IMHO.</p> <p>Putting an object on a request-scoped context seems a-okay to me.</p></pre>m3wm3wm3wm: <pre><blockquote> <p>if I were to do it all over again I&#39;d probably write it with gRPC </p> </blockquote> <p>Could you explain, perhaps with a concrete example, how grpc is related to this?</p></pre>pkieltyka: <pre><p>Wrt to making choices for approaches to writing services.</p></pre>piratelax40: <pre><p>what kind of pattern(s) have you found work best for the actual database access functions that you are calling from the handlerFunc</p> <p>eg, at this point I&#39;m creating a struct with a db pointer and creating methods with the handlerFunc signature</p> <pre><code>type ArticleStore struct { db *DB } func (a *ArticleStore) GetArticleByID(id string) (*article, err) { // can now references a.db to access the db } func (a *ArticleStore) ModelCtx(next http.Handler) http.Handler { return http.HanderFunc(func(w http.ResponseWriter, r *http.Request) { articleID := chi.URLParam(r, &#34;articleID&#34;) article, err := a.GetArticleByID(articleID) if err != nil { http.Error(w, http.StatusText(404), 404) return } ctx := context.WithValue(r.Context(), &#34;article&#34;, article) next.ServeHTTP(w, r.WithContext(ctx)) }) } </code></pre> <p>Then in chi</p> <pre><code>r.Use(articleStore.ModelCtx) </code></pre> <p>This &#39;works&#39; but feels like an ungainly way to be able to work with the database, however there doesn&#39;t seem to be a way to pass additional parameters to the handlerFuncs (like a db pointer) to reference inside the function?</p> <p>Would love input from anyone</p></pre>hobbified: <pre><p>Looks like an attempt at chained routing, which is really pretty nice in complex interfaces with nested object access. When you can traverse lots of different paths to get to the same type of object, and then perform the same set of actions regardless of how you got there, it makes sense. But for such a simple example, it looks worse rather than better. And, well, Go isn&#39;t really helping here, with context access being so cumbersome. It works better in other languages, but you&#39;re right, in Go it&#39;s probably best to turn it around and just repeat function calls over and over instead.</p></pre>pafortin: <pre><p>I&#39;d use the context for 1 or 2 things. Use it to store data that was extracted from a token that was sent in the request and also to timeout functions that could run for a very long time and cannot time themselves out for some reason. But to answer your question directly I do not think that putting the result of a DB query was the intent in the first place of the context object.</p></pre>

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

422 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传