Git Product home page Git Product logo

gear's Introduction

Gear

CI Codecov CodeQL Go Reference License

A lightweight, composable and high performance web service framework for Go.

Features

  • Effective and flexible middlewares flow control, create anything by middleware
  • Powerful and smart HTTP error handling
  • Trie base gear.Router, as faster as HttpRouter, support regexp parameters and group routes
  • Integrated timeout context.Context
  • Integrated response content compress
  • Integrated structured logging middleware
  • Integrated request body parser
  • Integrated signed cookies
  • Integrated JSON, JSONP, XML and HTML renderer
  • Integrated CORS, Secure, Favicon and Static middlewares
  • More useful methods on gear.Context to manipulate HTTP Request/Response
  • Run HTTP and gRPC on the same port
  • Completely HTTP/2.0 supported

Documentation

Go-Documentation

Import

// package gear
import "github.com/teambition/gear"

Design

  1. Server 底层基于原生 net/http 而不是 fasthttp
  2. 通过 gear.Middleware 中间件模式扩展功能模块
  3. 中间件的单向顺序流程控制和级联流程控制
  4. 功能强大,完美集成 context.Context 的 gear.Context
  5. 集中、智能、可自定义的错误和异常处理
  6. After Hook 和 End Hook 的后置处理
  7. Any interface 无限的 gear.Context 状态扩展能力
  8. 请求数据的解析和验证

FAQ

  1. 如何从源码自动生成 Swagger v2 的文档?
  2. Go 语言完整的应用项目结构最佳实践是怎样的?

Demo

Hello

https://github.com/teambition/gear/tree/master/example/hello

  app := gear.New()

  // Add logging middleware
  app.UseHandler(logging.Default(true))

  // Add router middleware
  router := gear.NewRouter()

  // try: http://127.0.0.1:3000/hello
  router.Get("/hello", func(ctx *gear.Context) error {
    return ctx.HTML(200, "<h1>Hello, Gear!</h1>")
  })

  // try: http://127.0.0.1:3000/test?query=hello
  router.Otherwise(func(ctx *gear.Context) error {
    return ctx.JSON(200, map[string]any{
      "Host":    ctx.Host,
      "Method":  ctx.Method,
      "Path":    ctx.Path,
      "URI":     ctx.Req.RequestURI,
      "Headers": ctx.Req.Header,
    })
  })
  app.UseHandler(router)
  app.Error(app.Listen(":3000"))

HTTP2 with Push

https://github.com/teambition/gear/tree/master/example/http2

package main

import (
  "net/http"

  "github.com/teambition/gear"
  "github.com/teambition/gear/logging"
  "github.com/teambition/gear/middleware/favicon"
)

// go run example/http2/app.go
// Visit: https://127.0.0.1:3000/
func main() {
  const htmlBody = `
<!DOCTYPE html>
<html>
  <head>
    <link href="/hello.css" rel="stylesheet" type="text/css">
  </head>
  <body>
    <h1>Hello, Gear!</h1>
  </body>
</html>`

  const pushBody = `
h1 {
  color: red;
}
`

  app := gear.New()

  app.UseHandler(logging.Default(true))
  app.Use(favicon.New("./testdata/favicon.ico"))

  router := gear.NewRouter()
  router.Get("/", func(ctx *gear.Context) error {
    ctx.Res.Push("/hello.css", &http.PushOptions{Method: "GET"})
    return ctx.HTML(200, htmlBody)
  })
  router.Get("/hello.css", func(ctx *gear.Context) error {
    ctx.Type("text/css")
    return ctx.End(200, []byte(pushBody))
  })
  app.UseHandler(router)
  app.Error(app.ListenTLS(":3000", "./testdata/out/test.crt", "./testdata/out/test.key"))
}

A CMD tool: static server

https://github.com/teambition/gear/tree/master/example/staticgo

Install it with go:

go install github.com/teambition/gear/example/staticgo@latest

It is a useful CMD tool that serve your local files as web server (support TLS). You can build osx, linux, windows version with make build.

package main

import (
  "flag"

  "github.com/teambition/gear"
  "github.com/teambition/gear/logging"
  "github.com/teambition/gear/middleware/cors"
  "github.com/teambition/gear/middleware/static"
)

var (
  address  = flag.String("addr", "127.0.0.1:3000", `address to listen on.`)
  path     = flag.String("path", "./", `static files path to serve.`)
  certFile = flag.String("certFile", "", `certFile path, used to create TLS static server.`)
  keyFile  = flag.String("keyFile", "", `keyFile path, used to create TLS static server.`)
)

