手把手教你用Golang封装一款适合自己使用的Web编程框架

  以下文章来源于非正式解决方案 ,作者winlion

  

手把手教你用Golang封装一款适合自己使用的Web编程框架

  非正式解决方案

  思考链接价值,非正式解决方案,既扯高大上如人工智能、大数据,也关注码农日常如分布式、java和golang,每天分享瞎想的东西。

  

手把手教你用Golang封装一款适合自己使用的Web编程框架

  目录结构说明如下

  名称内容model模型层目录,类比Java 中的entityview视图层,存放所有templete模板ctrl控制器层, 存放全部控制器service服务层,类比Java里面的servicehtml一些静态资源页面util核心工具包,Md5加密,返回数据封装等asset静态资源目录,存放js/css/image等args封装全部请求参数对象mnt上传文件的存放目录app.dev.conf开发环境配置文件appd.conf生产环境配置文件start.sh/start.bat启动脚本build.sh/build.bat打包脚本main.go主应用程序文件

  主程序主要做各种初始化工作

  func main() {

  //解析配置文件

  fpath :=flag.String("c","app.dev.conf","config file path")

  flag.Parse()

  _,err:=util.Parse(*fpath)

  if err!=nil{

  fmt.Sprintf("error when %s",err.Error())

  return

  }

  //配置日志

  logmap :=util.GetSec("log")

  service.InitLog(logmap)

  //初始化数据库

  dbmap :=util.GetSec("database")

  service.InitDb(dbmap)

  //注册funcmap

  ctrl.RegisterFuncMap()

  //控制器

  ctrl.RegisterCtrl()

  //静态资源文件

  fileservermap :=util.GetSec("fileserver")

  ctrl.InitFileServer(fileservermap)

  //初始化session

  sessioncfg:=util.GetSec("session")

  util.StartSession(sessioncfg)

  appcfg :=util.GetSec("app")

  //视图控制器

  ctrl.RegisterView(appcfg)

  fmt.Println("http ListenAndServe " + appcfg["addr"])

  //打开服务器监听http

  err=http.ListenAndServe(appcfg["addr"], nil)

  if err!=nil{

  fmt.Println(err.Error())

  log.Println(err.Error())

  }

  }3.1 配置文件解析

  使用配置文件开发包,如

  github/Unknwon/goconfig 包。

  //util/config.go

  var cfg *goconfig.ConfigFile

  var cfgmap map[string]map[string]string=make(map[string]map[string]string)

  var filepath string

  //解析peiz

  func Parse(fpath string)(c map[string]map[string]string ,err error){

  cfg, err :=goconfig.LoadConfigFile(fpath)

  filepath=fpath

  sec :=cfg.GetSectionList()

  for _,v :=range sec{

  cfgmap[v]=make(map[string]string,0)

  keys :=cfg.GetKeyList(v)

  for _,b:=range keys{

  cfgmap[v][b],_=cfg.GetValue(v,b)

  }

  }

  return cfgmap,err

  }

  //全部都存放在存放

  func GetAllCfg()(c map[string]map[string]string){

  return cfgmap

  }

  //重新刷新配置文件

  func ReloadAllCfg()(c map[string]map[string]string){

  return return Parse(filepath)

  }

  调用案列

  util.GetAllCfg()["app"]["port"]3.2 监听配置文件并自动刷新配置

  使用

  github/fsnotify/fsnotify包,装时候注意,一个函数里面如果有参数共享,应该放到一个携程里。

  //监听文件

  func WatchConfig(filepath ...string) {

  //创建一个监控对象

  go func() {

  watch, err :=fsnotify.NewWatcher()

  if err !=nil {

  log.Fatal(err)

  }

  defer watch.Close()

  //添加要监控的对象,文件或文件夹

  for _, fpath :=range filepath {

  err=watch.Add(fpath)

  if err !=nil {

  log.Fatal(err)

  }

  fmt.Println("WatchConfig " + fpath)

  }

  for {

  select {

  case ev :=<-watch.Events:

  {

  if ev.Op&fsnotify.Write==fsnotify.Write {

  //监听到文件系统使用加载新东西

  ReloadAllCfg()

  }

  fmt.Println(ev.Op, ev.Name)

  }

  case err :=<-watch.Errors:

  {

  log.Println("error : ", err)

  return

  }

  }

  }

  }()

  }

  fsnotify 支持很多种事件监听,一般在 Write 事件刷新配置文件

  //判断事件发生的类型,如下5种

  // Create 创建

  // Write 写入

  // Remove 删除

  // Rename 重命名

  // Chmod 修改权限

  if ev.Op&fsnotify.Create==fsnotify.Create {

  log.Println("创建文件 : ", ev.Name);

  }

  if ev.Op&fsnotify.Write==fsnotify.Write {

  log.Println("写入文件 : ", ev.Name);

  }

  if ev.Op&fsnotify.Remove==fsnotify.Remove {

  log.Println("删除文件 : ", ev.Name);

  }

  if ev.Op&fsnotifyame==fsnotifyame {

  log.Println("重命名文件 : ", ev.Name);

  }

  if ev.Op&fsnotify.Chmod==fsnotify.Chmod {

  log.Println("修改权限 : ", ev.Name);

  }3.3 区分系统级配置和用户级配置

  系统级配置参数 假设修改了会影响整个应用,需要另起服务的我们称之为系统级配置,修改了参数,往需要进行相应的操作。如修改了数据库连接地址,需要重置数据库连接操作。修改了应用服务器端口,则需要重启应用服务。

  用户级配置参数 如微信公众号 appsecret,每次调用的时候会从配置中获取,因此只需要重新加载数据即可。

  3.4 配置内容缓存

  需要将配置文件内容缓存到 map 中。 需要考虑到 map 的并发操作。

  4.1 状态变量定义在实体文件内部

  //model/user.go

  //用户性别和角色

  const (

  WOMEN=2

  MAN=1

  Unknow=0

  ROLE_ADMIN=1

  ROLE_USER=0

  )

  type User struct {

  Id int64 `xorm:"pk autoincr BIGINT(20)" form:"id" json:"id"`

  NickName string `xorm:"VARCHAR(40)" form:"nickName" json:"nickName"`

  Openid string `xorm:"VARCHAR(40)" form:"openid" json:"openid"`

  Mobile string `xorm:"VARCHAR(15)" form:"mobile" json:"mobile"`

  Passwd string `xorm:"VARCHAR(40)" form:"passwd" json:"-"`

  Role int `xorm:"int(11)" form:"role" json:"role"`

  Enable int `xorm:"int(11)" form:"enable" json:"enable"`

  Gender int `xorm:"int(11)" form:"gender" json:"gender"`

  }

  在如上代码中,常用角色变量 ROLE_USER和ROLE_ADMIN 定义在同一个文件中,便于阅读。

  4.2 为实体添加 tag

  实体和表结构对应,一定要定义 Form 和 Json tag。这样可以提高系统适用性,为什么呢?因为可以适配前端以各种 Content-Type 提交数据如。后端统一用该实体接收数据即可。

  //urlencode类

  application/x-www-form-urlencoded格式

  mobile=18273252300&passwd=123456

  //json类

  application/x-www-form-urlencoded格式

  {"mobile":"18273252315","passwd":"123456"}4.3 统一 tag 参数命名方式

  约定统一使用驼峰式或者下划线标记。如下,建议使用驼峰式。

  #驼峰

  NickName string `xorm:"VARCHAR(40)" form:"nickName" json:"nickName"`

  #下划线

  NickName string `xorm:"VARCHAR(40)" form:"nick_name" json:"nick_name"`

  如下几点需要注意

  5.1 模板文件和模板名称关联

  关联便于代码管理和阅读。本科证书模板位置 /view/demo/index.html,模板内容如下。

  {{define "demo/index"}}

  

  Hello,Modal

  

  {{end}}

  外部调用方法如下,大家能很自然知道模板文件位置。

  localhost/demo/index5.2 一个函数搞定全部 Html 请求类的页面

  主要是为了程序员生活更美好(早点下班+偷懒)。

  //ctrl/base.go

  func RegisterPage(isDev bool) {

  //初始化一个全局的模板变量

  GlobTemplete :=template.New("root")

  //把一些函数添加进去,这样页面里面就可以使用函数啦

  GlobTemplete.Funcs(GetFuncMap())

  //解析模板 ,demo/index=> 模板

  GlobTemplete, err :=GlobTemplete.ParseGlob("view/**/*")

  for _, templete :=range GlobTemplete.Templates() {

  tplname :=templete.Name()

  patern :="/" + tplname

  fmt.Printf("register templete %s==> %s

  ", patern, tplname)

  //这里就是 /demo/index 这个url 和对应的处理函数之间的关系

  http.HandleFunc(patern, func(w http.ResponseWriter, req *http.Request) {

  fmt.Println(patern + "=>" + tplname)

  if isDev {

  GlobTemplete :=template.New("root")

  GlobTemplete.Funcs(GetFuncMap())

  GlobTemplete, err=GlobTemplete.ParseGlob("view/**/*")

  for _, v :=range GlobTemplete.Templates() {

  if v.Name()==tplname {

  templete=v

  }

  }

  }

  err=templete.ExecuteTemplate(w, tplname, nil)

  if err !=nil {

  fmt.Println(err.Error())

  }

  })

  }

  }

  //在main.go中初始化

  func main(){

  ///

  ctrl.RegisterPage(true)

  //

  }

  外部调用方法如下,大家能很自然知道模板文件位置。

  localhost/demo/index5.3 添加调试模式支持

  为什么要添加调试模式支持?因为调试模式状态下,我们修改来了页面模板,需要立即看到页面内容,而不需要重启应用。核心代码如下,即在调试模式状态下,每次请求都重新解析模板。

  if isDev {

  GlobTemplete :=template.New("root")

  GlobTemplete.Funcs(GetFuncMap())

  GlobTemplete, err=GlobTemplete.ParseGlob("view/**/*")

  for _, v :=range GlobTemplete.Templates() {

  if v.Name()==tplname {

  templete=v

  }

  }

  }

  由上可见,调试模式效率是非常低的,我们不应该在生产环境采用调试模式。

  5.4 添加数据注入

  应用场景是在每个页面中都需要使用 session 中的用户 ID 数据。方法是在 RegisterPage 函数内部模板templete.ExecuteTemplate(w, tplname, nil)处秀修改成如下代码

  //从session中获取用户信息

  user :=loadDataFromSession(req)

  err=templete.ExecuteTemplate(w, tplname, user)

  前端模板调用代码如下

  {{define "demo/index"}}

  

  Hello,Modal ,User id is {{.Id}}

  

  {{end}}

  返回结果

  Hello,Modal ,User id is xxx5.5 在页面使用函数

  在 RegisterPage 方法内定义一个 funMap

  //ctrl/funcmap.go

  var resFuncMap template.FuncMap=make(template.FuncMap)

  func hello (){

  return "hello"

  }

  func hello2 (test string){

  return "hello" + test

  }

  //初始化方法

  func RegisterFuncMap(){

  resFuncMap ["hello"]=hello

  }

  main.go 中初始化

  //在main.go中初始化

  func main(){

  ///

  ctrl.RegisterFuncMap()

  //

  }

  前端模板调用代码如下

  {{define "demo/index"}}

  

  Hello,Modal ,hello func retutn {{hello}}

  Hello,Modal ,hello2 func retutn {{hello2 "参数2"}}

  

  {{end}}

  返回结果

  Hello,Modal ,hello func retutn hello

  Hello,Modal ,hello func retutn hello2参数25.6 多多使用 templete 方法

  主要使用场景是分角色菜单,用户

  {{define "demo/memo"}}

  {{if eq .Role 1}}

  菜单内容1

  {{else if eq .Role 2}}

  菜单内容2

  {{end}}

  

  {{end}}

  其他页面统一调用,进行角色菜单等控制。

  {{define "demo/index"}}

  

  {{templete "demo/menu"}}

  Hello,Modal ,hello func retutn {{hello}}

  Hello,Modal ,hello2 func retutn {{hello2 "参数2"}}

  

  {{end}}

  控制器层主要处理对外接口

  6.1 一个典型的控制器结构**

  import (

  ///很多包

  )

  //定义个一个控制器对象

  type UserCtrl struct {

  }

  //将url和处理函数绑定

  func ( ctrl *UserCtrl)Router(){

  Router("/user/login",ctrl.authwithcode)

  }

  //定义用户处理函数

  var userService service.UserService

  //用户通过小程序登录处理函数,输入code

  //通过util.RespOk 或者util.RespFail输出

  func( ctrl * UserCtrl)authwithcode(w http.ResponseWriter, req *http.Request) {

  var requestdata args.AuthArg

  util.Bind(req,&requestdata)

  cfgminapp :=util.GetSec("miniapp")

  resp,err :=util.Code2Session(cfgminapp["appid"],cfgminapp["secret"],requestdata.Code)

  if err!=nil{

  util.RespFail(w,err.Error())

  return

  }

  requestdata.User.Openid=resp.Openid

  requestdata.User.SessionKey=resp.SessionKey

  u,err:=userService.LoginWithOpenId(requestdata.User)

  if err!=nil{

  util.RespFail(w,err.Error())

  }else{

  util.RespOk(w,model.User{

  Ticket:u.Ticket,

  ClientId:u.ClientId,

  Role:u.Role,

  })

  }

  }6.2 数据和参数绑定

  所有参数都需要可预期在一个结构体里面。这样整个系统编程将变得非常简单。在上 面函数中,通过如下代码实现参数绑定

  var requestdata args.AuthArg

  util.Bind(req,&requestdata)

  其中 args.AuthArg 对象定义如下

  package args

  import "../model"

  type AuthArg struct {

  PageArg

  model.User

  Code string `json:"code" form:"code"`

  Kword string `json:"kword" form:"kword"`

  Passwd string `json:"passwd" form:"passwd"`

  }

  args 作用是存放一切请求参数。每个业务都建议定义一个 arg。每个 arg 都有一个公共属性 PageArg。PageArg 定义如下

  import (

  "fmt"

  "time"

  )

  //常用的搜索,大家可以自行添加

  type PageArg struct {

  Pagefrom int `json:"pagefrom" form:"pagefrom"`

  Pagesize int `json:"pagesize" form:"pagesize"`

  Kword string `json:"kword" form:"kword"`

  Asc string `json:"asc" form:"asc"`

  Desc string `json:"desc" form:"desc"`

  Stat int `json:"stat" form:"stat"`

  Datefrom time.Time `json:"datafrom" form:"datafrom"`

  Dateto time.Time `json:"dateto" form:"dateto"`

  Total int64 `json:"total" form:"total"`

  }

  //获得分页大小

  func (p*PageArg) GetPageSize() int{

  if p.Pagesize==0{

  return 100

  }else{

  return p.Pagesize

  }

  }

  //获得分页当前第几页

  func (p*PageArg) GetPageFrom() int{

  if p.Pagefrom<0{

  return 0

  }else{

  return p.Pagefrom

  }

  }

  //获得排序 ID DESC ,前端传递参数 desc=排序字段 或者asc=排序字段

  func (p*PageArg) GetOrderBy() string{

  if len(p.Asc)>0{

  return fmt.Sprintf(" %s asc",p.Asc)

  } else if len(p.Desc)>0{

  return fmt.Sprintf(" %s desc",p.Desc)

  }else{

  return ""

  }

  }6.3 参数绑定核心函数 util.Bind

  大体结构如下

  func Bind(req *http.Request,obj interface{}) error{

  contentType :=req.Header.Get("Content-Type")

  //如果是简单的json,那么直接用JSON解码

  if strings.Contains(strings.ToLower(contentType),"application/json"){

  return BindJson(req,obj)

  }

  //如果是其他的urlencode那么就用BindForm去处理

  if strings.Contains(strings.ToLower(contentType),"application/x-www-form-urlencoded"){

  return BindForm(req,obj)

  }

  //可以自行扩展xml

  if strings.Contains(strings.ToLower(contentType),"text/xml"){

  return BindXml(req,obj)

  }

  return errors.New("当前方法暂不支持")

  }

  以 BindJson 为例子

  func BindJson(req *http.Request,obj interface{}) error{

  s, err :=ioutil.ReadAll(req.Body) //把 body 内容读入字符串

  if err!=nil{

  return err

  }

  err=json.Unmarshal(s,obj)

  return err

  }

  可能大家更关心 BindForm,篇幅太长,大家可以移步

  github/winlion/restgo-admin6.4 封装返回函数

  一般封装一个底层 JSON,然后根据返回成功或失败响应对应的 code

  /util/resp.go

  package util

  import (

  "net/http"

  "encoding/json"

  "fmt")

  //定义个通用的结构体用于装载返回的数据

  type H struct {

  Code int `json:"code"`

  Rows interface{} `json:"rows,omitempty"`

  Data interface{} `json:"data,omitempty"`

  Msg string `json:"msg,omitempty"`

  Total interface{} `json:"total,omitempty"`

  }

  //返回Json的底层方法

  func RespJson(w http.ResponseWriter,data interface{}){

  header :=w.Header()

  header.Set("Content-Type","application/json;charset=utf-8")

  w.WriteHeader(http.StatusOK)

  ret,err :=json.Marshal(data)

  if err!=nil{

  fmt.Println(err.Error())

  }

  w.Write(ret)

  }

  //当操作成功返回Ok,

  func RespOk(w http.ResponseWriter,data interface{}){

  RespJson(w,H{

  Code:http.StatusOK,

  Data:data,

  })

  }

  //当操作失败返回Error,

  func RespFail(w http.ResponseWriter,msg string){

  RespJson(w,H{

  Code:http.StatusNotFound,

  Msg :msg,

  })

  }7.1 服务层一般结构

  以订单管理为例

  package service

  import (

  "../model"

  "../args"

  "github/go-xorm/xorm"

  "log"

  "github/pkg/errors"

  "encoding/json"

  "time"

  )

  type OrderService struct {

  }

  //构造条件

  func (service *OrderService)buildCond(arg args.PageArg)(*xorm.Session){

  orm :=DBengin.Where("id > ?",0)

  if(!arg.Datefrom.IsZero()){

  orm=orm.And("createat >=?",arg.Datefrom.String())

  }

  if(!arg.Dateto.IsZero()){

  orm=orm.And("createat

  }

  if (arg.Seller>0){

  orm=orm.And("seller=?",arg.Seller)

  }

  if (arg.Buyer>0){

  orm=orm.And("buyer=?",arg.Buyer)

  }

  if (arg.Stat>0){

  orm=orm.And("stat=?",arg.Stat)

  }

  return orm

  }

  //增加

  func (service *OrderService) Create(order model.Order) (model.Order,err){

  _,err=DBengin.InsertOne(&order)

  if err!=nil{

  log.Println(err.Error())

  }

  return order

  }

  //删除

  func (service *OrderService) Delete(order model.Order) (error){

  return nil

  }

  //修改

  func (service *OrderService) Create(order model.Order) (model.Order,err){

  _,err=DBengin.InsertOne(&order)

  if err!=nil{

  log.Println(err.Error())

  }

  return order

  }

  //搜索

  func (service *OrderService) Search(orderArg args.OrderArg) ([]model.Order, error){

  var ret []model.Order=make([]model.Order,0)

  return ret,nil

  }

  //查询某一个

  func (service *OrderService) Create(order model.Order) (model.Order){

  _,err :=DBengin.InsertOne(&order)

  if err!=nil{

  log.Println(err.Error())

  }

  return order

  }

  一般需要构建如下几类函数,具体随业务而定

  名称内容Create添加Update修改Search搜索,返回列表Find返回某一个对象Delete删除buildCond构建条件函数Count符合某一条件的记录数目

  7.2 条件统一管理

  我们可以用类似于如下函数来统一管理查询条件,该函数输出参数,输出一个 session。

  func (service *OrderService)buildCond(arg args.PageArg)(*xorm.Session)

  条件规范化可以让应用更灵活,让业务更清晰。如果不规范,楼主曾经经历的教训可能也会撞上你。

  7.3 连接数据库

  数据库建议使用 xorm。 在 server 包目录下新建 init.go 在其中实现数据库的初始化

  //定义全局变量DBengin

  var DBengin *xorm.Engine

  //定义初始化函数InitDb,dbmap是数据库配置参数,=来自于外部参数

  func InitDb(dbmap map[string]string){

  driverName :=dbmap["driveName"]

  dataSourceName :=dbmap["dataSourceName"]

  showsql :=dbmap["showSql"]!="false"

  maxIdle,_ :=strconv.Atoi(dbmap["maxIdle"])

  maxOpen,_ :=strconv.Atoi(dbmap["maxOpen"])

  sync :=dbmap["sync"]=="true"

  dbengin , err :=xorm.NewEngine(driverName, dataSourceName)

  if err !=nil {

  panic("data source init error==>"+err.Error())

  }

  if sync{

  dbengin.Sync2(new(model.User),

  new(model.Item),

  new(model.Order),

  new(model.User),

  )

  }

  dbengin.ShowSQL(showsql)

  dbengin.SetMaxIdleConns(maxIdle)

  dbengin.SetMaxOpenConns(maxOpen)

  dbengin.SetConnMaxLifetime(5*time.Second)

  DBengin=dbengin

  }

  main.go 中初始化数据库

  func main(){

  //

  dbmap=util.GetSec("database")

  server.InitDb(dbmap)

  //

  }

  具体使用可以参考 Xorm

  func (service *OrderService) Create(order model.Order) (model.Order){

  //就是这么用的

  _,err :=DBengin.InsertOne(&order)

  if err!=nil{

  log.Println(err.Error())

  }

  return order

  }8.1 路由功能怎实现

  在每一个 ctrl 中都定义一个 Router 函数

  func ( ctrl *UserCtrl)Router(){

  Router("/open/register",ctrl.Register)

  Router("/open/authwithpwd",ctrl.authwithpwd)

  Router("/user/find",ctrl.Find)

  Router("/user/quit",ctrl.quit)

  Router("/open/authwithcode",ctrl.authwithcode)

  }

  这些函数调用了 Router 方法,该方法本质上是对 http.HanderFunc 的封装

  //ctrl/base.go

  func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {

  http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {

  fun(w, req)

  })

  }

  定义路由注册函数

  //注册控制器

  func RegisterCtrl() {

  new(UserCtrl).Router()

  new(OpenCtrl).Router()

  new(AttachCtrl).Router()

  }

  注册路由 在 main.go 中完成路由注册

  func main(){

  //

  ctrl.RegisterCtrl()

  //

  }8.2 支持 Post/Get/Any

  解决思路如下 首先在 ctrl/base.go 里面定义一个 map

  PostRouterMap :=make(map[string]HandFunc)

  GetRouterMap :=make(map[string]HandFunc)

  接着定义路由绑定函数

  type Handlefunc func(w http.ResponseWriter,req *http.Request)

  func Post(formate string,handlefunc func(w http.ResponseWriter,req *http.Request)){

  http.HandleFunc(formate,func(w http.ResponseWriter,req *http.Request){

  if reqthod==httpthodPost {

  handlefunc(w,req)

  }else{

  //not sourport 处理

  }

  })

  }

  func Get(formate string,

  handlefunc func(w http.ResponseWriter,req *http.Request)){

  http.HandleFunc(formate,

  func(w http.ResponseWriter,req *http.Request){

  if reqthod==httpthodGet {

  handlefunc(w,req)

  }else{

  //not sourport 处理

  }

  })

  }

  //支持任意方式

  func Any(formate string,

  handlefunc func(w http.ResponseWriter,req *http.Request)){

  http.HandleFunc(formate,

  func(w http.ResponseWriter,req *http.Request){

  handlefunc(w,req)

  })

  }8.3 支持正则

  首先需要定义默认路由。RegisterRegExRouter() 中定义了默认路由 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request)。任何找不到的路由都会进入这个。

  //这一个专门存uri和处理函数之间关系的字典

  var RegExRouterMap map[string]func(w http.ResponseWriter, req *http.Request)=make(map[string]func(w http.ResponseWriter, req *http.Request), 0)

  //这是一个存储Uri和对应正则表达式的字典以后就不要编译啦。

  var RegexpMatchMap map[string]*regexp.Regexp=make(map[string]*regexp.Regexp, 0)

  func RegExRouter(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {

  RegExRouterMap[pantern]=fun

  //形成映射关系

  RegexpMatchMap[pantern],_=regexppile(pantern)

  }

  //没有找到需要一个默认404

  func notfound(w http.ResponseWriter, req *http.Request){

  w.Write([]byte("404 NOT FOUNT"))

  }

  func RegisterRegExRouter(){

  http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {

  uris :=strings.Split(req.RequestURI,"?")

  uri :=uris[0]

  handlefunc :=notfound

  for p,regm :=range RegexpMatchMap{

  if regm.MatchString(uri){

  handlefunc=RegExRouterMap[p]

  break

  }

  }

  handlefunc(w,req)

  })

  }

  在路由注册中初始化

  //注册控制器

  func RegisterCtrl() {

  //new(AttachCtrl).Router()

  RegisterRegExRouter()

  }

  现在我们可以在控制器页面通过 RegExRouter 添加正则路由啦

  //ctrl/user.go

  func (ctrl *UserCtrl) Router() {

  Router("/open/authwithcode", ctrl.authwithcode)

  RegExRouter("/d/.*", ctrl.regtext)

  }

  func (ctrl *UserCtrl) regtext(w http.ResponseWriter, req *http.Request) {

  util.RespOk(w, req.RequestURI)

  }

  客户端请求

  localhost/d/12345678977

  响应数据

  {"code":200,"data":"/d/12345678977"}8.4 404 没找到

  在如上所示中定义了 notfound 函数,当没有任何一个匹配对象时候,进入这个函数。

  //没有找到需要一个默认404

  func notfound(w http.ResponseWriter, req *http.Request){

  w.Write([]byte("404 NOT FOUNT"))

  }8.5 实现拦截器功能

  我们可以在 Router 方法里面实现拦截器功能,主要用来做鉴权,日志记录等

  func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {

  http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {

  //包含某些关键字的不需要鉴权啦

  if strings.Contains(req.RequestURI, "/test/") {

  fun(w, req)

  } else {

  //否则判断一下,如果从xxxplatform平台来的不需要鉴权,直接往下走

  ticket :=req.Header.Get("request-ticket")

  clientid :=req.Header.Get("request-clientid")

  platform :=req.Header.Get("request-platform")

  if platform !="xxxplatform" {

  fun(w, req)

  return

  }

  //否则这要鉴权,通过就直接往下走

  if userService.Authorization(ticket, clientid) {

  fun(w, req)

  } else {

  //没通过返回木有权限。

  util.RespFail(w, "没有权限")

  }

  }

  })

  fmt.Printf("register patern %s==> %s

  ", pantern, pantern)

  }8.6 提升路由性能

  我主要在 Router 函数上下功夫,一种可用的设计是利用携程,如下

  func Router(pantern string, fun func(w http.ResponseWriter, req *http.Request)) {

  http.HandleFunc(pantern, func(w http.ResponseWriter, req *http.Request) {

  //先copy出来

  var bodyBytes []byte

  if c.Request.Body !=nil {

  bodyBytes, _=ioutil.ReadAll(req.Body)

  }

  // 把刚刚读出来的再写进去

  req.Body=ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

  //这样就可以利用携程干事情啦

  go fun(w, req)

  })

  }

  需要注意的是要先把数据 copy 出来,然后才能利用携程,否则 fun 函数里面取出的数据是空的。

  session、日志,可以引用第三方包。 鉴权可以参考拦截器。 安全,防 xss 攻击可以参考拦截器。 

上一篇:Web Api调用遇到错误提示System.Web.HttpException (0x80004005): The controller for path '' was not f


下一篇:nginx反向代理