英文:
How could I manage the App Engine Go runtime context to avoid App Engine lock-in?
问题
我正在编写一个Go应用程序,运行在App Engine的Go运行时环境中。
我注意到几乎任何使用App Engine服务的操作(如Datastore、Mail,甚至Capabilities)都需要传递一个appengine.Context
实例,该实例必须使用函数appengine.NewContext(req *http.Request) Context
来检索。
虽然我正在为App Engine编写这个应用程序,但我希望能够将它轻松快速地移植到其他平台(可能是不支持任何App Engine API的平台),如果我选择这样做的话。
因此,我通过编写一些小包装器来抽象化与App Engine服务和API的实际交互(包括请求处理函数),以实现对App Engine的特定交互的抽象。采用这种方法,如果我决定迁移到其他平台,我只需重写那些将我的应用程序与App Engine绑定的特定模块。简单明了。
唯一的问题是appengine.Context
对象。我无法将它从我的请求处理程序传递到处理这些API的模块中,而不将我的大部分代码与App Engine绑定在一起。我可以传递http.Request
对象,从中可以派生出appengine.Context
对象,但这将需要耦合可能不应该耦合的东西。(我认为最佳实践是除了专门处理HTTP请求的部分之外,我的应用程序甚至不知道它是一个Web应用程序。)
我首先想到的解决方案是在某个模块中创建一个持久变量。类似这样:
package context
import (
"appengine"
)
var Context appengine.Context
然后,在我的请求处理程序中,我可以使用context.Context = appengine.NewContext(r)
来设置该变量,在直接使用App Engine服务的模块中,我可以通过访问context.Context
来获取上下文。中间的代码都不需要知道appengine.Context
对象的存在。唯一的问题是,“一个给定实例可以同时处理多个请求”,这可能会导致这个计划中的竞争条件和意外行为。(一个请求设置它,另一个请求设置它,第一个请求访问它并获取错误的appengine.Context
对象。)
理论上,我可以将appengine.Context
存储到数据存储中,但这样我就必须将一些特定于请求的标识符传递到逻辑层,以标识哪个数据存储中的appengine.Context
对象是当前请求的对象,这将再次耦合我认为不应该耦合的东西。(而且,它会增加我的应用程序对数据存储的使用。)
我还可以将appengine.Context
对象作为interface{}
类型传递到整个逻辑链中,并使任何不需要appengine.Context
对象的模块忽略它。这样可以避免将我的大部分应用程序与任何特定的东西绑定在一起。然而,这似乎非常混乱。
所以,我有点不知道如何清晰地确保需要appengine.Context
对象的App Engine特定模块可以获取它。希望你们能给我一个我自己还没有想到的解决方案。
提前感谢!
英文:
I'm writing a Go application to run on App Engine's Go runtime.
I notice that pretty much any operation which uses an App Engine service (such as Datastore, Mail, or even Capabilities) requires that you pass it an instance of appengine.Context
which must be retrieved using the function appengine.NewContext(req *http.Request) Context
.
While I am writing this app for App Engine, I want to be able to move it to some other platform (possibly one which doesn't support any of the App Engine API) easily and quickly if I should so choose.
So, I'm abstracting away the actual interaction with App Engine services and API's by writing little wrappers around any App-Engine-specific interaction (including request handling functions). With this approach, if I ever do wish to move to a different platform, I'll just rewrite those specific modules which tie my application to App Engine. Easy and straightforward.
The only problem is that appengine.Context
object. I can't pass it down from my request handlers through my layers of logic to the modules which handle these API's without tying pretty much all of my code to App Engine. I could pass the http.Request
object from which the appengine.Context
object can be derived, but that would require coupling things that probably shouldn't be coupled. (I think it's best practice for none of my application to even know it's a web application except those portions specifically dedicated to handling HTTP requests.)
The first solution that sprang to mind was to just create a persistent variable in some module. Something like this:
package context
import (
"appengine"
)
var Context appengine.Context
Then, in my request handlers, I can set that variable with context.Context = appengine.NewContext(r)
and in the modules that directly use App Engine services, I can fetch the context by accesing context.Context
. None of the intervening code would need to know of the appengine.Context
object's existence. The only problem is that "multiple requests may be handled concurrently by a given instance", which can lead to race conditions and unexpected behavior with this plan. (One request sets it, another sets it, the first one accesses it and gets the wrong appengine.Context
object.)
I could in theory store the appengine.Context
to datastore, but then I'd have to pass some request-specific identifier down the logic layers to the service-specific modules identifying which appengine.Context
object in datastore is the one for the current request, which would again couple things I don't think should be coupled. (And, it would increase my application's datastore usage.)
I could also pass the appengine.Context
object down the whole logic chain with the type interface{}
the whole way and have any module which doesn't need the appengine.Context
object ignore it. That would avoid tying most of my application to anything specific. That also seems highly messy, however.
So, I'm at a bit of a loss how to cleanly ensure the App-Engine-specific modules which need the appengine.Context
object can get it. Hopefully you folks can give me a solution I have yet to think of myself.
Thanks in advance!
答案1
得分: 8
这很棘手,因为你自己设定的作用域规则(这是一个明智的规则)意味着不传递Context
实例,并且没有类似于Java的ThreadLocal
的东西可以通过巧妙的方式实现相同的目的。实际上,这是一件好事。
Context
结合了日志支持(容易)和对appengine服务的调用(不容易)。我认为有十个appengine函数需要一个Context
。我看不到任何干净的解决方案,除了将所有这些函数都包装在你自己的外观后面。
有一件事可以帮助你 - 你可以在应用程序中包含一个配置文件,指示它是否在GAE中,使用某种标志。你的全局布尔值只需要存储这个标志(不是共享的上下文)。当决定是否使用NewContext(r)
来获取访问GAE服务的Context
,或者使用类似的结构来访问你自己的替代服务时,你的外观函数可以查看这个标志。
编辑:最后一句话,当你解决了这个问题后,我可以邀请你分享一下你是如何解决的,甚至可以用一个开源项目来分享吗?问这个有点厚颜无耻,但如果你不问...
英文:
This is tricky because your self-imposed scoping rule (which is a sensible one) means not passing a Context
instance around, and there is nothing similar to Java's ThreadLocal
to achieve the same ends by sneaky means. That's actually a Good Thing, really.
Context
combines logging support (easy) with a Call
to appengine services (not easy). There are I think ten appengine functions that need a Context
. I can't see any clean solution other than wrapping all of these behind your own facade.
There is one thing that can help you - you can include a configuration file with your app that indicates whether it's in GAE or otherwise, using a flag of some sort. Your global boolean need only store this flag (not a shared context). Your facade functions can then consult this flag when deciding whether to use NewContext(r)
to obtain the Context
to access GAE services, or use a lookalike structure to access your own substitute services.
Edit: As a final remark, when you solve this may I invite you to share how you did it, possibly even with an open-source project? Cheeky of me to ask, but if you don't ask...
答案2
得分: 7
我(希望)通过将我的请求处理程序(在这个例子中称为“realHandler”)包装成这样来解决了这个问题:
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ds := NewDataStore(r)
realHandler(w, r, ds)
})
NewDataStore创建了一个DataStore,它是一个简单的包装器,用于抽象GAE数据存储。它有一个未公开的字段,用于存储上下文:
type DataStore struct {
c appengine.Context
}
func NewDataStore(req *http.Request) *DataStore {
return &DataStore{appengine.NewContext(req)}
}
在请求处理程序中,当我需要时,我可以直接访问抽象的数据存储,而不必担心已经设置好的GAE上下文:
func realHandler(w http.ResponseWriter, req *http.Request, db *DataStore) {
var s SomeStruct{}
key, err := db.Add("Structs", &s)
...
}
英文:
I (hopefully) solved this issue by wrapping my request handlers (in this example one called "realHandler") like this:
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ds := NewDataStore(r)
realHandler(w, r, ds)
})
NewDataStore creates a DataStore, which is a simple wrapper that abstracts the GAE datastore. It has an unexposed field where it stores the context:
type DataStore struct {
c appengine.Context
}
func NewDataStore(req *http.Request) *DataStore {
return &DataStore{appengine.NewContext(req)}
}
Within the request handler, I can just access the abstracted datastore when I need it without worrying about the GAE context, which has already been set:
func realHandler(w http.ResponseWriter, req *http.Request, db *DataStore) {
var s SomeStruct{}
key, err := db.Add("Structs", &s)
...
}
答案3
得分: 1
特别是在Datastore的情况下,您应该能够在不同的请求之间重用相同的appengine.Context
。我自己没有尝试过,但这是一种名为goon的Datastore的替代API的工作方式:
> Goon与datastore包在各种方面有所不同:它记住了appengine Context,只需要在创建时指定一次。
顺便说一下,存储应该依赖于HTTP请求听起来很荒谬。我不认为Datastore在通常意义上依赖于特定请求。很可能,它需要识别一个特定的Google App Engine应用程序,这显然在每个请求中都保持不变。我的推测是基于对google.golang.org/appengine
的源代码的快速浏览。
您可以对其他Google App Engine API进行类似的观察。当然,所有细节可能是特定于实现的,我在实际应用中使用这些观察之前会进行更深入的研究。
英文:
Particularly in case of Datastore you should be able to reuse the same appengine.Context
among different requsts. I haven't tried to do it myself but that's the way an alternative API for Datastore called goon works:
> Goon differs from the datastore package in various ways: it remembers the appengine Context, which need only be specified once at creation time
The fact that storage should depend on HTTP request sounds ridiculous by the way. I don't think Datastore depends on a particular request in the usual sense. Most probably, it's needed to identify a particular Google App Engine application which obviously stays the same from request to request. My speculations are based on quick skim over the source code of google.golang.org/appengine
.
You may do similar observations in regard of the other Google App Engine APIs. Of cause all the details could be implementation specific and I'd performed more deep research before actually using those observations in a real application.
答案4
得分: 1
值得注意的是,Go团队引入了golang.org/x/net/context包。
后来,上下文在Managed VM中可用,存储库在这里。文档说明:
此存储库支持App Engine上的Go运行时,包括经典App Engine和Managed VM。它提供了与App Engine服务交互的API。其规范导入路径为
google.golang.org/appengine
。
这意味着您可以轻松地在开发环境之外编写依赖于appengine
的其他包。
特别是,可以很容易地包装像appengine/log
这样的包(一个简单的日志包装器示例)。
但更重要的是,这允许我们以以下形式创建处理程序:
Go博客上有一篇关于context
包的文章在这里。我在这里写了关于使用上下文的文章在这里。如果您决定在处理程序中使用上下文传递,最好在一个地方为所有请求创建上下文。您可以使用非标准的请求路由器(如github.com/orian/wctx)来实现。
英文:
It's worth to note that Go team introduced a golang.org/x/net/context package.
Later on the context was made available in Managed VMs, the repo is here. The documentation states:
> This repository supports the Go runtime on App Engine, including both classic App Engine and Managed VMs. It provides APIs for interacting with App Engine services. Its canonical import path is google.golang.org/appengine
.
What it means is that you could easily write another packages out of dev environment depending on appengine
.
In particular it becomes very easy to wrap around packages like appengine/log
(trivial log wrapper example).
But even more important this allows one to create handlers in a form:
There's an article about a context
package on Go blog here. I wrote about using context here. If you decide to use the handler with context passing around it's good to create context for all requrests in one place. You can do it by using non standard requrest router like github.com/orian/wctx.
答案5
得分: 0
我通过在接口后面包装appengine.NewContext
来处理这个问题,并通过不同的包提供不同的实现。这样,我就不必将GAE链接到任何不使用它的二进制文件中:
type Context interface {
GetHTTPContext(r *http.Request) context.Context
}
我提供了一个方法供子包在导入时注册自己,类似于`database/sql`的风格:
var _context Context
func Register(c Context) {
_context = c // 为了简洁起见,省略了空值检查和双重注册检查
}
我从一个默认实现开始,用于普通的非GAE二进制文件,它只是获取现有的上下文:
var _context Context = &defaultContext{} // 默认使用这个
type defaultContext struct {}
func (d *defaultContext) GetHTTPContext(r *http.Request) context.Context {
return r.Context()
}
然后,我将一个App Engine实现放在`mything/context/appengine`包中:
import(
ctx "mything/context"
)
type aecontext struct {}
func (a *aecontext) GetHTTPContext(r *http.Request) context.Context {
return appengine.NewContext(r)
}
func init() {
ctx.Register(&aecontext{})
}
然后,我的GAE二进制文件可以引入子包,在`init`中注册自己:
import(
_ "mything/context/appengine"
)
我的应用代码使用`GetHTTPContext(r)`来获取适当的上下文,以传递给依赖项。
<details>
<summary>英文:</summary>
I handled this by wrapping `appengine.NewContext` behind an interface, and provide different implementations through different packages. That way, I don't have to link GAE into any binaries where it isn't used:
type Context interface {
GetHTTPContext(r *http.Request) context.Context
}
I provide a method for subpackages to register themselves when imported for side effects, sort of `database/sql`-style:
var _context Context
func Register(c Context) {
_context = c // Nil checking, double registration checking omitted for brevity
}
I start with a default implementation for vanilla, non-GAE binaries, that simply grabs the existing context:
var _context Context = &defaultContext{} // use this by default
type defaultContext struct {}
func (d *defaultContext) GetHTTPContext(r *http.Request) context.Context {
return r.Context()
}
Then I put an App Engine implementation in a package `mything/context/appengine`:
import(
ctx "mything/context"
)
type aecontext struct {}
func (a *aecontext) GetHTTPContext(r *http.Request) context.Context {
return appengine.NewContext(r)
}
func init() {
ctx.Register(&aecontext{})
}
Then my GAE binary can pull in the subpackage, which registers itself in `init`:
import(
_ "mything/context/appengine"
)
And my app code uses `GetHTTPContext(r)` to get an appropriate context to pass into dependencies.
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论