Skip to main content

· 7 min read
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func CookieTool() gin.HandlerFunc {
return func(c *gin.Context) {
// Get cookie
if cookie, err := c.Cookie("label"); err == nil {
if cookie == "ok" {
c.Next()
return
}
}

// Cookie verification failed
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden with no cookie"})
c.Abort()
}
}

func main() {
route := gin.Default()

route.GET("/login", func(c *gin.Context) {
// Set cookie {"label": "ok" }, maxAge 30 seconds.
//func (c *Context) SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool
c.SetCookie("label", "ok", 30, "/", "localhost", false, true)
c.String(200, "Login success!")
})

route.GET("/home", CookieTool(), func(c *gin.Context) {
c.JSON(200, gin.H{"data": "Your home page"})
})

route.Run(":8080")
}

需要注意的是Cookie里的domain是严格匹配的:so 127.0.0.1 不同于 localhost, 如果domain设置的是localhost,那么你用127.0.0.1:8080/admin访问仍是Forbidden with no cookie

custom-validation

package main

import (
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)

// Booking contains binded and validated data.
type Booking struct {
CheckIn time.Time `form:"check_in" json:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" json:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}

//define tag
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if ok {
today := time.Now()
if today.After(date) {
return false
}
}
return true
}

func main() {
route := gin.Default()

//register
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}

route.POST("/bookable", getBookable)
route.Run(":8080")
}

func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBind(&b); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}

这时你可能会一脸懵逼,time解析失败 -- 原因是我们需要传入UTC time

favicon

package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
app := gin.Default()

// serve static favicon file from a location relative to main.go directory
//app.StaticFile("/favicon.ico", "./.assets/favicon.ico")
app.StaticFile("/favicon.ico", "./favicon.ico")

app.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "Hello favicon.")
})

app.Run(":8080")
}

file binding

package main

import (
"fmt"
"mime/multipart"
"net/http"
"path/filepath"

"github.com/gin-gonic/gin"
)

type BindFile struct {
Name string `form:"name" binding:"required"`
Email string `form:"email" binding:"required"`
File *multipart.FileHeader `form:"file" binding:"required"`
}

func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) {
var bindFile BindFile

// Bind file
if err := c.ShouldBind(&bindFile); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("err: %s", err.Error()))
return
}

// Save uploaded file
file := bindFile.File
fmt.Println("file name:", file.Filename)
fmt.Println("file size:", file.Size) //kb
dst := filepath.Base(file.Filename)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
return
}

c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully with fields name=%s and email=%s.", file.Filename, bindFile.Name, bindFile.Email))
})
router.Run(":8080")
}

将index.html放入public目录

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>File binding</title>
</head>
<body>
<h1>Bind file with fields</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
File: <input type="file" name="file"><br><br>
<input type="submit" value="Submit">
</form>
</body>

然后浏览器访问 http://127.0.0.1:8080/

submit之后:

可以看到 pdf已成功上传

再来看看文件大小对不对?

OK

我们再用postman测试一下

都是没问题的

graceful shutdown

a, notify with context

// build go1.16

package main

import (
"context"
"log"
"net/http"
"os/signal"
"syscall"
"time"

"github.com/gin-gonic/gin"
)

func main() {
// Create context that listens for the interrupt signal from the OS.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(10 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})

srv := &http.Server{
Addr: ":8080",
Handler: router,
}

// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()

// Listen for the interrupt signal.
<-ctx.Done()

// Restore default behavior on the interrupt signal and notify user of shutdown.
stop()
log.Println("shutting down gracefully, press Ctrl+C again to force")

// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown: ", err)
}

log.Println("Server exiting")
}

b, notify without context

//go:build go1.8
// +build go1.8

package main

import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})

srv := &http.Server{
Addr: ":8080",
Handler: router,
}

// Initializing the server in a goroutine so that
// it won't block the graceful shutdown handling below
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()

// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal, 1)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")

// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown: ", err)
}

log.Println("Server exiting")
}

group routes

package routes

import (
"github.com/gin-gonic/gin"
"net/http"
)

var router = gin.Default()

// Run will start the server
func Run() {
getRoutes()
router.Run(":5000")
}

// getRoutes will create our routes of our entire application
// this way every group of routes can be defined in their own file
// so this one won't be so messy
func getRoutes() {
v1 := router.Group("/v1")
addUserRoutes(v1)
addPingRoutes(v1)

v2 := router.Group("/v2")
addPingRoutes(v2)
}

func addUserRoutes(rg *gin.RouterGroup) {
users := rg.Group("/users")

users.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, "users")
})
users.GET("/comments", func(c *gin.Context) {
c.JSON(http.StatusOK, "users comments")
})
users.GET("/pictures", func(c *gin.Context) {
c.JSON(http.StatusOK, "users pictures")
})
}

func addPingRoutes(rg *gin.RouterGroup) {
ping := rg.Group("/ping")

ping.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, "pong")
})
}

mutiple service

package main

import (
"log"
"net/http"
"time"

"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)

var g errgroup.Group

func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})

return e
}

func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})

return e
}

func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}

g.Go(func() error {
return server01.ListenAndServe()
})

g.Go(func() error {
return server02.ListenAndServe()
})

if err := g.Wait(); err != nil {
log.Fatal(err)
}
}

· 6 min read

简介

tailwindcss 是一款原子性、需要预编译的、可用于快速构建用户自定义ui的css框架

可以看到其github star数已高达70k,tailwindcss为什么这么火,其魅力何在?

案例

接下来我们通过一个简单案例来解释一下

首先我们用next.js迅速创建一个简单应用

$ npx create-next-app . # 全部选用默认配置
$ npm run dev
// src/app/page.js
export default function Home() {
return (
<main>
<div className="bg-black w-10 h-10 md:bg-[green]"></div>
</main>
)
}

这段class的意思是:默认情况下背景色black,宽度10rem,高度10rem; 如果达到 screen 的 middle size 那么背景就会变成green

接着,我们用模拟器分别测试一下这段代码在iphone/ipad上的显示效果:

可以看到不同分辨率下,页面会达到不同的显示效果 - tailwindcss 不费吹灰之力就完成了传统需要写很多css代码才能完成的工作

vs bootstrap

有人会问:className="bg-black w-10 h-10 md:bg-[green]"这书写风格和bootstrap如出一辙啊,都是通过添加class来控制ui显示的 会不会是bootstrap的一个翻版?

答案不是,这2个框架的侧重点还是不一样的

从面向对象编程的角度来看两者的区别:

要自定义扩展页面样式,Bootstrap 使用的是继承,而 Tailwind 则使用的是组合,如果你对面向对象编程有比较深刻的理解,就可以体味到两者各自的优势,如果项目需要扩展样式,使用 Tailwind 越到后面越灵活,而如果项目样式根本不需要扩展,使用 Bootstrap 就够了。

从用户体验来看两者的区别:

bootstrap预先定义了很多常用的ui组件(如button,card,table),用户可以开箱即用(虽然tailwindcss也有提供,但需要付费)

tailwindcss对传统css进行了扩展,用户可以自定义变量来控制样式eg: w-[16px];原子型,粒度更细,一个class一个属性,便于扩展

不足

虽然bootstrap开箱即用,很方便,但是它元素耦合性高,如果需要自定义的样式,则需覆盖默认的样式,这可能会导致大量无效样式属性的加载。 而 Tailwind 恰恰相反,开箱什么组件和样式库都没有提供,一切都需要自己 DIY:你需要自行去为每个页面元素设计样式,然后组合使用 Tailwind 提供的工具集 class(每个 class 通常只负责设置单个属性,而不是像 Bootstrap 那样包含一堆属性)达到最终的渲染效果。因此,使用 Tailwind 每个 HTML 元素的 class 属性通常会有一连串值,这看起来好像很麻烦,不过,Tailwind 的优点正好弥补了 Bootstrap 的不足:对于一些长期维护的、面向用户的、需要有定制样式风格的项目,你不需要去加载和覆盖框架内置的样式属性,就可以轻松设计定制出自己独特风格的样式代码。

如何选择

实际开发中我们需要按照自己的项目需求去选择合适的 CSS 框架

  • 对于内部系统、管理后台、原型项目,使用 Bootstrap 可能更合适,
  • 而对于需要长期维护的前端界面、或者需要定制设计样式风格的项目,则使用 Tailwind 可能更合适。

常用class

1. Basic Style

  • 字体颜色: text-white, text-[#e96fc3], text-red-500 ...
  • 字体种类、粗细: font-xxx
  • 背景: bg-cyan-300

2. Layout

a, container

<div class="container mx-auto md:px-4">
<!-- ... -->
</div>

b, column

<div class="gap-8 columns-2 md:columns-3 hover:columns-4 ...">
<img class="w-full aspect-video ..." src="..." />
<img class="w-full aspect-square ..." src="..." />
<!-- ... -->
</div>

c, box-sizing

d, display

e, float

f, clear

g, object-fit

Utilities for controlling how a replaced element's content should be resized.

<img class="object-contain hover:object-scale-down">
<img class="object-contain md:object-scale-down">

h, Object Position

<img class="object-none object-left-top bg-yellow-300 w-24 h-24 ..." src="...">

i, Overflow

Utilities for controlling how an element handles content that is too large for the container.

j, Position

k, Top / Right / Bottom / Left

l, Visibility

m, Z-Index

Utilities for controlling the stack order of an element.

For more, please visite playground & documentation v3.3.2

· 3 min read

简介

viper是go语言配置文件的一个管理库,github star 23.2K

viper有以下特性:

  • 设置默认值
  • 从json, toml,yaml,envfile 和 java properties格式的配置文件读取配置信息
  • 实时监控和重新读区配置文件
  • 从远程配置系统(etcd或consul)读取并监控配置变化
  • 从命令行参数读取配置
  • 从buffer读区配置
  • 设置显式值

get started

下面来看看viper的简单用法, 以config.yml为例

#install
go get github.com/spf13/viper

目录结构:

package main

import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"log"
"os"
"path"
)

type Config struct {
Use string `mapstructure:"use"`
Cfg []Server `mapstructure:"cfg"`
}

type Server struct {
Name string `mapstructure:"name"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Likes []string `mapstructure:"likes"`
}

