Coder Social home page Coder Social logo

elton's Introduction

elton

Build Status

Elton的实现参考了koa,统一中间件的形式,方便定制各类中间件,所有中间件的处理方式都非常简单,如果需要交付给下一中间件,则调用Context.Next()。如果当前中间件出错,则返回Error结束调用。如果当前处理函数已正常完成处理,则将响应数据赋值Context.Body = 响应数据,由响应中间件将Body转换为相应的响应数据,如JSON等。

package main

import (
	"log"
	"time"

	"github.com/vicanso/elton"
	errorHandler "github.com/vicanso/elton-error-handler"
	recover "github.com/vicanso/elton-recover"
	responder "github.com/vicanso/elton-responder"
	"github.com/vicanso/hes"
)

func main() {

	d := elton.New()

	// 捕捉panic异常,避免程序崩溃
	d.Use(recover.New())
	// 错误处理,将错误转换为json响应
	d.Use(errorHandler.NewDefault())
	// 请求处理时长
	d.Use(func(c *elton.Context) (err error) {
		started := time.Now()
		err = c.Next()
		log.Printf("response time:%s", time.Since(started))
		return
	})
	// 对响应数据 c.Body 转换为相应的json响应
	d.Use(responder.NewDefault())

	getSession := func(c *elton.Context) error {
		c.Set("account", "tree.xie")
		return c.Next()
	}
	d.GET("/users/me", getSession, func(c *elton.Context) (err error) {
		c.Body = &struct {
			Name string `json:"name"`
			Type string `json:"type"`
		}{
			c.Get("account").(string),
			"vip",
		}
		return
	})

	d.GET("/error", func(c *elton.Context) (err error) {
		// 自定义的error
		err = &hes.Error{
			StatusCode: 400,
			Category:   "custom-error",
			Message:    "error message",
		}
		return
	})

	d.ListenAndServe(":8001")
}

入门说明

常用中间件

  • basic auth HTTP Basic Auth,建议只用于内部管理系统使用
  • body parser 请求数据的解析中间件,支持application/json以及application/x-www-form-urlencoded两种数据类型
  • compress 数据压缩中间件,默认支持gzip、brotli(需要支持编译参数以及编译相应动态库)以及snappy,也可根据需要增加相应的压缩处理
  • concurrent limiter 根据指定参数限制并发请求,可用于订单提交等防止重复提交或限制提交频率的场景
  • error handler 用于将处理函数的Error转换为对应的响应数据,如HTTP响应中的状态码(40x, 50x),对应的出错类别等,建议在实际使用中根据项目自定义的Error对象生成相应的响应数据
  • etag 用于生成HTTP响应数据的ETag
  • fresh 判断HTTP请求是否未修改(Not Modified)
  • json picker 用于从响应的JSON中筛选指定字段
  • logger 生成HTTP请求日志,支持从请求头、响应头中获取相应信息
  • proxy Proxy中间件,可定义请求转发至其它的服务
  • recover 捕获程序的panic异常,避免程序崩溃
  • responder 响应处理中间件,用于将Context.Body(interface{})转换为对应的JSON数据并输出。如果系统使用xml等输出响应数据,可参考此中间件实现interface{}至xml的转换。
  • router-concurrent-limiter 路由并发限制中间件,可以针对路由限制并发请求量。
  • session Session中间件,默认支持保存至redis或内存中,也可自定义相应的存储
  • stats 请求处理的统计中间件,包括处理时长、状态码、响应数据长度、连接数等信息
  • static serve 静态文件处理中间件,默认支持从目录中读取静态文件或实现StaticFile的相关接口,从packr或者数据库(mongodb)等读取文件
  • tracker 可以用于在POST、PUT等提交类的接口中增加跟踪日志,此中间件将输出QueryString,Params以及RequestBody部分,并能将指定的字段做"***"的处理,避免输出敏感信息

Elton