func main() {
  flag.Parse()
  app := gear.New()

  app.UseHandler(logging.Default(true))
  app.Use(cors.New())
  app.Use(static.New(static.Options{Root: *path}))

  logging.Println("staticgo v1.2.0, created by https://github.com/teambition/gear")
  logging.Printf("listen: %s, serve: %s\n", *address, *path)

  if *certFile != "" && *keyFile != "" {
    app.Error(app.ListenTLS(*address, *certFile, *keyFile))
  } else {
    app.Error(app.Listen(*address))
  }
}

A CMD tool: reverse proxy server

https://github.com/teambition/gear/tree/master/example/gearproxy

Install it with go:

go install github.com/teambition/gear/example/gearproxy@latest
gearproxy -help

HTTP2 & gRPC

https://github.com/teambition/gear/tree/master/example/grpc_server

https://github.com/teambition/gear/tree/master/example/grpc_client

About Router

gear.Router is a trie base HTTP request handler. Features:

  1. Support named parameter
  2. Support regexp
  3. Support suffix matching
  4. Support multi-router
  5. Support router layer middlewares
  6. Support fixed path automatic redirection
  7. Support trailing slash automatic redirection
  8. Automatic handle 405 Method Not Allowed
  9. Automatic handle OPTIONS method
  10. Best Performance

The registered path, against which the router matches incoming requests, can contain six types of parameters:

Syntax Description
:name named parameter
:name(regexp) named with regexp parameter
:name+suffix named parameter with suffix matching
:name(regexp)+suffix named with regexp parameter and suffix matching
:name* named with catch-all parameter
::name not named parameter, it is literal :name

Named parameters are dynamic path segments. They match anything until the next '/' or the path end:

Defined: /api/:type/:ID

/api/user/123             matched: type="user", ID="123"
/api/user                 no match
/api/user/123/comments    no match

Named with regexp parameters match anything using regexp until the next '/' or the path end:

Defined: /api/:type/:ID(^\d+$)

/api/user/123             matched: type="user", ID="123"
/api/user                 no match
/api/user/abc             no match
/api/user/123/comments    no match

Named parameters with suffix, such as Google API Design:

Defined: /api/:resource/:ID+:undelete

/api/file/123                     no match
/api/file/123:undelete            matched: resource="file", ID="123"
/api/file/123:undelete/comments   no match

Named with regexp parameters and suffix:

Defined: /api/:resource/:ID(^\d+$)+:cancel

/api/task/123                   no match
/api/task/123:cancel            matched: resource="task", ID="123"
/api/task/abc:cancel            no match

Named with catch-all parameters match anything until the path end, including the directory index (the '/' before the catch-all). Since they match anything until the end, catch-all parameters must always be the final path element.

Defined: /files/:filepath*

/files                           no match
/files/LICENSE                   matched: filepath="LICENSE"
/files/templates/article.html    matched: filepath="templates/article.html"

The value of parameters is saved on the Matched.Params. Retrieve the value of a parameter by name:

type := matched.Params("type")
id   := matched.Params("ID")

More Middlewares

License

Gear is licensed under the MIT license. Copyright © 2016-2023 Teambition.

gear's People

Contributors

cssivision avatar davaddi avatar davidcai1111 avatar dependabot[bot] avatar dyxushuai avatar evolsnow avatar isayme avatar lyrictian avatar zensh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gear's Issues

logging panic: Header called after Handler finished

运行场景:

app := gear.New()
app.UseHandler(logging.Default())

崩溃分析:

Response.WriteHeader()时,异步执行end hook,并结束请求:

// execute "end hooks" with LIFO order after Response.WriteHeader.
// they run in a goroutine, in order to not block current process.
if len(r.endHooks) > 0 {
	go runHooks(r.endHooks)
}

而logging 在运行时需打印Header相关信息,故有几率出现Header called after Handler finished

func (l *Logger) Serve(ctx *gear.Context) error {
	log := l.FromCtx(ctx)
	// Add a "end hook" to flush logs
	ctx.OnEnd(func() {
		// Ignore empty log
		if len(log) == 0 {
			return
		}
		log["Status"] = ctx.Res.Status()
		log["Type"] = ctx.Res.Type()
		log["Length"] = ctx.Res.Get(gear.HeaderContentLength)
		l.consume(log, ctx)
	})
	return nil
}

http2下会panic