func main() {
pth := path.Join(os.Getenv("GOPATH"), "src/github.com/scott-x/test")

// name of config file (without extension)
viper.SetConfigName("config")
// REQUIRED if the config file does not have the extension in the name
viper.SetConfigType("yaml")
// optionally look for config in the working directory
viper.AddConfigPath(".")
// path to look for the config file in
viper.AddConfigPath(pth)

err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("fatal error config file: %w", err))
}

var config Config
if err = viper.Unmarshal(&config); err != nil {
panic("Unmarshal failed:" + err.Error())
}
log.Println(config)

go func() {
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()
}()
}

运行:

在不修改源码的情况下实现线上线下环境分离

思路:

  • 1,线下创建一个环境变量,如果能够读到就为debug模式,否则为生产模式
  • 2,利用flag 命令行指定环境 eg: xx -env debug

下面展示环境变量的用法,eg:

export TEST_ENV=1

代码结构:

package main

import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"log"
"os"
"path"
)

type Config struct {
Mysql `mapstructure:"mysql"`
}

type Mysql struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"prot"`
Username string `mapstructure:"username"`
}

func main() {
pth := path.Join(os.Getenv("GOPATH"), "src/github.com/scott-x/test/env")
var configFileName, env string
if myenv := os.Getenv("TEST_ENV"); len(myenv) > 0 {
env = "debug"
} else {
env = "pro"
}
configFileName = fmt.Sprintf("config-%s.yml", env)
log.Println("configFileName:", configFileName)

// name of config file (without extension)
viper.SetConfigName(configFileName)
// REQUIRED if the config file does not have the extension in the name
viper.SetConfigType("yaml")
// optionally look for config in the working directory
viper.AddConfigPath(".")
// path to look for the config file in
viper.AddConfigPath(pth)

err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("fatal error config file: %w", err))
}

var config Config
if err = viper.Unmarshal(&config); err != nil {
panic("Unmarshal failed:" + err.Error())
}
log.Println(config)

go func() {
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()
}()
}

不设置环境变量运行:

设置环境变量后运行:

· 2 min read

你是否遇到这样的情形,费了好大功夫把服务部署上线,但是某一时刻因为停电而导致服务挂掉

通电后,你又把之前的动作给做了一遍,这种重复性工作不仅耗时,还有可能出错; 有人说我可以写个脚本让它自己去操作,但是还是需要手动去开启服务

我们的目标是无需人为干预,开机自动启动服务

我们可以使用docker或者借用flag包,把系统重启时拿不到的环境变量写在配置文件里

这样稍微配置一下crontab就可以达到开机自动重启服务的目的

以下是个小demo, 默认它会读取项目目录下的config.json, 服务真正上线时需要用-c指定配置文件的位置

package main

import (
"flag"
"fmt"
)

func main() {
var myConfigFile string
flag.StringVar(&myConfigFile, "c", "./config.json", "the position of the config file")
flag.Parse()
fmt.Println(myConfigFile) //go run main.go -c /some/path/file
}

· 24 min read

gin目前已有69.3K star

一个简单的http demo

package main

import (
"github.com/gin-gonic/gin"
"log"
)

func main() {
//gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.GET("/", index)
log.Println("server is running at http://127.0.0.1:3333")
router.Run(":3333")
}

func index(c *gin.Context) {
//db operation
c.JSON(200, gin.H{
"code": "2000",
"msg": "ok",
"likes": []string{"apple", "pear", "orange"},
})
}

gin重要struct

想要了解gin,必须掌握 gin.Context

type Context struct {
Request *http.Request
Writer ResponseWriter

Params Params

// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}

// Errors is a list of errors attached to all the
// handlers/middlewares who used this context.
Errors errorMsgs

// Accepted defines a list of manually accepted formats
// for content negotiation.
Accepted []string
// contains filtered or unexported fields
}

type ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier

// Status returns the HTTP response status code of the current request.
Status() int

// Size returns the number of bytes
// already written into the response http body.
// See Written()
Size() int

// WriteString writes the string into the response body.
WriteString(string) (int, error)

// Written returns true if the response body was already written.
Written() bool

// WriteHeaderNow forces to write the http header (status code + headers).
WriteHeaderNow()

// Pusher get the http.Pusher for server push
Pusher() http.Pusher
}

net/http: regarding Cookie

http.Request

type Cookie struct {
Name string //eg:"x-my-token"
Value string //jwt token

Path string // optional eg:"/"
Domain string // optional eg:"localhost"
Expires time.Time // optional
RawExpires string // for reading cookies only

// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
MaxAge int
Secure bool //需要设置成false 否则postman拿不到预期的response
HttpOnly bool //需设置为true
SameSite SameSite
Raw string
Unparsed []string // Raw text of unparsed attribute-value pairs
}
func SetCookie(c *gin.Context) {
cookie := http.Cookie{
Name: "x-token",
Value: "jwt_token_value",
Path: "/",
Domain: "localhost",
Expires: time.Now().Add(time.Minute * time.Duration(5)), //5 min 后过期
RawExpires: "",
MaxAge: 0,
Secure: false, //如果这里设置为true 则postman永远拿不到cookie
HttpOnly: true,
SameSite: 0,
Raw: "",
Unparsed: nil,
}
http.SetCookie(c.Writer, &cookie)
c.JSON(200, gin.H{
"code": 2000,
"msg": "success",
})
}

获取参数

package main

import (
"github.com/gin-gonic/gin"
"log"
)

func main() {
route := gin.Default()

apiGroup := route.Group("/api")
{
//http://127.0.0.1:9999/api/test1
apiGroup.GET("/test1", Test1)
//http://127.0.0.1:9999/api/test2
apiGroup.GET("/test2", Test2)
//http://127.0.0.1:9999/api/test3
apiGroup.GET("/test3/:name/:company", Test3)
}

log.Fatal(route.Run(":9999"))
}

func Test1(c *gin.Context) {
//get username & password
u := struct {
Username string `json:"username"`
Password string `json:"password"`
}{}
err := c.ShouldBindJSON(&u)
if err != nil {
c.JSONP(200, gin.H{
"code": 2001,
"msg": "bad request",
})
return
}

c.JSONP(200, gin.H{
"code": 2000,
"msg": "success",
"data": u,
})
}

func Test2(c *gin.Context) {
//func (c *Context) Query(key string) (value string
name := c.Query("name")
age := c.Query("age")
//建议使用DefaultQuery() 如果用户没输入还可设定默认值
//注意Query()返回值都是string
//name := C.DefaultQuery("name","Scott")
//age := C.DefaultQuery("age","18")
c.JSON(200, gin.H{
"code": 2000,
"name": name,
"age": age,
})
}

func Test3(c *gin.Context) {
//func (c *Context) Param(key string) string
name := c.Param("name")
company := c.Param("company")
c.JSON(200, gin.H{
"code": 2000,
"name": name,
"company": company,
})
}

如果前端是通过json传递数据的 那么需要调用 c.ShouldBindJSON()

如果是通过papram url传递 则调用c.Query()

如果是通过变量的方式传递 则调用c.Param()

middleware

gin的middleware很简单:定义一个function 返回 gin.HandlerFunc 即可

package main

import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)

func main() {
route := gin.Default()

apiGroup := route.Group("/api")
apiGroup.Use(MiddlewareDemo())
{
apiGroup.GET("/test1", Test1) //http://127.0.0.1:9999/api/test1
apiGroup.GET("/test2", Test2) //http://127.0.0.1:9999/api/test2
}

log.Fatal(route.Run(":9999"))
}

func Test1(c *gin.Context) {
c.JSONP(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": "test1",
})
}

func Test2(c *gin.Context) {
c.JSONP(http.StatusOK, gin.H{
"code": 2000,
"msg": "success",
"data": "test2",
})
}

// gin的middleware很简单:定义一个function 返回 gin.HandlerFunc 即可
func MiddlewareDemo() gin.HandlerFunc {
return func(c *gin.Context) {
log.Println("middleware called")
//拦截用c.Abort()
c.Next()
}
}

middleware执行顺序:

package main

import (
"fmt"
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

router.Use(globalMiddleware())

router.GET("/rest/n/api/*some", mid1(), mid2(), handler)

router.Run()
}

func globalMiddleware() gin.HandlerFunc {
fmt.Println("globalMiddleware")
//return前可以初始化 按注册顺序 只会执行一次
return func(c *gin.Context) {
fmt.Println("1")
c.Next()
fmt.Println("2")
}
}

func handler(c *gin.Context) {
fmt.Println("exec handler.")
}

func mid1() gin.HandlerFunc {
fmt.Println("mid1")
//return前可以初始化 按注册顺序 只会执行一次
return func(c *gin.Context) {

fmt.Println("3")
c.Next()
fmt.Println("4")
}
}

func mid2() gin.HandlerFunc {
fmt.Println("mid2")
//return前可以初始化 按注册顺序 只会执行一次
return func(c *gin.Context) {
fmt.Println("5")
c.Next()
fmt.Println("6")
}
}

启动服务:

请求:http://127.0.0.1:8080/rest/n/api/1

再次请求:http://127.0.0.1:8080/rest/n/api/1

画图表示:

参考官网: How to build one effective middleware?

常见middleware:

更多用法

Abort

package main

import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)

func main() {
route := gin.Default()

apiGroup := route.Group("/api").Use(AuthMiddleware())
{
apiGroup.GET("/test", Test)
}

log.Fatal(route.Run(":9999"))
}

func Test(c *gin.Context) {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 2000,
"msg": "test",
})
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
isLogin := false
if isLogin {
c.Next()
} else {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "未登陆",
})
c.Abort() //Abort阻止了后续逻辑的执行 如果换成return是不行的 ,return并不能hold住后面的逻辑
}
}
}

Abort的原理是让Engine的index越界

render html

gin没有自己去实现一套模版的语法,它用的go语言标准库的一套模板html/template

web框架从请求到返回的全过程

  • router.LoadHTMLFiles(file string)
  • router.LoadHTMLGlob("templates/*")
  • router.LoadHTMLGlob("templates/**/*")

以上与文件相关的的API不能同时出现,只能选一个, 完整demo

exit gracefully

package main

import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"os"
"os/signal"
"syscall"
)

func main() {
//优雅退出: 当我们关闭程序的时候应该做的后续处理,
//比如我们做爬虫的时候,数据爬下来了,这时一个ctrl+c/kill程序关闭了 数据没有处理
//微服务:启动之前 或者启动之后做一件事: 将当前的服务的ip地址和端口号注册到注册中心
//我们当前的服务停止了以后并没有告知注册中心,这时如果前端正好请求了这个ip的服务 就会报错

router := gin.Default()

router.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "pong",
})
})

//主协程退出 子协程也跟着退出
go func() {
router.Run(":8088")
}()

// 如果想要接收信号,kill可以,但kill -9 强杀命令 golang是没机会收到信号的
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) //处理ctrl+c 和 kill
<-quit

//处理后续的逻辑
fmt.Println("关闭server中。。。")
fmt.Println("注销服务。。。")
}

表单验证

表单验证是web框架的一项重要功能

在gin中,若要将请求主体绑定到结构体中,需使用模型绑定,目前支持JSON, XML,YAML和标准表单值(foo=bar&boo=baz)的绑定

gin使用的是第三方库go-playground/validator来验证参数, GitHub Start 13.3K, documentation

使用方法:需要在绑定的字段上设置tag,比如,绑定格式为json,需这样设置json:"fieldname"

//postman/YAPI 
//如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
//否则只能通过raw/JSON的方式发送
type LoginForm struct {
//多个条件用逗号隔开 不用加空格
User string `form:"user" json:"user" binding:"required,min=3,max=5"`
Password string `form:"user" json:"password" binding:"required"`
}

此外,gin还提供了2套绑定方法

  • Must Bind
    • Methods: Bind, BindJSON, BindXML, BindQuery, BindYAML
    • Behavior: 这些方法底层使用MustBindWith, 如果存在绑定错误,请求将被以下指令中止c.AbordWithError(400,err).SetType(ErrorTypeBind), 响应状态码会被设置为400,请求头Content-Type被设置为text/plain;charset=utf-8. 注意如果你试图在此之后设置响应代码,将会发出[Gin-debug] [Warning] Headers were already written. Wanted to override status code 400 with 422, 如果你希望更好的控制行为,请使用ShouldBind的相关方法
  • Should bind - 实际开发中我们只要留意ShouldBind, ShouldBindJSON这2个方法就好了
    • Methods: ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
    • Behavior: 这些方法底层使用ShouldBindWith,如果存在绑定错误,则返回错误,开发人员可以正确的处理请求和错误

当我们使用绑定方法时,gin会根据Content-Type推断出使用哪种绑定器,如果你确定你绑定的是什么,可以使用MustBindWith或者BindingWith

你还可以给字段指定特定规则的修饰符,如果一个字段用binding:"required"修饰,并且在绑定改字段的值为空,那么将会返回一个错误

package main

import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过raw/JSON的方式发送,一般我们配置json的tag就可以了
type LoginForm struct {
//多个条件用逗号隔开 不用加空格
User string `form:"user" json:"user" binding:"required,min=3,max=5"`
Password string `form:"password" json:"password" binding:"required"`
}

type SignUpParam struct {
Name string `json:"name" binding:"required"`
Age uint8 `json:"age" binding:"gte=1,lte=18"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