实现HTTP服务的监听、中间件的顺序调用以及路由的选择调用。

创建一个Elton的实例,并初始化相应的http.Server。

d := elton.New()

创建一个Elton的实例,并未初始化相应的http.Server,可根据需要再初始化。

d := elton.NewWithoutServer()
s := &http.Server{
	Handler: d,
}
d.Server = s

Server

http.Server对象,在初始化Elton时,将创建一个默认的Server,可以根据自己的应用场景调整Server的参数配置,如下:

d := elton.New()
d.Server.MaxHeaderBytes = 10 * 1024

Router

httprouter.Router对象,Elton使用httprouter来处理http的路由对应关系,此对象如无必要无需要做调整。

Routers

记录当前Elton实例中所有的路由信息,为[]*RouterInfo,每个路由信息包括Method与Path,此属性只用于统计等场景使用,不需要调整。

// RouterInfo router's info
RouterInfo struct {
  Method string `json:"method,omitempty"`
  Path   string `json:"path,omitempty"`
}

Middlewares

当前Elton实例中的所有中间件处理函数,为[]Handler,如果需要添加中间件,尽量使用Use,不要直接append此属性。此类函数在匹配路由成功后才会调用,如果不匹配的路由则不会调用。

d := elton.New()
d.Use(responder.NewDefault())

PreMiddlewares

当前Elton实例中的前置中间件处理函数,为[]PreHandler,此类函数在匹配路由前调用。

d := elton.New()
d.Pre(func(req *http.Request) {

})

ErrorHandler

自定义的Error处理,若路由处理过程中返回Error,则会触发此调用,非未指定此处理函数,则使用默认的处理。

注意若在处理过程中返回的Error已被处理(如Error Handler),则并不会触发此出错调用,尽量使用中间件将Error转换为相应的输出,如JSON。

d := elton.New()

d.ErrorHandler = func(c *elton.Context, err error) {
  if err != nil {
    log.Printf("未处理异常,url:%s, err:%v", c.Request.RequestURI, err)
  }
  c.Response.WriteHeader(http.StatusInternalServerError)
  c.Response.Write([]byte(err.Error()))
}

d.GET("/ping", func(c *elton.Context) (err error) {
  return hes.New("abcd")
})
d.ListenAndServe(":8001")

NotFoundHandler

未匹配到相应路由时的处理,当无法获取到相应路由时,则会调用此函数(未匹配相应路由时,所有的中间件也不会被调用)。如果有相关统计需要或者自定义的404页面,则可调整此函数,否则可不设置(使用默认)。

d := elton.New()

d.NotFoundHandler = func(resp http.ResponseWriter, req *http.Request) {
  // 要增加统计,方便分析404的处理是被攻击还是接口调用错误
  resp.WriteHeader(http.StatusNotFound)
  resp.Write([]byte("Not found"))
}

d.GET("/ping", func(c *elton.Context) (err error) {
  return hes.New("abcd")
})
d.ListenAndServe(":8001")

GenerateID

ID生成函数,用于每次请求调用时,生成唯一的ID值。

d := elton.New()

d.GenerateID = func() string {
  t := time.Now()
  entropy := rand.New(rand.NewSource(t.UnixNano()))
  return ulid.MustNew(ulid.Timestamp(t), entropy).String()
}

d.Use(responder.NewDefault())

d.GET("/ping", func(c *elton.Context) (err error) {
  log.Println(c.ID)
  c.Body = "pong"
  return
})
d.ListenAndServe(":8001")

EnableTrace

是否启用调用跟踪,设置此参数为true,则会记录每个Handler的调用时长。

d := elton.New()

d.EnableTrace = true
d.OnTrace(func(c *elton.Context, traceInfos []*elton.TraceInfo) {
	log.Println(traceInfos[0])
})

d.Use(responder.NewDefault())

d.GET("/ping", func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})
d.ListenAndServe(":8001")

SignedKeys

