OOP 的思想, 无疑是非常实用有效的. 事实是, 无论语言是否直接支持面向对象的编程. 程序员在写代码的时候常常会应用 OOP 的思想.
Go 语言下没有类(Class), 没有构造函数, 没有 this 指针, 没有多态, 只有复合对象(或匿名属性). 复合对象和继承是完全不同的. 在以后的文字中, 继承这个词不再代表一般 OOP 下的继承, 指的是复合对象. 应用 OOP 的思想, WEB 应用下控制器常见形式祖先类型的示意写法(现实中没有太大意义).
this
// 定义基础控制器结构 type BaseController struct { Data interface{} // 应用要维护的数据 Req *http.Request // 请求对象 Res http.ResponseWriter // 响应对象 } // 官方 net/http 包要求实现的接口 func (p *BaseController) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.Req = r // 保存起来供实例使用 p.Res = w if r.Method == "POST" { p.Post() } } // 对应 http POST 方式 func (p *BaseController) Post() { // 继承者必须覆盖这个方法, 否则认为不允许 POST 访问 // BaseController 是不可能知道继承者要做什么, 那就只能返回 403 拒绝访问 p.Res.WriteHeader(403) } // Login 控制器 type Login struct { BaseController // 匿名复合 } // 这里必须覆盖 BaseController.Post, 以实现 Login 的具体行为 func (p *Login) Post() { if p.Req.Form.Get("login_name") == "" { p.Data = "无效的登录名" return } // 这里省略登录成功的过程 p.Data = "登录成功" } // 把这些行为定义成接口 type Controller interface { ServeHTTP(http.ResponseWriter, *http.Request) Post() }
用例
http.Handle("/login", &Login{})
但是这有并发问题
很明显现实中这样的用法是错误的, 因为 WEB 的请求是并发的, 这样写所有并发的请求都由同一个 &Login{} 去处理响应, Req,Res,Data 在并发中都被指向相同的对象. 这是无法正常工作的. 这就是常说的维护上下文, Context.
&Login{}
Req
Res
Data
并发环境每一个请求都要有维护独占数据的能力. 除非没有独占数据要维护.
先重新审视 官方包 server.go 中的代码
type Handler interface { ServeHTTP(ResponseWriter, *Request) } func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) } func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { mux.Handle(pattern, HandlerFunc(handler)) // 进行了转换, 只是为了符合接口要求 } type HandlerFunc func(ResponseWriter, *Request) // 确实没有独占数据要维护 // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
这个http.Handler接口其实只是被当作一个函数使用了. 并发问题留给使用者自己解决. 可以这样做
http.Handler
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { p := &Login{} p.ServeHTTP(w, r) })
每次请求都有新的Login对象产生. 当然这个写法很生硬, 如果有 100 个控制器, 难道还要写 100 个不同的写法?! 可以采用下面的方法.
Login
不用结构体直接使用函数, 所有上下文维护都在函数内部定义成局部变量, 局部变量在函数内部是独占的.
func main() { http.HandleFunc("/login", login) } func login(w http.ResponseWriter, r *http.Request) { // 维护的数据是局部变量 var data interface{} var post = func() { // 用闭包函数访问局部变量 if r.Form.Get("login_name") == "" { data = "无效的登录名" return } data = "登录成功" } post() }
完全就是个函数, 但是并发下, 这完全没有问题. 问题在于如何和其他的模块进行数据沟通, 方法确实有, 比如可以用 gorilla/context. 但是无法想象整个项目都用这种写法.
Go 没有构造函数的概念的. 没关系我们约定一个. 其他语言常用Constructor, 这里选用 New 更符合 Go 风格.
Constructor
New
// 给控制器接口增加一个构造函数 type Controller interface { New() Controller ServeHTTP(w http.ResponseWriter, r *http.Request) Post() } // 扩充 Login , 实现 New 方法 func (p *Login) New() Controller { return &Login{} } // 定义一个 http.Handler 接口, 支持构造函数 type HandlerNew struct { Controller Controller } // http.Handler 接口实现 func (p *HandlerNew) ServeHTTP(w http.ResponseWriter, r *http.Request) { c := p.Constructor.New() c.ServeHTTP(w, r) }
http.Handle("/login", HandlerNew{new(Login)})
反射包 reflect 中的 reflect.Value 有 New 方法, 可以动态的构造出一个新对象. 有些框架就是采用了这种方法, 但是用 Value.New 只是得到一个空属性对象, 要对对象进行初始化依然要约定初始化函数, 这反而比 约定构造函数费事儿. 这里就不具体讨论了.
reflect
reflect.Value
Value.New
笔者在发现 Martini 之前也很困惑到底用什么办法更好, 所以写了早期的 TypePress, 和对应的 Go-Blog-In-Action. Martini 巧妙解决了 WEB 并发中的上下文维护. Martini 发现了 WEB 开发中单个请求响应要维护的上下文有这样的事实:
因此 Martini 采用了这样的方案:
这个方法比 gorilla/context 更高效实用, 虽然都是用 map 保存上下文数据, 差别有
这和 OOP 有何关系? 关系是
golang 不是真正的继承, 这给维护上下文数据造成了问题. Martini 解决了上下文数据维护问题, 应用可以放心的用复合写逻辑代码. 上下文数据交给 Martini 就好.
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8