func main() {
router := gin.Default()
router.POST("/loginJSON", func(c *gin.Context) {
var loginForm LoginForm
if err := c.ShouldBind(&loginForm); err != nil {
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"user": loginForm.User,
"password": loginForm.Password,
"msg": "登录成功",
})
})

router.POST("/signup", func(c *gin.Context) {
var signup SignUpParam
if err := c.ShouldBind(&signup); err != nil {
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "注册成功",
})
})
router.Run(":8088")
}

接下来,我们用postman来发送请求

测试登陆接口

  1. 不加任何参数

  1. 只传入user,不满足长度要求

  1. 只传入user,满足长度要求

  1. 传入user和password

测试注册接口

输入的字段只满足name

输入的字段满足所有要求

注意

可以看到gin会把验证的所有错误抛出来, 同时错误信息不太友好, 其实validator是支持多语言的,接下来我们将错误信息翻译成中文

将错误信息翻译成中文

在看下面这个demo之前,请先读懂validator给的example

package main

import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"log"
"net/http"
)

// postman/YAPI
//如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
//否则只能通过body/raw/JSON的方式发送
type SignUpForm struct {
//多个条件用逗号隔开 不用加空格
Name string `json:"name" binding:"required,min=3"`
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
//跨字段
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

var trans ut.Translator //国际化翻译器

func InitTrans(locale string) (err error) {
//修改gin框架中的validator引擎属性,实现定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
//第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
uni := ut.New(enT, zhT, enT)
trans, ok = uni.GetTranslator(locale) //初始化全局翻译器
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}

switch locale {
case "en":
en_translations.RegisterDefaultTranslations(v, trans)
case "zh":
zh_translations.RegisterDefaultTranslations(v, trans)
default:
en_translations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}

func main() {
if err := InitTrans("zh"); err != nil {
fmt.Println("初始化翻译器错误")
return
}
router := gin.Default()
router.POST("/signpForm", func(c *gin.Context) {
var signupForm SignUpForm
if err := c.ShouldBind(&signupForm); err != nil {
//
errs, ok := err.(validator.ValidationErrors)
//如果非验证错误
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": errs.Error(),
})
return
}
//如果是验证错误
c.JSON(http.StatusBadRequest, gin.H{
"error": errs.Translate(trans),
})
return
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"user": signupForm.Name,
"age": signupForm.Age,
"email": signupForm.Email,
"password": signupForm.Password,
"msg": "登录成功",
})
})
router.Run(":8088")
}