用于生成带签名的cookie的密钥,基于keygrip来生成与校验是否合法。

d := elton.New()
d.SignedKeys = new(elton.RWMutexSignedKeys)

SetFunctionName

设置函数名字,主要用于trace中统计时的函数展示,如果需要统计Handler的处理时间,建议指定函数名称,便于统计信息的记录。

// 未设置函数名称
d := elton.New()

d.EnableTrace = true
d.OnTrace(func(c *elton.Context, traceInfos []*elton.TraceInfo) {
	buf, _ := json.Marshal(traceInfos)
	// [{"name":"github.com/vicanso/test/vendor/github.com/vicanso/elton/middleware.NewResponder.func1","duration":10488},{"name":"main.main.func2","duration":1160}]
	log.Println(string(buf))
})

d.Use(responder.NewDefault())

d.GET("/ping", func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})
d.ListenAndServe(":8001")
// 设置responder中间件的名称
d := elton.New()

d.EnableTrace = true
d.OnTrace(func(c *elton.Context, traceInfos elton.TraceInfos) {
	buf, _ := json.Marshal(traceInfos)
	// [{"name":"responder","duration":21755},{"name":"main.main.func2","duration":1750}]
	log.Println(string(buf))
	// elton-0;dur=0.021755;desc="responder",elton-1;dur=0.00175;desc="main.main.func2"
	log.Println(traceInfos.ServerTiming("elton-"))
})
fn := responder.NewDefault()
d.Use(fn)
d.SetFunctionName(fn, "responder")

d.GET("/ping", func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})
d.ListenAndServe(":8001")

ListenAndServe

监听并提供HTTP服务。

d := elton.New()

d.ListenAndServe(":8001")

Serve

提供HTTP服务。

ln, _ := net.Listen("tcp", "127.0.0.1:0")
d := elton.New()
d.Serve(ln)

Close

关闭HTTP服务。

ServeHTTP

http.Handler Interface的实现,在此函数中根据HTTP请求的Method与URL.Path,从Router(httprouter)中选择符合的Handler,若无符合的,则触发404。

Handle

添加Handler的处理函数,配置请求的Method与Path,添加相应的处理函数,Path的相关配置与httprouter一致。

d := elton.New()


d.Use(responder.NewDefault())

noop := func(c *elton.Context) error {
	return c.Next()
}

d.Handle("GET", "/ping", noop, func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})

d.Handle("POST", "/users/:type", func(c *elton.Context) (err error) {
	c.Body = "OK"
	return
})

d.Handle("GET", "/files/*file", func(c *elton.Context) (err error) {
	c.Body = "file content"
	return
})

d.ListenAndServe(":8001")

Elton还支持GET,POST,PUT,PATCH,DELETE,HEAD,TRACE以及OPTIONS的方法,这几个方法与Handle一致,Method则为相对应的处理,下面两个例子的处理是完全相同的。

d.Handle("GET", "/ping", noop, func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})
d.GET("/ping", noop, func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})

ALL

添加8个Method的处理函数,包括GET,POST,PUT,PATCH,DELETE,HEAD,TRACE以及OPTIONS,尽量只根据路由需要,添加相应的Method,不建议直接使用此函数。

d.ALL("/ping", noop, func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})

Use

添加全局中间件处理函数,对于所有路由都需要使用到的中间件,则使用此函数添加,若非所有路由都使用到,可以只添加到相应的Group或者就单独添加至Handler。特别需要注意的是,如session之类需要读取数据库的,如非必要,不要使用全局中间件形式。

d := elton.New()

// 记录HTTP请求的时间、响应码
d.Use(func(c *elton.Context) (err error) {
	startedAt := time.Now()
	req := c.Request
	err = c.Next()
	log.Printf("%s %s %d use %s", req.Method, req.URL.RequestURI(), c.StatusCode, time.Since(startedAt).String())
	return err
})

d.Use(responder.NewDefault())

