最近使用go,刚好需要用go搭建一个web服务,由于之前使用过是php ci框架来编写,习惯了php ci的按url路径来映射到具体的控制器,然后调用控制器里面的方法的方式;
这种方式好处是不需要修改一个集中式的控制代码。
PHP CI的路由方式
例如:如果php ci框架代码结构如下图:

其中controllers就是存放控制器,如果控制器中有目录,框架会根据目录层次自动搜索控制器加载:
如:有一个文件是:
application\controllers\cultural\media\v1\Item.php
而里面有一个方法:
public function item_manager_page() {
session_start();
if(!isset($_SESSION['user'])) {
header("location:".HOST_URL."/offical/login_page?show_login=1");
return;
}
$user = $_SESSION['user'];
$data['user'] = $user;
$app = $_SESSION['app'];
$db_config['hostname'] = $app['db_hostname'];
$db_config['username'] = $app['db_username'];
$db_config['password'] = $app['db_password'];
$db_config['database'] = 'mydatabase';
$db_config['dbdriver'] = 'mysqli';
$db_config['port'] = 3306;
$db_config['database'] = $app['db_name'];
$data['app'] = $app;
if(!isset($_REQUEST['pi'])) {
$_REQUEST['pi'] = 1;
}
if(!isset($_REQUEST['pc'])) {
$_REQUEST['pc'] = 10;
}
$pi = $_REQUEST['pi'];
$pc = $_REQUEST['pc'];
$data['pc'] = $pc;
$data['pi'] = $pi;
$this->load->model($app['type'].'/'.$app['subtype'].'/'.$app['vername'].'/'.'items_model','',$db_config);
$items = $this->items_model->page_query(null, $pi, $pc);
$data['items'] = $items;
$this->load->view($app['type'].'/'.$app['subtype'].'/'.$app['vername'].'/admin/item_manager_page',$data);
}
那么通过:
http://www.xxx.com/cultural/media/v1/item/item_manager_page就可以访问这个页面。
beego的方式
首先,beego需要在router.go中注册:
beego.Router("/cultural/media/v1/item/item_manager_page",&controllers.ItemController{},"post:ItemManagerPage")
如果页面比较多,那么每次都要修改这个文件,如果我们希望做一个大型的系统,比如一个模板类型的网站,那么我们需要对控制器按规则存放,如有:培训,电影,编程等,而每个主类下有存放了各种分类别,如果培训下面存放:成人培训,英语培训,课程辅导类。
那么希望controllers的组织形式就是
controllers/train/english
controllers/train/adult
controllers/movie/happy
controllers/movie/sad
等。
当然beego有自动映射的方式,但是不支持深路径,感觉还是不是很方便,所以我稍微改造了下,让beego可以像php ci一样,按照url路径自动从so加载controllers的代码。主要是通过go的plugin加载so,然后在so内部固定一个获取控制器对象的函数
func GetController() interface{}
然后通过go的reflect映射得到它的函数列表,并和url路径中的最后一个路径做匹配,注册到router中即可:
主要代码如下:
type ControllerRegister struct {
routers map[string]*Tree
enablePolicy bool
policies map[string]*Tree
enableFilter bool
so_routers map[string]interface{} //这里添加上我们的自定义map,存放从so获取到的对象
filters [FinishRouter + 1][]*FilterRouter
pool sync.Pool
}
// NewControllerRegister returns a new ControllerRegister.
func NewControllerRegister() *ControllerRegister {
return &ControllerRegister{
routers: make(map[string]*Tree),
policies: make(map[string]*Tree),
pool: sync.Pool{
New: func() interface{} {
return beecontext.NewContext()
},
},
so_routers:make(map[string]interface{}),//添加初始化代码
}
}
修改下面这个函数里面的代码:
func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request)
下面是修改的代码,读者自己去router.go中查找即可,修改如下:(主要就是如果路由没发现,就到so的目录查找):
// User can define RunController and RunMethod in filter
if context.Input.RunController != nil && context.Input.RunMethod != "" {
findRouter = true
runMethod = context.Input.RunMethod
runRouter = context.Input.RunController
} else {
routerInfo, findRouter = p.FindRouter(context)
}
//下面是自己加的查找代码,主要就是查找so,反射函数,注册,然后注册成功后重新查找
if !findRouter {//这里进一步判断是否可以命中我们的so文件
paths := strings.Split(r.URL.Path, "/")
// fmt.Println("could not find path=", r.URL.Path, ", paths=", len(paths))
if len(paths) >= 3 {
key := strings.Join(paths[0:len(paths)-1], "/")
methodName := strings.ToLower(paths[len(paths)-1])
c, ok := p.so_routers[key]
if !ok {//如果不存在,那么看看本地路径是否有
//这里后续改为支持配置so的目录
soPath := "so_controllers" + strings.Join(paths[0:len(paths)-1], "/") + ".so"
so, err := plugin.Open(soPath)
if err == nil {//加载so成功
getFun, err := so.Lookup("GetController")//获取返回对象的挂载函数
if err == nil {//存在
c := getFun.(func() interface{})()//创建控制器
rt := reflect.TypeOf(c)
methodNum := rt.NumMethod()
p.so_routers[key] = c //登记
for i := 0; i < methodNum; i++ {
name := strings.ToLower(rt.Method(i).Name)//全部转小写
if name == methodName {//找到方法,判断方法是否被注册
// 这里还需要加一个判断是否在PublicFunc中注册,否则不安全
p.Add(r.URL.Path, c.(ControllerInterface), r.Method+":"+rt.Method(i).Name)
routerInfo, findRouter = p.FindRouter(context)
break;
}
}
}
}
} else {//已经加载过so,还是没找到路由,那么就在so里面再找下
rt := reflect.TypeOf(c)
methodNum := rt.NumMethod()
for i := 0; i < methodNum; i++ {
name := strings.ToLower(rt.Method(i).Name)//全部转小写
if name == methodName {//找到方法,那么就注册
p.Add(r.URL.Path, c.(ControllerInterface), r.Method+":"+rt.Method(i).Name)
routerInfo, findRouter = p.FindRouter(context)
break;
}
}
}
}
}
这里还有些东西没处理,就是对方法的一些过滤,避免内部controller方法被注册,有些形如:
DelSession
Delete
CheckXSRFCookie
是内部方法,不应该注册,但是因为没有判断,就会本注册进去。
打算通过controller提供一个方法或者field,判断是否可以注册函数到url。
接下来,在so_controllers目录下,可以创建自己的控制器代码switch.go:
package main
import (
"context"
"net"
"strconv"
"time"
"encoding/json"
"git.apache.org/thrift.git/lib/go/thrift"
"github.com/astaxie/beego"
)
type SwitchController struct {
beego.Controller
PublicFun map[string]string //用来判断方法是否可以被route安全注册,还没实现
}
//这个方法必须都有,用来提供给外部创建本对象使用
func GetController() interface{} {
return &SwitchController{}
}
func (this *SwitchController) createThriftClient() (*streamswitch.LiveTypeSwitchClient,*thrift.TFramedTransport) {
tsock, err := thrift.NewTSocketTimeout(net.JoinHostPort(beego.AppConfig.String("thrifthost"), beego.AppConfig.String("thriftport")), time.Duration(100)*time.Millisecond)
if err != nil {
return nil, nil
}
trans := thrift.NewTFramedTransport(tsock)
if err != nil {
return nil, nil
}
protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
iprot := protocolFactory.GetProtocol(trans)
oprot := protocolFactory.GetProtocol(trans)
client := streamswitch.NewLiveTypeThriftClient(thrift.NewTStandardClient(iprot, oprot))
if err := trans.Open(); err != nil {
return nil, nil
}
return client, trans
}
func (this *SwitchController) SetGlobalType() {
var param struct {LiveType int32}
data := this.Ctx.Input.RequestBody
//json数据
err := json.Unmarshal(data, ¶m)
if err != nil {
this.Data["json"] = &struct{
Code int32
Msg string
}{Code:-1, Msg:"error param"}
this.ServeJSON()
return
}
live_type := param.LiveType
//create thrift client
client, trans := this.createThriftClient()
if client == nil || trans == nil {
this.Data["json"] = &struct{
Code int32
Msg string
}{Code:-2, Msg:"create thrift client failed"}
this.ServeJSON()
return
}
defer trans.Close()
//request thrift
ret, err := client.SetGlobalType(context.Background(), live_type)
if err != nil {
beego.Error(err.Error())
this.Data["json"] = &struct{
Code int32
Msg string
}{Code:-5, Msg:"SetGlobalLiveType failed"}
this.ServeJSON()
return
} else if ret != 0 {
this.Data["json"] = &struct{
Code int32
Msg string
}{Code:-6, Msg:"SetGlobalLiveType failed, errcode=" + strconv.Itoa(int(ret))}
this.ServeJSON()
return
}
this.Data["json"] = &struct {Code int32}{0}
this.ServeJSON()
return
}
func (c *SwitchController) Index() {
c.Data["hosturl"] = beego.AppConfig.String("hosturl")
c.Data["indexurl"] = beego.AppConfig.String("indexurl")
c.Data["apiurl"] = beego.AppConfig.String("apiurl")
c.Data["menu"] = "设置"
c.TplName = "index.html"
}
这个例子被我删了一些代码,你知道的,主要就是实现一个SwitchController,它有页面的接口Index,并且再Index中设置了一个变量menu(用来在加载模板后,模板中相应按钮做高亮,这就不说了);有一个cgi接口SetGlobalType
然后我们在so_controllers中编译:go build -buildmode=plugin switch.go,就会生成switch.so了
访问方式就是:
打开页面:http://www.xxxx.com/switch/index
调用cgi:http://www.xxxx.com/switch/setglobaltype
当然这种方式可能存在一些问题,编译出的so文件,可能会占用内存,做管理端应该还没问题,如果是其他的,需要再测试测试看看效果。
后续工作:
1、增加json配置文件,支持启动的时候就加载so注册,同时支持查找路由中动态加。
2、这种方式不足之处在在于对公共资源的访问不太友好,比如要做sql的连接池,每个so都隔离的,就没法做;解决方法:定义的接口函数传入一个公共模块对象类似:
type SwitchController struct {
beego.Controller
PublicFun map[string]string //用来判断方法是否可以被route安全注册,还没实现
Common interface{} //这里的类型还需要考虑下
}
//这个方法必须都有,用来提供给外部创建本对象使用
func GetController(common interface{}) interface{} {
return &SwitchController{Common:common}
}
然后在成员函数中就可以使用这些公共的模块,如sql连接池,redis连接池,配置等。
初学go,有不足之处欢迎指正
网友评论