接着我们用postman测试一下接口

可以看到错误信息提示已经人性化很多了,但是字段名还是golang的风格,不符合json的模式

将错误信息里的字段名大写改成小写

package main

import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"log"
"net/http"
"reflect"
"strings"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过body/raw/JSON的方式发送
type SignUpForm struct {
//多个条件用逗号隔开 不用加空格
Name string `json:"name,aa" binding:"required,min=3"`
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
//跨字段
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

var trans ut.Translator //国际化翻译器

func InitTrans(locale string) (err error) {
//修改gin框架中的validator引擎属性,实现定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//注册一个获取json的tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
fmt.Println("fld.Tag.Get(json):", fld.Tag.Get("json"))
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
fmt.Println("json tag name:", name)
//兼容 - : golang中json tag为 - 会被忽略
if name == "-" { //RePassword string `json:"-"`
return ""
}
return name
})
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
//第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
uni := ut.New(enT, zhT, enT)
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}

switch locale {
case "en":
en_translations.RegisterDefaultTranslations(v, trans)
case "zh":
zh_translations.RegisterDefaultTranslations(v, trans)
default:
en_translations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}

func main() {
if err := InitTrans("zh"); err != nil {
fmt.Println("初始化翻译器错误")
return
}
router := gin.Default()
router.POST("/signpForm", func(c *gin.Context) {
var signupForm SignUpForm
if err := c.ShouldBind(&signupForm); err != nil {
//
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": errs.Error(),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
//errs.Translate(trans)的本质就是map[string]string
"error": errs.Translate(trans),
})
return
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"user": signupForm.Name,
"age": signupForm.Age,
"email": signupForm.Email,
"password": signupForm.Password,
"msg": "登录成功",
})
})
router.Run(":8088")
}