d.GET("/ping", func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})

d.ListenAndServe(":8001")

Pre

添加全局前置中间件处理函数,对于所有请求都会调用(包括无匹配路由的请求)。

d := elton.New()
// replace url prefix /api
urlPrefix := "/api"
d.Pre(func(req *http.Request) {
	path := req.URL.Path
	if strings.HasPrefix(path, urlPrefix) {
		req.URL.Path = path[len(urlPrefix):]
	}
})

d.GET("/ping", func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})

d.ListenAndServe(":8001")

AddValidator

增加参数校验函数,用于param的校验(在最后一个handler执行时调用)

d := elton.New()
d.AddValidator("id", func(value string) error {
	reg := regexp.MustCompile(`^[0-9]{5}$`)
	if !reg.MatchString(value) {
		return errors.New("id should be numbers")
	}
	return nil
})
d.GET("/:id", func(c *Context) error {
	c.NoContent()
	return nil
})

AddGroup

将group中的所有路由处理添加至Elton。

d := elton.New()
userGroup := NewGroup("/users", func(c *Context) error {
	return c.Next()
})
d.AddGroup(userGroup)

OnError

添加Error的监听函数,如果当任一Handler的处理返回Error,并且其它的Handler并未将此Error处理,则会触发error事件。

d := elton.New()

d.OnError(func(c *elton.Context, err error) {
	// 发送邮件告警等
	log.Println("unhandle error, " + err.Error())
})

d.Use(responder.NewDefault())

d.GET("/ping", func(c *elton.Context) (err error) {
	c.Body = "pong"
	return
})

d.ListenAndServe(":8001")

Context

HTTP处理中的Context,在各Hadnler中传递的实例,它包括了HTTP请求、响应以及路由参数等。

Request

http.Request实例,包含HTTP请求的各相关信息,相关的使用方式可直接查看官方文件。如果Context无提供相应的方法或属性时,才使用此对象。

Response

http.ResponseWriter,用于设置HTTP响应相关状态码、响应头与响应数据,context有各类函数操作此对象,一般无需通过直接操作此对象。

Headers

HTTP响应头,默认初始化为Response的Headers,此http.Header为响应头。

Committed

是否已将响应数据返回(状态码、数据等已写入至Response),除非需要单独处理数据的响应,否则不要设置此属性。

ID

Context ID,如果有设置Elton.GenerateID,则在每次接收到请求,创建Context之后,调用GenerateID生成,一般用于日志或者统计中唯一标识当前请求。

Route

当前对应的路由。

Next

next函数,此函数会在获取请求时自动生成,无需调整。

Params

路由参数对象,它由httprouter的路由参数httprouter.Params转换得来。

RawParams

路由参数对象,httprouter的路由参数httprouter.Params

StatusCode

HTTP响应码,设置HTTP请求处理结果的响应码。

Body

HTTP响应数据,此属性为interface{},因此可以设置不同的数据类型(与koa类似)。注意:设置Body之后,还需要使用中间件responder来将此属性转换为字节,并设置相应的Content-Type,此中间件主要将各类的struct转换为json,对于具体的实现可以查阅代码,或者自己实现相应的responder。

BodyBuffer

HTTP的响应数据缓冲(字节),此数据为真正返回的响应体,一般不需要赋值此参数,而由responder中间件将Body转换为字节(BodyBuffer),并写入相应的Content-Type

RequestBody

HTTP请求体,对于POSTPUT以及PATCH提交数据的请求,此字段用于保存请求体。注意:默认Elton中并未从请求中读取相应的请求体,需要使用body_parser中间件来生成或者自定义相应的中间件。

Reset

重置函数,将Context的属性重置,主要用于sync.Pool中复用提升性能。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
// &{GET /users/me HTTP/1.1 1 1 map[] {} <nil> 0 [] false example.com map[] map[] <nil> map[] 192.0.2.1:1234 /users/me <nil> <nil> <nil> <nil>}
fmt.Println(c.Request)
c.Reset()
// <nil>
fmt.Println(c.Request)