现象:
panic: Header called after Handler finished
goroutine 214 [running]:
net/http.(*http2responseWriter).Header(0xc00025e0d8, 0x0)
/usr/local/go/src/net/http/h2_bundle.go:6310 +0x99
github.com/teambition/gear.(*Response).Header(0xc000262c00, 0xb62267)
/go/pkg/mod/github.com/teambition/[email protected]/response.go:85 +0x34
github.com/teambition/gear.(*Response).Get(0xc000262c00, 0xb62267, 0xc, 0x0, 0x0)
/go/pkg/mod/github.com/teambition/[email protected]/response.go:28 +0x2b
github.com/teambition/gear/logging.New.func2(0xc00024d890, 0xc0002f4630)
/go/pkg/mod/github.com/teambition/[email protected]/logging/logger.go:236 +0x690
github.com/teambition/gear/logging.(*Logger).Serve.func1()
/go/pkg/mod/github.com/teambition/[email protected]/logging/logger.go:533 +0x178
github.com/teambition/gear.runHooks(0xc00025e0e0, 0x1, 0x1)
/go/pkg/mod/github.com/teambition/[email protected]/response.go:186 +0x39
created by github.com/teambition/gear.(*App).ServeHTTP.func1
/go/pkg/mod/github.com/teambition/[email protected]/app.go:439 +0x179

原因:
https://github.com/teambition/gear/blob/v1.15.0/logging/logger.go#L236 会从Res中读取Header值:

ctx.Res.Get(gear.HeaderXRequestID);

而Res是nil:
https://github.com/golang/net/blob/ed066c81e75eba56dd9bd2139ade88125b855585/http2/server.go#L2525

Http2在处理完请求后在 handlerDone() 中会把Res置为nil:
https://github.com/golang/net/blob/ed066c81e75eba56dd9bd2139ade88125b855585/http2/server.go#L2629

日志输出是在 OnEnd 中调用res,(OnEnd挂在end hook上,被新的goroutine执行),所以panic了。
https://github.com/teambition/gear/blob/v1.15.0/logging/logger.go#L526

关于 SetOnError 的回调方法

目前对于 SetOnError 的回调要求格式是func(ctx *Context, err HTTPError).

当前的预期(个人理解)是: 使用者在回调方法里处理err并调用 ctx.End 之类的方法.

存在的问题有:

  1. errParseError 处理后的对象, 使用者无法知道原始错误信息;
  2. 无法使用 ctx.Error 响应数据, 会存在死循环;

解决办法

将回调的格式改为 func(ctx *Context, err error) error:
当返回新的 error 时, gear 使用新的 error 交给 ParseError 处理, 否则还是使用原err;

这个方法并没有解决回调里调用ctx.Error导致的死循环问题.

带来的好处是: 用户可以拿到原始 error 并处理, 然后返回给 gear 继续处理;

当然, 这个方法是存在兼容问题的! 解决办法是: 回调函数支持两种方式
对于旧的方式, 再包装成新的格式.

func (ctx *Context, err error) error {
    fn(ctx, ParseError(err, ctx.Res.status()))
    return nil // 返回 nil 表示已经自行处理过
}

如果使用旧的方式, 打印一个 deprecated warning 信息.

如何注册 pprof 的路由?

您好,我正在使用您的 gear框架开发一个web应用,最近想要通过 pprof 分析应用性能方面的问题,按照 pprof 文档的指导 https://golang.org/pkg/net/http/pprof/

自定义一个 pprof 的 controller,部分代码如下:

package ctl

type Pprof struct {
	...
}

// Init ...
func (p *Pprof) Get(ctx *gear.Context) error {
	switch ctx.Param("pp") {
	default:
		pprof.Index(ctx.Res.w, ctx.Req)
	case "":
		pprof.Index(ctx.Res.w, ctx.Req)
	case "cmdline":
		pprof.Cmdline(ctx.Res.w, ctx.Req)
	case "profile":
		pprof.Profile(ctx.Res.w, ctx.Req)
	case "symbol":
		pprof.Symbol(ctx.Res.w, ctx.Req)
	}
	ctx.Res.WriteHeader(200)
}

但是查下源码发现 gear.Contexthttp.ResponseWriter不能导出,所以上面代码编译不通过。

type Response struct {
	status      int    // response Status Code
	body        []byte // the response content.
	afterHooks  []func()
	endHooks    []func()
	ended       atomicBool // indicate that app middlewares run out.
	wroteHeader atomicBool
	w           http.ResponseWriter // the origin http.ResponseWriter, should not be override.
	rw          http.ResponseWriter // maybe a http.ResponseWriter wrapper
}

请问这种情况有办法处理吗?

[feature request] expose app.Serve(lis)

gear HTTP2 & gRPC not working for http server without TLS(or maybe the new gRPC lib related issue).

We use cmux as port proxy, so we need using app.Server.Serve(lis) to work

lis, err := net.Listen("tcp", ":8080")
if err != nil {
	log.Fatal(err)
}

// Create a cmux.
m := cmux.New(l)