再次发送请求

可以看到 Email 已变成了email, 字段名已成功以json tag为主

最终改造

上面的实例还是不太友好,我们要将 "SignUpForm.email" 改成 "email"

errs.Translate(trans)的本质就是map[string]string,我们修改一下key就ok了

package main

import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"log"
"net/http"
"reflect"
"strings"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过body/raw/JSON的方式发送
type SignUpForm struct {
//多个条件用逗号隔开 不用加空格
Name string `json:"name,aa" binding:"required,min=3"`
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
//跨字段
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

var trans ut.Translator //国际化翻译器

func InitTrans(locale string) (err error) {
//修改gin框架中的validator引擎属性,实现定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//注册一个获取json的tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
//fmt.Println("fld.Tag.Get(json):", fld.Tag.Get("json"))
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
//fmt.Println("json tag name:", name)
//兼容 - : golang中json tag为 - 会被忽略
if name == "-" { //RePassword string `json:"-"`
return ""
}
return name
})
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
//第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
uni := ut.New(enT, zhT, enT)
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}

switch locale {
case "en":
en_translations.RegisterDefaultTranslations(v, trans)
case "zh":
zh_translations.RegisterDefaultTranslations(v, trans)
default:
en_translations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}

// eg: "SignUpForm.email"` 改成 `"email"`
func fixStructKey(fileds map[string]string) map[string]string {
rsp := make(map[string]string)
for field, err := range fileds {
rsp[field[strings.LastIndex(field, ".")+1:]] = err
}
return rsp
}