RemoteAddr

获取请求端的IP

fmt.Println(c.RemoteAddr())

RealIP

获取客户端的真实IP,先判断请求头是否有X-Forwarded-For,如果没有再取X-Real-Ip,都没有则从连接IP中取。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
req.Header.Set("X-Forwarded-For", "8.8.8.8")
c := elton.NewContext(resp, req)
// 8.8.8.8
fmt.Println(c.RealIP())

ClientIP

获取客户端真实IP,其获取方式与RealIP类似,但在获取到IP时,先判断是否公网IP,如果非公网IP,则继续获取下一符合条件的IP。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
req.Header.Set("X-Forwarded-For", "127.0.0.1, 8.8.8.8")
c := elton.NewContext(resp, req)
// 8.8.8.8
fmt.Println(c.ClientIP())

Param

获取路由的参数。

// curl 'http://127.0.0.1:8001/users/me'
d.GET("/users/:type", func(c *elton.Context) (err error) {
  t := c.Param("type")
  // me
  fmt.Println(t)
  c.Body = t
  return
})

QueryParam

获取query的参数值,此函数返回的并非字符串数组,只取数组的第一个,如果query中的相同的key的使用,请直接使用Request.URL.Query()来获取。

resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/users/me?type=vip", nil)
c := elton.NewContext(resp, req)
// vip
fmt.Println(c.QueryParam("type"))

Query

获取请求的querystring,此函数返回的query对象为map[string]string,不同于原有的map[string][]string,因为使用相同的key的场景不多,因此增加此函数方便使用。如果有相同的key的场景,请直接使用Request.URL.Query()来获取。

resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/users/me?type=vip", nil)
c := elton.NewContext(resp, req)
// map[type:vip]
fmt.Println(c.Query())

Redirect

重定向当前请求。

d.GET("/redirect", func(c *elton.Context) (err error) {
  c.Redirect(301, "/ping")
  return
})

Set

设置临时保存的值至context,在context的生命周期内有效。

d.Use(func(c *elton.Context) error {
  c.Set("id", rand.Int())
  return c.Next()
})

d.GET("/ping", func(c *elton.Context) (err error) {
  // 6129484611666145821
  fmt.Println(c.Get("id").(int))
  c.Body = "pong"
  return
})

Get

从context中获取保存的值,注意返回的为interface{}类型,需要自己做类型转换。

d.Use(func(c *elton.Context) error {
  c.Set("id", rand.Int())
  return c.Next()
})

d.GET("/ping", func(c *elton.Context) (err error) {
  // 6129484611666145821
  fmt.Println(c.Get("id").(int))
  c.Body = "pong"
  return
})

GetRequestHeader

从HTTP请求头中获取相应的值。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
fmt.Println(c.GetRequestHeader("X-Token"))

Header

返回HTTP响应头。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.SetHeader("X-Response-Id", "abc")
// map[X-Response-Id:[abc]]
fmt.Println(c.Header())

GetHeader

从HTTP响应头中获取相应的值。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.SetHeader("X-Response-Id", "abc")
// abc
fmt.Println(c.GetHeader("X-Response-Id"))

SetHeader

设置HTTP的响应头。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.SetHeader("X-Response-Id", "abc")
// abc
fmt.Println(c.GetHeader("X-Response-Id"))

AddHeader

添加HTTP响应头,用于添加多组相同名字的响应头,如Set-Cookie等。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.AddHeader("X-Response-Id", "abc")
c.AddHeader("X-Response-Id", "def")
// map[X-Response-Id:[abc def]]
fmt.Println(c.Header())

Cookie/SignedCookie

获取HTTP请求头中的cookie。SignedCookie则会根据初始化Elton时配置的Keys来校验cookie是否符合,符合才返回。