// Match connections in order:
// First grpc, then HTTP, and otherwise Go RPC/TCP.
grpcL := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"))
_ = grpcL
httpL := m.Match(cmux.HTTP1Fast())

app := gear.New()

// etc.

// but we can not inject logger because is not exposable.
app.Server.Handler = app
app.Server.Serve(httpL)

Can we expose app.Serve(lis) func?

route with Options{TrailingSlashRedirect :false} return 421 not 404

package main

import (
"github.com/teambition/gear"
 "github.com/teambition/gear/logging"

)
func main(){
  app := gear.New()
  app.UseHandler(logging.Default(true))
  router := gear.NewRouter(gear.RouterOptions{TrailingSlashRedirect :false})
  router.Get("/hello/:name", func(ctx *gear.Context) error {
    return ctx.HTML(200, "<h1>Hello, Gear!</h1>")
  })
  app.UseHandler(router)
  app.Error(app.Listen(":3000"))
  }
  

$curl curl http://localhost:3000/hello/gear/
127.0.0.1 - - [2021-09-10T10:35:52.391Z] "GET /hello/gear/ HTTP/1.1" 421 119 0.000ms
{"error":"MisdirectedRequest","message":"The request was directed at a server that is not able to produce a response."}

but in net/http

package main
import (
    "fmt"
    "net/http"
)
func main() {
    http.HandleFunc("/hello/gear", func (w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Welcome to my website!")
    })
   http.ListenAndServe(":3000", nil)
}

$curl curl http://localhost:3000/hello/gear/
404 not found

如何处理 POST 请求的 压缩数据?

现在客户端 POST 请求上来的数据是一些监控数据,因为数据量大所以采用了 zlib 压缩,如何在 gear 这边自动检测 header 并解压?

我看了 gear 关于中间件部分的文档和代码,想通过 middleware 的方式来实现,

package middleware

import (
	"compress/zlib"

	"github.com/teambition/gear"
)

// Decompress ...
func Decompress() gear.Middleware {
	return func(ctx *gear.Context) error {
		if ctx.GetHeader(gear.HeaderContentEncoding) == "deflate" {
			var err error
			ctx.Req.Body, err = zlib.NewReader(ctx.Req.Body)

			if err != nil {
				return err
			}
		}

		return nil
	}
}

app.go 注册这个中间件

app.Use(middleware.Decompress())

但是这样做好像有问题,日志报错413的错误

error:Error{Code:413, Err:\"RequestEntityTooLarge\", Msg:\"unexpected EOF\", Data:\u003cnil\u003e, Stack:\"\"}

希望您有空的时候帮忙看看我这个需求应该如何实现比较好。

支持该框架

找一个go web框架来学习,发现gear源码写的很有条理,很规范,支持支持

undefined: http.ErrAbortHandler

ritsus-iMac:fingerprint zhaoyili$ go get github.com/teambition/gear
# github.com/teambition/gear src/github.com/teambition/gear/app.go:343: undefined: http.ErrAbortHandler src/github.com/teambition/gear/app.go:386: app.Server.Shutdown undefined (type *http.Server has no field or method Shutdown) src/github.com/teambition/gear/app.go:388: app.Server.Close undefined (type *http.Server has no field or method Close) src/github.com/teambition/gear/response.go:169: undefined: http.PushOptions src/github.com/teambition/gear/response.go:170: undefined: http.Pusher src/github.com/teambition/gear/util.go:354: undefined: url.PathEscape

app.Server.serve(listener) not work

app := gear.New()
app.Use(func(ctx *gear.Context) error {
	return ctx.HTML(200, "Hello World")
})
listener, _ := net.Listen("tcp", ":9090")
app.Server.Serve(listener)

It won't response Hello World for any request.
I think add app.Server.Handler = app in New() may help. So is app.Server.ErrorLog = app.logger.

multi router example not work

func main() {
	// Create app
	app := gear.New()

	// Create views router
	viewRouter := gear.NewRouter()
	viewRouter.Get("/", func(ctx *gear.Context) error {
		return nil
	})
	// add more ...

	apiRouter := gear.NewRouter(gear.RouterOptions{
		Root:                  "/api",
		IgnoreCase:            true,
		FixedPathRedirect:     true,
		TrailingSlashRedirect: true,
	})
	// support one more middleware
	apiRouter.Get("/user/:id", func(ctx *gear.Context) error {
		return nil
	})
	// add more ..

	app.UseHandler(apiRouter) // Must add apiRouter first.
	app.UseHandler(viewRouter)
	// Start app at 3000
	app.Listen(":3000")
}

The viewRouter works, but apiRouter NOT.

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.