func main() {
if err := InitTrans("zh"); err != nil {
fmt.Println("初始化翻译器错误")
return
}
router := gin.Default()
router.POST("/signpForm", func(c *gin.Context) {
var signupForm SignUpForm
if err := c.ShouldBind(&signupForm); err != nil {
//
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": errs.Error(),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
//errs.Translate(trans)的本质就是map[string]string
"error": fixStructKey(errs.Translate(trans)),
})
return
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"user": signupForm.Name,
"age": signupForm.Age,
"email": signupForm.Email,
"password": signupForm.Password,
"msg": "登录成功",
})
})
router.Run(":8088")
}

再次发送请求

OK!

自定义验证器

业务要求变化多样,官方的validator不可能满足我们的所有要求,因此validator也暴露出了定制自己的验证器的方法

  1. 定义验证器
//返回值是否通过验证
func ValidateXXX(fl validator.FieldLevel) bool{
//获取字段值
mobile := fl.Field().String()
//剩下的交给自己
return true
}
  1. 注册验证器
//注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("mobile", ValidateXXX)
}
  1. 接着我们就可以像下面这样添加自定义规则了
type LoginForm struct {
//多个条件用逗号隔开 不能加空格
Mobile string `json:"mobile" form:"mobile" binding:"required,mobile"`
Password string `json:"password" form:"password" binding:"required"`
}

来看看完整的demo:

package main

import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"net/http"
"reflect"
"regexp"
"strings"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过body/raw/JSON的方式发送
type LoginForm struct {
//多个条件用逗号隔开 不用加空格
Mobile string `json:"mobile" form:"mobile" binding:"required,mobile"`
Password string `json:"password" form:"password" binding:"required"`
}

var trans ut.Translator //国际化翻译器

// 返回值是否通过验证
func validateMobile(fl validator.FieldLevel) bool {
//获取字段值
mobile := fl.Field().String()
//剩下的交给自己
mobileRe := regexp.MustCompile(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`)
if res := mobileRe.FindString(mobile); len(res) > 0 {
return true
}
return false
}

func InitTrans(locale string) (err error) {
//修改gin框架中的validator引擎属性,实现定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//注册一个获取json的tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
//fmt.Println("fld.Tag.Get(json):", fld.Tag.Get("json"))
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
//fmt.Println("json tag name:", name)
//兼容 - : golang中json tag为 - 会被忽略
if name == "-" { //RePassword string `json:"-"`
return ""
}
return name
})
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
//第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
uni := ut.New(enT, zhT, enT)
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}

switch locale {
case "en":
en_translations.RegisterDefaultTranslations(v, trans)
case "zh":
zh_translations.RegisterDefaultTranslations(v, trans)
default:
en_translations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}

// eg: "SignUpForm.email"` 改成 `"email"`
func fixStructKey(fileds map[string]string) map[string]string {
rsp := make(map[string]string)
for field, err := range fileds {
rsp[field[strings.LastIndex(field, ".")+1:]] = err
}
return rsp
}

func handleValidatorError(c *gin.Context, err error) {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": errs.Error(),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
//errs.Translate(trans)的本质就是map[string]string
"error": fixStructKey(errs.Translate(trans)),
})
return
//log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
}

