如官网所描述的
type (
LoginReq {
Username string `json:"username"`
Password string `json:"password""`
}
LoginReply {
Id int64 `json:"id"`
Name string `json:"name"`
Gender string `json:"gender"`
AccessToken string `json:"accessToken"`
AccessExpire int64 `json:"accessExpire"`
RefreshAfter int64 `json:"refreshAfter"`
}
)
service user-api {
@handler login
post /user/login (LoginReq) returns (LoginReply)
}
编写了一个.api
文件
生成之后,发现了一个奇怪的问题,post方法有很多形式,那么go-zero究竟支持哪些形式呢?查看源码找到了go-zero的处理方式
// api/internal/handler/loginhandler.go
func loginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.LoginReq
if err := httpx.Parse(r, &req); err != nil {
httpx.Error(w, err)
return
}
l := logic.NewLoginLogic(r.Context(), svcCtx)
resp, err := l.Login(&req)
if err != nil {
httpx.Error(w, err)
} else {
httpx.OkJson(w, resp)
}
}
}
可以看到go-zero帮我们把传参方式做了一个go-zero形式的包装httpx.Parse
。对准这个函数ctrl-b,进入这个httpx.parse
,可以看到
func Parse(r *http.Request, v interface{}) error {
if err := ParsePath(r, v); err != nil {
return err
}
if err := ParseForm(r, v); err != nil {
return err
}
if err := ParseHeaders(r, v); err != nil {
return err
}
return ParseJsonBody(r, v)
}
有意思,4种读参方式顺序执行。我立刻产生了一种想法,如果真的是顺序运行,那么真正有效的只有最后了一个啊,显然不可能这样,只有一种解释就是,四个函数内部都设置了一个唯一条件的判断,保证一个条件只能正常执行其中之一。
那先按顺序看看ctrl-b
这个ParsePath
// Like http://localhost/bag/:name
func ParsePath(r *http.Request, v interface{}) error {
vars := pathvar.Vars(r)
m := make(map[string]interface{}, len(vars))
for k, v := range vars {
m[k] = v
}
return pathUnmarshaler.Unmarshal(m, v)
}
顶端注释给我们简单的解释了这种传参方式,在restful api
规范了解过一点点,在url
中传参,这可不是我想要的传参方法啊,前面的逻辑很经典的处理了path参数,如果使用真的使用这个方式传参数,是可以看到m最终是包含了通过path传来的参数的,然而最终退出httpx.Parse
这个函数,是可以看到req
中是没有path方式传来的,显然问题出在这个pathUnmarshaler.Unmarshal
函数,进去看看
经过一系列的跳转来到了这个函数
// github.com/zeromicro/[email protected]/core/mapping/unmarshaler.go
func (u *Unmarshaler) unmarshalWithFullName(m Valuer, v interface{}, fullName string) error {
rv := reflect.ValueOf(v)
if err := ValidatePtr(&rv); err != nil {
return err
}
rte := reflect.TypeOf(v).Elem()
if rte.Kind() != reflect.Struct {
return errValueNotStruct
}
rve := rv.Elem()
numFields := rte.NumField()
for i := 0; i < numFields; i++ {
field := rte.Field(i)
if usingDifferentKeys(u.key, field) {
continue
}
if err := u.processField(field, rve.Field(i), m, fullName); err != nil {
return err
}
}
return nil
}
简单的调试后发现,这个判断条件总为真
if usingDifferentKeys(u.key, field) {
continue
}
我目测是将读出的path
的参数写入req
的逻辑在于这个u.processField
函数中,而这个continue跳过了这个过程。那这个usingDifferentKeys
是什么意思呢?首先看参数u.key
是什么意思,那必须要知道u这个实例是什么,找了一下就是上面的ParsePath
函数中的这个pathUnmarshaler
,这个实例是一个全局实例,在文件顶上创建出来的,
var (
formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues())
pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues())
)
这个pathKey
是个const值,即“path”
,也在这个文件的顶部,具体不说明了,总是实例中的u.key被初始化为“path”
进入这个函数看看
// github.com/zeromicro/[email protected]/core/mapping/utils.go
func usingDifferentKeys(key string, field reflect.StructField) bool {
if len(field.Tag) > 0 {
if _, ok := field.Tag.Lookup(key); !ok {
return true
}
}
return false
}
即查看types.LoginReq
类型中该字段的tag第一个是否是u.key
也就是那个“path”
也就是说,我们在.api
文件中写的tag就代表了我们希望以什么形式获取这个请求数据。
这样似乎我们就可以更加多样化一些,不同的参数可以通过不同的方式传。