分类 "Golang" 下的文章

公司大量使用微服务框架 go-micro,但框架作者已不再维护低版本(新版本已经商业化),只能自己动手了。

起因是我用 Go 1.16 编译,启动报错:

panic: qtls.ConnectionState not compatible with tls.ConnectionState

goroutine 1 [running]:
github.com/lucas-clemente/quic-go/internal/handshake.init.0()
        /go/pkg/mod/github.com/lucas-clemente/quic-go@v0.12.1/internal/handshake/unsafe.go:17 +0x118

阅读全文

“ 阅读本文大约需要 40 分钟。”

上一篇文章 《大白话讲讲 Go 语言的 sync.Map(一)》 讲到 entry 数据结构,原因是 Go 语言标准库的 map 不是线程安全的,通过加一层抽象回避这个问题。

当一个 key 被删除的时候,比如李四销户了,以前要撕掉小账本,现在可以在大账本上写 expunged,

对,什么也不写也是 OK 的。也就是说,

entry.p 可能是真正的数据的地址,也可能是 nil,也可能是 expunged。

阅读全文

“ 阅读本文大约需要 4.25 分钟。”

程序是枯燥乏味的。

在讲 sync.Map 之前,我们先说说什么是 map(映射)。

我们每个人都有身份证号码,如果我需要从身份证号码查到对应的姓名,用 map 存储是非常合适的。

map[000...001] = 张三
map[000...002] = 李四
...
map[999...993] = 钱五

身份证号码有 18 位,如果要知道 111...002 这个人叫什么名字,没有 map 我只能从 000...001 一个一个往下查找,效率是非常低的。

阅读全文

背景

对于大多数 Gopher 来说,编写 Go 程序会直接在目录建立 main.go,xxx.go,yyy.go……

不是说不好,对于小型工程来说,简单反而简洁明了,我也提倡小工程没必要整一些花里胡哨的东西。

毕竟 Go 语言作为现代微服务的开发新宠,各个方面都比较自由,没有很多约束。我想,这也是它充满活力的原因。

对于大型工程而言,或者团队协作中,没有明确的规范,只会使得项目越来越凌乱……

因为每个人的心中对代码的管理、组织,对业务的理解不完全是一致的。

我参考了 非官网社区的规范 以及公司的规范,谈谈平时是怎么组织的,希望我的理解,对大家有所帮助。

目录结构示例

.
├── api                          路由与服务挂接
├── cmd                          程序入口,可以有多个程序
│   └── server
│       ├── inject               自动生成依赖注入代码
│       └── main.go
├── config                       配置相关文件夹
├── internal                     程序内部逻辑
│   ├── database
│   │   ├── redis.go 
│   │   └── mysql.go
│   ├── dao                      数据库操作接口/实现
│   │   ├── dao_impls
│   │   │   └── user_impls.go
│   │   └── user.go              用户 DAO 接口
│   ├── svc_impls                服务接口实现
│   │   ├── svc_auth
│   │   └── svc_user
│   └── sdks                     外部 SDK 依赖
└── service                      服务接口定义
        ├── auth.go              认证服务定义
        └── user.go              用户服务定义

面向接口编程

正如你所看到的,我的目录结构将接口和实现分开存放了。

根据依赖倒置原则(Dependence Inversion Principle),对象应依赖接口,而不是依赖实现。

阅读全文

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

比方说下面的 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.Request 的 ParseForm 和 ParseMultipartForm 方法,它只会解析一次。

而 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 的实现。