func main() {
if err := InitTrans("zh"); err != nil {
fmt.Println("初始化翻译器错误")
return
}

//注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("mobile", validateMobile)
}

router := gin.Default()
router.POST("/signpForm", func(c *gin.Context) {
var loginForm LoginForm
if err := c.ShouldBind(&loginForm); err != nil {
handleValidatorError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "登录成功",
})
})
router.Run(":8088")
}

启动服务并向postman发起请求

  • a, 不携带任何参数, 接口返回正常

  • b, 手机号输入非法的

自定义的验证器居然漏翻译了

好在官方给出了解决方案(line 105-111)

package main

import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"net/http"
"reflect"
"regexp"
"strings"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过body/raw/JSON的方式发送
type LoginForm struct {
//多个条件用逗号隔开 不用加空格
Mobile string `json:"mobile" form:"mobile" binding:"required,mobile"`
Password string `json:"password" form:"password" binding:"required"`
}

var trans ut.Translator //国际化翻译器

// 返回值是否通过验证
func validateMobile(fl validator.FieldLevel) bool {
//获取字段值
mobile := fl.Field().String()
//剩下的交给自己
mobileRe := regexp.MustCompile(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`)
if res := mobileRe.FindString(mobile); len(res) > 0 {
return true
}
return false
}

func InitTrans(locale string) (err error) {
//修改gin框架中的validator引擎属性,实现定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//注册一个获取json的tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
//fmt.Println("fld.Tag.Get(json):", fld.Tag.Get("json"))
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
//fmt.Println("json tag name:", name)
//兼容 - : golang中json tag为 - 会被忽略
if name == "-" { //RePassword string `json:"-"`
return ""
}
return name
})
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
//第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
uni := ut.New(enT, zhT, enT)
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}

switch locale {
case "en":
en_translations.RegisterDefaultTranslations(v, trans)
case "zh":
zh_translations.RegisterDefaultTranslations(v, trans)
default:
en_translations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}

// eg: "SignUpForm.email"` 改成 `"email"`
func fixStructKey(fileds map[string]string) map[string]string {
rsp := make(map[string]string)
for field, err := range fileds {
rsp[field[strings.LastIndex(field, ".")+1:]] = err
}
return rsp
}

func handleValidatorError(c *gin.Context, err error) {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": errs.Error(),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
//errs.Translate(trans)的本质就是map[string]string
"error": fixStructKey(errs.Translate(trans)),
})
return
//log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
}

func main() {
if err := InitTrans("zh"); err != nil {
fmt.Println("初始化翻译器错误")
return
}

//注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("mobile", validateMobile)
_ = v.RegisterTranslation("mobile", trans, func(ut ut.Translator) error {
return ut.Add("required", "{0}手机号码不合法", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
}

router := gin.Default()
router.POST("/signpForm", func(c *gin.Context) {
var loginForm LoginForm
if err := c.ShouldBind(&loginForm); err != nil {
handleValidatorError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "登录成功",
})
})
router.Run(":8088")
}

更新代码,再次请求

Everything is normal!

· One min read
npm i react-hls-player --save
import React from 'react';
import ReactHlsPlayer from "react-hls-player";
//这个库有缺陷 好久没维护了 只支持到react16
//另外video必须是.m3u8格式的

export default class HlsPlayer extends React.Component {
render(){
const { url } = this.props
return (
<ReactHlsPlayer
src={url}
autoPlay={false}
controls={true}
width="100%"
height="auto"
/>
)
}
}
<HlsPlayer
src={"https://video.scott-xiong.com/movie/2/output.m3u8"}
/>

· 3 min read

熟悉vim的童鞋可能用过很多vim的插件,但是如何实现的呢?

今天以lua的视角来体验一下lua插件的开发过程:

file structure

1.首先,任意位置创建如下目录

.
└── lua
└── luaPlugin
└── init.lua

init.lua中我们随便写点什么:

2.如何使这个插件生效呢?

我们需要把插件添加到lua的runtime path中:

# 插件根目录中运行
vim --cmd "set rtp+=./"

接着我们就可以打开vim运行这个插件了

可以看到"hello world"已正常输出

需要注意的是无论你require多少次"hello world"只会输出一次

export function

我们可能会经常看到如下类似的配置:引入一个模块,调用一个function,然后配置leader key

这里暂不讨论lua是如何配置的

init.lua中我们先来定义一个lua function然后导出:

print("hello world")

local function some_function( ... )
-- body
print("hello from function")
end

--important
return {
some_function = some_function
}

接着熟悉的配方,熟悉的套路,先set path下:

# 插件根目录中运行 
vim --cmd "set rtp+=./"

接着调用function:

可以看到"hello world""hello from function"都输出了:前者是首次加载时才会输出,可以用于初始化;后者是function的调用,每次调用都会执行

interact with vim

更新lua code如下:

local function run_jest( ... )
-- body
local file = vim.fn.expand("%:p")
print("my file is " .. file)
end

--important
return {
run_jest = run_jest
}

然后调用function

可以看到路径可以正常输出:

新增一句,打开一个新的terminal

vim运行:

可以看到没有任何问题

在此基础上, 我们进行深层次的操作,在打开的terminal中输出hello world

local function run_jest( ... )
-- body
local file = vim.fn.expand("%:p")
print("my file is " .. file)
vim.cmd("vsplit | terminal")
local command = ':call jobsend(b:terminal_job_id, "echo hello world\\n")'
vim.cmd(command)
end

--important
return {
run_jest = run_jest
}

运行

新开的terminal正常输出"hello world"(这里有很大的发挥空间,输出测试结果,打印日志信息等)

· 2 min read

Neovim uses interprocess communications for stuff like that. You have to implement the neovim remote plugin protocol in go (or use a library, if that exists) and then write a server program that does what you want by communicating with nvim.

Neovim 使用进程间通信来处理类似的事情。 你必须在 go 中实现 neovim 远程插件协议(或使用相关库),然后编写一个服务器程序,通过与 nvim 通信来执行你想要的操作。

目前支持的库有:

下面来看看如何去实现?

package main

import (
"strings"
"github.com/neovim/go-client/nvim/plugin"
)

func hello(args []string) (string, error) {
return "Hello " + strings.Join(args, " "), nil
}

func main() {
plugin.Main(func(p *plugin.Plugin) error {
p.HandleFunction(&plugin.FunctionOptions{Name: "Hello"}, hello)
return nil
})
}
  1. 首先将以上代码进行编译go build -o hello,然后将二进制文件放入$GOPATH/bin目录中
  2. 任意位置编写个vim文件,以test.vim为例:
if exists('g:loaded_hello')
finish
endif
let g:loaded_hello = 1

function! s:Requirehello(host) abort
" 'hello' is the binary created by compiling the program above.
return jobstart(['hello'], {'rpc': v:true})
endfunction

call remote#host#Register('hello', 'x', function('s:Requirehello'))
" The following lines are generated by running the program
" command line flag --manifest hello
call remote#host#RegisterPlugin('hello', '0', [
\ {'type': 'function', 'name': 'Hello', 'sync': 1, 'opts': {}},
\ ])

