最近在思考一个问题,针对用户的输入,能不能快速校验?

比方说下面的 struct,大家用过 gin 的就知道,支持指定某个字段为 required,用户如果不输入,就检验不通过。

type LoginForm struct {
    Username string `json:"username" form:"username"`
    Key      string `json:"key" form:"key"`
    Sign     string `json:"sign" form:"sign"`
}

然而,我们不能指望用户输入了,这个字段就是可以用的!例如他提交了这个表单:

{
    "username": " ",
    "key": "value",
    "sign": "     "
}

也就是说,用户输入了空格,就成功躲避我们的校验,所以我们不得不再校验一次:

form.Username = strings.TrimSpace(form.Username)
form.Key = strings.TrimSpace(form.Key)
form.Sign = strings.TrimSpace(form.Sign)

……显然,非常不优雅!如果字段多的话,满屏都是这样子的代码,相信屏幕前的你也是受不了的!

那有没有好办法?

因为公司存在 PHP 业务,为了兼容,JSON 的解析使用了 jsoniter 这个第三方包。如果你也是使用 gin 框架,别忘了编译指定 -tags=jsoniter 构建标签:

go build -tags=jsoniter -o ./${PROJECT_NAME} ./cmd/server

而 jsoniter 有个非常强大的功能,支持在解析类型的时候,执行你给定的钩子!

于是只要我们在项目启动的时候注册以下钩子函数:

当解析到 string 的时候,自动帮我们 TrimSpace !!!

func init() {
    jsonAutoTrimSpace()
}

func jsonAutoTrimSpace() {
    jsoniter.RegisterTypeDecoderFunc("string", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
        s := strings.TrimSpace(iter.ReadString())
        *((*string)(ptr)) = s
    })
}

至此,JSON 数据解析的时候,就支持自动去掉空白字符了。但是有时候参数不是 JSON 传递的,是通过 URL 的参数形式、或者是 POST 表单,该怎么办?

这个大家可以看下 http.RequestParseFormParseMultipartForm 方法,它只会解析一次。

而 gin 底层的 Bind 也是调用了这两个方法,我们可以在用户 Bind 之前,调用 ParseForm 和 ParseMultipartForm 方法,对里面的 Value 遍历并 TrimSpace 就好了!后面用户第二次调用,拿到的也是我们 TrimSpace 后的内容了。

func shouldBindForm(req *http.Request) (err error) {
    if err = req.ParseForm(); err != nil {
        return
    }
    const defaultMemory = 32 << 20
    if err = req.ParseMultipartForm(defaultMemory); err != nil {
        if err == http.ErrNotMultipart {
            err = nil
        } else {
            return
        }
    }
    for k, v := range req.Form {
        for i, s := range v {
            req.Form[k][i] = strings.TrimSpace(s)
        }
    }
    for k, v := range req.PostForm {
        for i, s := range v {
            req.PostForm[k][i] = strings.TrimSpace(s)
        }
    }
    if req.MultipartForm != nil {
        for k, v := range req.MultipartForm.Value {
            for i, s := range v {
                req.MultipartForm.Value[k][i] = strings.TrimSpace(s)
            }
        }
    }
    return
}

shouldBindForm 的调用时机,需要检查一下 ContentType,如果是表单,或者是 GET 请求,就执行一下,具体可以参考 gin 的实现。

上一篇文章 说说 Go 语言 for-range 的坑 说的是 for-range 的,工作中,其实还是遇到蛮多奇奇怪怪的问题,这里也顺便整理了一下,就当作是续集:)

先继续看 for-range 的另一个坑:

下面代码输出什么?

func main() {
    var a = []int{1, 2, 3, 4, 5}
    var r = make([]int, 0)

    for i, v := range a {
        if i == 0 {
            a = append(a, 6, 7)
        }
        r = append(r, v)
    }
    fmt.Println(r)
}

阅读全文

事情是这样,服务器很多人在使用,以前的离职同事留了一大堆不知道是什么东西。

那些文件看不了,又删不掉,非常碍眼。

我搜索了挺多资料,没有一篇文章能真的解决问题(感觉都是抄来抄去的)。

用 SFTP 工具、PHPStorm 全家桶,都删不了……

后面找到了一篇英文资料,提示说可以用 inode 编号删,所以就有了今天这篇文章。

步骤很简单,分两步:

阅读全文

nil 是什么?

可以看看官方的描述,在 buildin/buildin.go 里:

// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

nil 是一个预先声明的标识符,代表指针、通道、函数、接口、哈希表或切片的零值。

OK,回到问题,nil 能比较自己吗?

阅读全文

cURL,熟悉 Linux 的同学,没有人不知道这个命令吧:)

它有非常非常多的参数,我这里就不复制粘贴了,有需要可以 -h 或者谷歌搜索看看。

我从实用性的角度,说下我比较常用的几个参数:

  • -v:啰嗦模式,打印请求头部、响应头部,如果是 https 请求会打印握手信息;
  • -H:包含 HTTP 头部,可多次使用
  • -s:不打印进度条
  • -o:指定输出,如果不想看输出的内容,可以 -o /dev/null 丢弃
  • -k:忽略 https 证书校验
  • -d:用来提交数据
  • -X:指定 HTTP method
  • -w:可以输出连接的时间、服务器处理的时间、请求总时间等

简单的参数组合

curl -v -H 'Host: imlht.com.test' \
     -H 'Content-Type: application/json' \
     -H 'Cookie: uid=123' \
     -s \
     -o /dev/null \
     -X PUT \
     -k \
     -d '{"name": "John"}' https://10.43.1.101/user/123

有几个小细节,我提一下:

-k 可以忽略 https 的证书校验,调试的时候很好用。

比如上面的 imlht.com.test,一般情况下你是没有这个证书的,但是你又想测试 10.43.1.101 的 /user 接口,此时可以直接跳过测接口的内容。

-d 参数默认的 Content-Type 是表单,如果你提交的数据是 JSON,需要像上面一样指定好 JSON 头部。

有些接口会依赖这个头部解析 body 的内容;另外默认的 method 是 POST,这里我用了 -X PUT 指定 method。

有点复杂的 -w 参数

curl -w "\ntime_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_redirect: %{time_redirect}\ntime_pretransfer: %{time_pretransfer}\ntime_starttransfer: %{time_starttransfer}\n--------\ntime_total: %{time_total}\n" https://im.qq.com/
(省略一部分内容)
...
</body>
</html>

time_namelookup: 0.028
time_connect: 0.030
time_appconnect: 0.136
time_redirect: 0.000
time_pretransfer: 0.136
time_starttransfer: 0.296
--------
time_total: 0.436
  • time_namelookup :DNS 解析耗时
  • time_connect :TCP 连接建立耗时,也就是三次握手的时间
  • time_appconnect :SSL/SSH 等上层协议建立连接的时间,比如 connect/handshake 的时间
  • time_redirect :从开始到最后一个请求事务的时间
  • time_pretransfer :从请求开始到响应开始传输的时间
  • time_starttransfer :从请求开始到第一个字节将要传输的时间
  • time_total :这次请求花费的全部时间

根据上面的解释,我们可以得到以下的结论:

  • DNS 解析耗时:28ms
  • TCP 连接时间: pretransfter(136) - namelookup(28) = 108ms
  • 服务器处理时间:starttransfter(296) - pretransfer(136) = 160ms
  • 内容传输时间: total(436) - starttransfer(296) = 140ms