(原) Chi快速入门

原创文章,请后转载,并注明出处。

Chi是一个轻量的Web框架。Github,网文介绍可以看这里.

package main

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {
    r := chi.NewRouter()
    r.Use(middleware.Logger)
    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World!"))
    })
    http.ListenAndServe(":3000", r)
}

看起来还是挺简单的。

路由

Connect(pattern string, h http.HandlerFunc)
Delete(pattern string, h http.HandlerFunc)
Get(pattern string, h http.HandlerFunc)
Head(pattern string, h http.HandlerFunc)
Options(pattern string, h http.HandlerFunc)
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)

一个都不能少。

r.Put("/path", myHandler)
/users/{userID}  chi.URLParam(r, "userID")
/admin/*   chi.URLParam(r, "*")
r.Get("/articles/{rid:^[0-9]{5,6}}", getArticle)
r := chi.NewRouter()

r.Get("/articles/{date}-{slug}", getArticle)

func getArticle(w http.ResponseWriter, r *http.Request) {
  dateParam := chi.URLParam(r, "date")
  slugParam := chi.URLParam(r, "slug")
  article, err := database.GetArticle(date, slug)

  if err != nil {
    w.WriteHeader(422)
    w.Write([]byte(fmt.Sprintf("error fetching article %s-%s: %v", dateParam, slugParam, err)))
    return
  }
  
  if article == nil {
    w.WriteHeader(404)
    w.Write([]byte("article not found"))
    return
  }
  w.Write([]byte(article.Text()))
})

自定义404和405

r.NotFound(func(w http.ResponseWriter, r *http.Request) {
  w.WriteHeader(404)
  w.Write([]byte("route does not exist"))
})
r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
  w.WriteHeader(405)
  w.Write([]byte("method is not valid"))
})

子路由

func main(){
    r := chi.NewRouter()
    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World!"))
    })

    // Creating a New Router
    apiRouter := chi.NewRouter()
    apiRouter.Get("/articles/{date}-{slug}", getArticle)

    // Mounting the new Sub Router on the main router
    r.Mount("/api", apiRouter)
}
r.Route("/articles", func(r chi.Router) {
    r.With(paginate).Get("/", listArticles)                           // GET /articles
    r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017

    r.Post("/", createArticle)                                        // POST /articles
    r.Get("/search", searchArticles)                                  // GET /articles/search

    // Regexp url parameters:
    r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug)                // GET /articles/home-is-toronto

    // Subrouters:
    r.Route("/{articleID}", func(r chi.Router) {
      r.Use(ArticleCtx)
      r.Get("/", getArticle)                                          // GET /articles/123
      r.Put("/", updateArticle)                                       // PUT /articles/123
      r.Delete("/", deleteArticle)                                    // DELETE /articles/123
    })
  })

路由组

通过分组,可以不同组使用不同的中间件 ·

func main(){
    r := chi.NewRouter()
    
    // Public Routes
    r.Group(func(r chi.Router) {
        r.Get("/", HelloWorld)
        r.Get("/{AssetUrl}", GetAsset)
        r.Get("/manage/url/{path}", FetchAssetDetailsByURL)
        r.Get("/manage/id/{path}", FetchAssetDetailsByID)
    })

    // Private Routes
    // Require Authentication
    r.Group(func(r chi.Router) {
        r.Use(AuthMiddleware)
        r.Post("/manage", CreateAsset)
    })

}

中间件

此中间件添加了一个上下文变量user=123

// HTTP middleware setting a value on the request context
func MyMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // create new context from `r` request context, and assign key `"user"`
    // to value of `"123"`
    ctx := context.WithValue(r.Context(), "user", "123")

    // call the next handler in the chain, passing the response writer and
    // the updated request object with the new context value.
    //
    // note: context.Context values are nested, so any previously set
    // values will be accessible as well, and the new `"user"` key
    // will be accessible from this point forward.
    next.ServeHTTP(w, r.WithContext(ctx))
  })
}

中间件下方的程序就可以获取到了

func MyHandler(w http.ResponseWriter, r *http.Request) {
    // here we read from the request context and fetch out `"user"` key set in
    // the MyMiddleware example above.
    user := r.Context().Value("user").(string)

    // respond to the client
    w.Write([]byte(fmt.Sprintf("hi %s", user)))
}

AllowContentEncoding

内容编码白名单中间件(gzip, deflate, gzip.deflate, deflate.gzip).意思是必须使用这类压缩方式?

import (
  "github.com/go-chi/chi/v5/middleware"
)

func main() {
  r := chi.NewRouter()
  r.Use(middleware.AllowContentEncoding("deflate", "gzip"))
  r.Post("/", func(w http.ResponseWriter, r *http.Request) {})
}

AllowContentType

允许类型白名单. 只允许某类型的内容. (application/json, text/xml, application/json, text/xml)

import (
  "github.com/go-chi/chi/v5/middleware"
)

func main(){
  r := chi.NewRouter()
  r.Use(middleware.AllowContentType("application/json","text/xml"))
  r.Post("/", func(w http.ResponseWriter, r *http.Request) {})
}

CleanPath

清洁路径,清除多余的双斜杠 如果用户请求 /users//1 或 //users////1 都将被视为:/users/1

Compress

压缩中间件 确保在响应上设置 Content-Type 标头,否则此中间件将不会压缩响应正文

import (
  "github.com/go-chi/chi/v5/middleware"
)

func main(){
  r := chi.NewRouter()
  r.Use(middleware.Compress(5, "text/html", "text/css"))
  r.Post("/", func(w http.ResponseWriter, r *http.Request) {})
}

ContentCharset

生成一个处理程序,如果所有字符集都不匹配,则该处理程序将写入415响应

  r := chi.NewRouter()
  allowedCharsets := []string{"UTF-8", "Latin-1", ""}
  r.Use(middleware.ContentCharset(allowedCharsets...))
  r.Post("/", func(w http.ResponseWriter, r *http.Request) {})

CORS

func main() {
  r := chi.NewRouter()

  // Basic CORS
  // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing
  r.Use(cors.Handler(cors.Options{
    // AllowedOrigins:   []string{"https://foo.com"}, // Use this to allow specific origin hosts
    AllowedOrigins:   []string{"https://*", "http://*"},
    // AllowOriginFunc:  func(r *http.Request, origin string) bool { return true },
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
    ExposedHeaders:   []string{"Link"},
    AllowCredentials: false,
    MaxAge:           300, // Maximum value not ignored by any of major browsers
  }))

  r.Get("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("welcome"))
  })

  http.ListenAndServe(":3000", r)
}

GetHead

自动将未定义的 HEAD 请求路由到 GET 处理程序. 不是太明白意思

func main(){
  r := chi.NewRouter()
  r.Use(middleware.GetHead)
  r.Get("/", func(w http.ResponseWriter, r *http.Request) {})
}

Heartbeat

func main(){
  r := chi.NewRouter()
  r.Use(middleware.Heartbeat("/"))
}

Logger

记录每个请求的开始和结束,以及一些有用的数据,包括请求的内容、响应状态以及返回所需的时间。

NoCache

设置了许多 HTTP 标头,以防止路由器(或子路由器)被上游代理和/或客户端缓存。

Oauth 2.0

关于授权服务(略过)

Profiler

用于挂载 net/http/pprof

import (
  "github.com/go-chi/chi/v5/middleware"
)

 func main(){
   r := chi.NewRouter()
   // ..middlewares
   r.Mount("/debug", middleware.Profiler())
   // ..routes
}

RealIP

获取一个真实的IP

Recoverer

从崩溃中恢复,记录崩溃(和回溯),并尽可能返回 HTTP 500(内部服务器错误)状态

RedirectSlashes

减去尾部斜杠的作用? RedirectSlashes 中间件与 http 不兼容

RouteHeaders

StripSlashes

Throttle

用于限制所有用户一次处理的当前请求数。(不是针对某一个用户) 默认情况下,Throttle 的 BacklogTimeout 为 60 秒

import (
  "github.com/go-chi/chi/v5/middleware"
)

func main(){
    r := chi.NewRouter()
    r.Use(middleware.Throttle(15))
    r.Post("/", func(w http.ResponseWriter, r *http.Request) {})
}

ThrottleBacklog

限制一次处理的当前处理请求数,并为保存有限数量的待处理请求提供积压。

import (
  "time"

  "github.com/go-chi/chi/v5/middleware"
)

func main(){
    r := chi.NewRouter()
    r.Use(ThrottleBacklog(10, 50, time.Second*10))
    r.Post("/", func(w http.ResponseWriter, r *http.Request) {})
}

Timeout

在给定超时后取消 ctx 并向客户端返回 504 网关超时错误。

 r.Get("/long", func(w http.ResponseWriter, r *http.Request) {
   ctx := r.Context()
   processTime := time.Duration(rand.Intn(4)+1) * time.Second

   select {
   case <-ctx.Done():
     return

   case <-time.After(processTime):
      // The above channel simulates some hard work.
   }

   w.Write([]byte("done"))
 })

// 用法
import (
  "github.com/go-chi/chi/v5/middleware"
)

func main(){
    r := chi.NewRouter()
    r.Use(middleware.Timeout(time.Second*60))
    // handlers ...
}

JWT身份验证

Http 限速中间件

package main

import (
  "net/http"

  "github.com/go-chi/chi"
  "github.com/go-chi/chi/middleware"
  "github.com/go-chi/httprate"
)

func main() {
  r := chi.NewRouter()
  r.Use(middleware.Logger)

  // 启用每分钟100个请求的速率请求限制器
  //
  // 在下面的代码示例中,速率限制被绑定到请求的IP地址通过LimitByIP中间件处理程序进行
  // 要为所有请求设置一个速率限制器,请使用httprate.LimitAll(..)
  // 
  r.Use(httprate.LimitByIP(100, 1*time.Minute))

  r.Get("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("."))
  })

  http.ListenAndServe(":3333", r)
}

官网文档就这么多,然后就是自己看示例学习

文件服务器示例

package main

import (
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

func main() {
	r := chi.NewRouter()
	r.Use(middleware.Logger)

	// Index handler
	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hi"))
	})

	// Create a route along /files that will serve contents from
	// the ./data/ folder.
	workDir, _ := os.Getwd()
	filesDir := http.Dir(filepath.Join(workDir, "data"))
	FileServer(r, "/files", filesDir)

	http.ListenAndServe(":3333", r)
}

// FileServer conveniently sets up a http.FileServer handler to serve
// static files from a http.FileSystem.
func FileServer(r chi.Router, path string, root http.FileSystem) {
	if strings.ContainsAny(path, "{}*") {
		panic("FileServer does not permit any URL parameters.")
	}

	if path != "/" && path[len(path)-1] != '/' {
		r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
		path += "/"
	}
	path += "*"

	r.Get(path, func(w http.ResponseWriter, r *http.Request) {
		rctx := chi.RouteContext(r.Context())
		pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
		fs := http.StripPrefix(pathPrefix, http.FileServer(root))
		fs.ServeHTTP(w, r)
	})
}