" vim:ts=4:sw=4:et
  1. init.lua加入这句即可vim.cmd('source ~/some/path/test.vim')
  2. 打开vim使用 :echo Hello('world')

来看看效果:

· One min read
package main

import (
"fmt"
"golang.org/x/net/websocket"
"net/http"
)

type Server struct {
conns map[*websocket.Conn]bool
}

func NewServer() *Server {
return &Server{
conns: make(map[*websocket.Conn]bool),
}
}

func (s *Server) handleWS(ws *websocket.Conn) {
fmt.Println("new incoming connection from client:", ws.RemoteAddr())
s.conns[ws] = true
s.readLoop(ws)
}

func (s *Server) readLoop(ws *websocket.Conn) {
buf := make([]byte, 1024)
for {
n, err := ws.Read(buf)
if err != nil {
fmt.Println("read error:", err)
continue
}
data := buf[:n]
fmt.Println("received data:", string(data))
ws.Write([]byte("thank you for the msg"))
}
}

func main() {
s := NewServer()
http.Handle("/ws", websocket.Handler(s.handleWS))
http.ListenAndServe(":8911", nil)
}

· 2 min read

<Empty/>

import { Empty } from 'antd';

const App = () => <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
export default App;

<Button>

import { Button, Space } from 'antd';


<Button type="primary" size='small' danger>
Button
</Button>

<Icon/>

import { AreaChartOutlined } from '@ant-design/icons';

<AreaChartOutlined />

<Tag/>

import { Tag } from 'antd';

const App = () => <Tag color="magenta">magenta</Tag>;
export default App;

<Pagination/>

import { Pagination } from 'antd';

const App = () => <Pagination
defaultCurrent={1}
pageSize={page_size}
total={all_items}
current={current_page}
onChange={this.pageChange}
/>;
export default App;

API

<Table/>

一般Table组件搭配Empty组件使用

antd的table组件是真tm难用啊,很多人反应数据达到200+,卡的一批,知乎:为什么Ant-Design的Table性能这么低?

但是table还是得用啊,临时解决方案用的是react-bootstrap

# 1. install
npm install react-bootstrap --save
# 2, public index.html引入样式文件
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">

使用:

import React, { Component } from 'react'
import { Table} from 'react-bootstrap';

export default class Test extends Component {
render() {
return (
<div>
<Table striped bordered condensed='true' hover>
<thead>
<tr>
<th>#</th>
<th>First Name</th>
<th>Last Name</th>
<th>Username</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
<tr>
<td>2</td>
<td>Jacob</td>
<td>Thornton</td>
<td>@fat</td>
</tr>
<tr>
<td>3</td>
<td colSpan="2">Larry the Bird</td>
<td>@twitter</td>
</tr>
</tbody>
</Table>

</div>
)
}
}

效果如下:

<Input/>

import { Input } from 'antd';
const App = () => <Input placeholder="Basic usage" />;
export default App;

import { Input } from 'antd';
const { Search } = Input;

<Search
placeholder="input search text"
allowClear
enterButton="Search"
size="large"
onSearch={onSearch}
/>

API