req := httptest.NewRequest("GET", "/users/me", nil)
req.AddCookie(&http.Cookie{
  Name:  "jt",
  Value: "abc",
})
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
// jt=abc <nil>
fmt.Println(c.Cookie("jt"))

AddCookie/AddSignedCookie

设置Cookie至HTTP响应头中。AddSignedCookie则根据当前的Cookie以及初化Elton时配置的Keys再生成一个校验cookie(Name为当前Cookie的Name + ".sig")。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.AddCookie(&http.Cookie{
  Name:  "jt",
  Value: "abc",
})
// map[Set-Cookie:[jt=abc]]
fmt.Println(c.Header())

NoContent

设置HTTP请求的响应状态码为204,响应体为空。

d.GET("/no-content", func(c *elton.Context) (err error) {
  c.NoContent()
  return
})

NotModified

设置HTTP请求的响应状态码为304,响应体为空。注意此方法判断是否客户端的缓存数据与服务端的响应数据一致再使用,不建议自己调用此函数,建议使用中间件fresh

d.GET("/not-modified", func(c *elton.Context) (err error) {
  c.NotModified()
  return
})

Created

设置HTTP请求的响应码为201,并设置body。

d.POST("/users", func(c *elton.Context) (err error) {
  c.Created(map[string]string{
    "account": "tree.xie",
  })
  return
})

NoCache

设置HTTP响应头的Cache-Control: no-cache

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.NoCache()
// map[Cache-Control:[no-cache]]
fmt.Println(c.Header())

NoStore

设置HTTP响应头的Cache-Control: no-store

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.NoCache()
// map[Cache-Control:[no-store]]
fmt.Println(c.Header())

CacheMaxAge

设置HTTP响应头的Cache-Control: public, max-age=x

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.CacheMaxAge("1m")
// map[Cache-Control:[public, max-age=60]]
fmt.Println(c.Header())
c.CacheMaxAge("1m", "10s")
// map[Cache-Control:[public, max-age=60, s-maxage=10]]
fmt.Println(c.Header()

SetContentTypeByExt

通过文件(文件后缀)设置Content-Type。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.SetContentTypeByExt("user.json")
// map[Content-Type:[application/json]]
fmt.Println(c.Header())

DisableReuse

禁止context复用,如果context在所有handler执行之后,还需要使用(如设置了超时出错,但无法对正在执行的handler中断,此时context还在使用中),则需要调用此函数禁用context的复用。

req := httptest.NewRequest("GET", "/users/me", nil)
resp := httptest.NewRecorder()
c := elton.NewContext(resp, req)
c.DisableReuse()

Pass

将当前context转至另一个新的server实例处理。

d1 := elton.New()
d2 := elton.New()


d1.GET("/ping", func(c *elton.Context) (err error) {
	c.Pass(d2)
	return
})
d1.ListenAndServe(":8001")

Pipe

将当前Reader pipe向Response,用于流式输出响应数据,节省内存使用。

resp := httptest.NewRecorder()
c := NewContext(resp, nil)
data := []byte("abcd")
r := bytes.NewReader(data)
c.Pipe(r)

IsReaderBody

判断该context的body是否io.Reader,主要用于流式响应处理。

Group

NewGroup

创建一个组,它包括Path的前缀以及组内公共中间件(非全局),适用于创建有相同前置校验条件的路由处理,如用户相关的操作。返回的Group对象包括GETPOSTPUT等方法,与Elton的似,之后可以通过AddGroup将所有路由处理添加至Elton实例。

d := elton.New()
userGroup := elton.NewGroup("/users", noop)
userGroup.GET("/me", func(c *elton.Context) (err error) {
	// 从session中读取用户信息...
	c.Body = "user info"
	return
})
userGroup.POST("/login", func(c *elton.Context) (err error) {
	// 登录验证处理...
	c.Body = "login success"
	return
})
d.AddGroup(userGroup)

elton's People

Contributors

vicanso avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.