1. Move BindXXX core logic to context.Context for two reasons:
   1.1 Controller should be stateless -- Due to historical reason, it's hard for us to do this but we should try it
   1.2 If users didn't use Controller to write their functions, they should be allowed to use those methods
2. Move XXXResp to context.Context
This commit is contained in:
Ming Deng 2021-08-06 22:45:35 +08:00
parent 0ace49bd46
commit e7d91a2bed
9 changed files with 365 additions and 233 deletions

2
.gitignore vendored
View File

@ -12,4 +12,6 @@ pkg/_beeTmp2/
test/tmp/
core/config/env/pkg/
my save path/
profile.out

View File

@ -55,6 +55,7 @@
- Deprecated BeeMap and replace all usage with `sync.map` [4616](https://github.com/beego/beego/pull/4616)
- TaskManager support graceful shutdown [4635](https://github.com/beego/beego/pull/4635)
- Add comments to `web.Config`, rename `RouterXXX` to `CtrlXXX`, define `HandleFunc` [4714](https://github.com/beego/beego/pull/4714)
- Refactor: Move `BindXXX` and `XXXResp` methods to `context.Context`. [4718](https://github.com/beego/beego/pull/4718)
- fix bug:reflect.ValueOf(nil) in getFlatParams [4715](https://github.com/beego/beego/pull/4715)
## Fix Sonar

2
go.mod
View File

@ -36,5 +36,7 @@ require (
go.etcd.io/etcd/client/v3 v3.5.0
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
google.golang.org/grpc v1.38.0
google.golang.org/protobuf v1.26.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

View File

@ -27,24 +27,38 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"time"
"google.golang.org/protobuf/proto"
"gopkg.in/yaml.v3"
"github.com/beego/beego/v2/core/utils"
"github.com/beego/beego/v2/server/web/session"
)
// Commonly used mime-types
const (
ApplicationJSON = "application/json"
ApplicationXML = "application/xml"
ApplicationYAML = "application/x-yaml"
TextXML = "text/xml"
ApplicationJSON = "application/json"
ApplicationXML = "application/xml"
ApplicationForm = "application/x-www-form-urlencoded"
ApplicationProto = "application/x-protobuf"
ApplicationYAML = "application/x-yaml"
TextXML = "text/xml"
formatTime = "15:04:05"
formatDate = "2006-01-02"
formatDateTime = "2006-01-02 15:04:05"
formatDateTimeT = "2006-01-02T15:04:05"
)
// NewContext return the Context with Input and Output
@ -65,6 +79,108 @@ type Context struct {
_xsrfToken string
}
func (ctx *Context) Bind(obj interface{}) error {
ct, exist := ctx.Request.Header["Content-Type"]
if !exist || len(ct) == 0 {
return ctx.BindJSON(obj)
}
i, l := 0, len(ct[0])
for i < l && ct[0][i] != ';' {
i++
}
switch ct[0][0:i] {
case ApplicationJSON:
return ctx.BindJSON(obj)
case ApplicationXML, TextXML:
return ctx.BindXML(obj)
case ApplicationForm:
return ctx.BindForm(obj)
case ApplicationProto:
return ctx.BindProtobuf(obj.(proto.Message))
case ApplicationYAML:
return ctx.BindYAML(obj)
default:
return errors.New("Unsupported Content-Type:" + ct[0])
}
}
// Resp sends response based on the Accept Header
// By default response will be in JSON
func (ctx *Context) Resp(data interface{}) error {
accept := ctx.Input.Header("Accept")
switch accept {
case ApplicationYAML:
return ctx.YamlResp(data)
case ApplicationXML, TextXML:
return ctx.XMLResp(data)
case ApplicationProto:
return ctx.ProtoResp(data.(proto.Message))
default:
return ctx.JSONResp(data)
}
}
func (ctx *Context) JSONResp(data interface{}) error {
return ctx.Output.JSON(data, false, false)
}
func (ctx *Context) XMLResp(data interface{}) error {
return ctx.Output.XML(data, false)
}
func (ctx *Context) YamlResp(data interface{}) error {
return ctx.Output.YAML(data)
}
func (ctx *Context) ProtoResp(data proto.Message) error {
return ctx.Output.Proto(data)
}
// BindYAML only read data from http request body
func (ctx *Context) BindYAML(obj interface{}) error {
return yaml.Unmarshal(ctx.Input.RequestBody, obj)
}
// BindForm will parse form values to struct via tag.
func (ctx *Context) BindForm(obj interface{}) error {
err := ctx.Request.ParseForm()
if err != nil {
return err
}
return ParseForm(ctx.Request.Form, obj)
}
// BindJSON only read data from http request body
func (ctx *Context) BindJSON(obj interface{}) error {
return json.Unmarshal(ctx.Input.RequestBody, obj)
}
// BindProtobuf only read data from http request body
func (ctx *Context) BindProtobuf(obj proto.Message) error {
return proto.Unmarshal(ctx.Input.RequestBody, obj)
}
// BindXML only read data from http request body
func (ctx *Context) BindXML(obj interface{}) error {
return xml.Unmarshal(ctx.Input.RequestBody, obj)
}
// ParseForm will parse form values to struct via tag.
func ParseForm(form url.Values, obj interface{}) error {
objT := reflect.TypeOf(obj)
objV := reflect.ValueOf(obj)
if !isStructPtr(objT) {
return fmt.Errorf("%v must be a struct pointer", obj)
}
objT = objT.Elem()
objV = objV.Elem()
return parseFormToStruct(form, objT, objV)
}
func isStructPtr(t reflect.Type) bool {
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct
}
// Reset initializes Context, BeegoInput and BeegoOutput
func (ctx *Context) Reset(rw http.ResponseWriter, r *http.Request) {
ctx.Request = r
@ -91,7 +207,7 @@ func (ctx *Context) Abort(status int, body string) {
// WriteString writes a string to response body.
func (ctx *Context) WriteString(content string) {
ctx.ResponseWriter.Write([]byte(content))
_, _ = ctx.ResponseWriter.Write([]byte(content))
}
// GetCookie gets a cookie from a request for a given key.
@ -124,7 +240,10 @@ func (ctx *Context) GetSecureCookie(Secret, key string) (string, bool) {
sig := parts[2]
h := hmac.New(sha256.New, []byte(Secret))
fmt.Fprintf(h, "%s%s", vs, timestamp)
_, err := fmt.Fprintf(h, "%s%s", vs, timestamp)
if err != nil {
return "", false
}
if fmt.Sprintf("%02x", h.Sum(nil)) != sig {
return "", false
@ -138,7 +257,7 @@ func (ctx *Context) SetSecureCookie(Secret, name, value string, others ...interf
vs := base64.URLEncoding.EncodeToString([]byte(value))
timestamp := strconv.FormatInt(time.Now().UnixNano(), 10)
h := hmac.New(sha256.New, []byte(Secret))
fmt.Fprintf(h, "%s%s", vs, timestamp)
_, _ = fmt.Fprintf(h, "%s%s", vs, timestamp)
sig := fmt.Sprintf("%02x", h.Sum(nil))
cookie := strings.Join([]string{vs, timestamp, sig}, "|")
ctx.Output.Cookie(name, cookie, others...)

189
server/web/context/form.go Normal file
View File

@ -0,0 +1,189 @@
// Copyright 2020 beego
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package context
import (
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
var (
sliceOfInts = reflect.TypeOf([]int(nil))
sliceOfStrings = reflect.TypeOf([]string(nil))
)
// ParseForm will parse form values to struct via tag.
// Support for anonymous struct.
func parseFormToStruct(form url.Values, objT reflect.Type, objV reflect.Value) error {
for i := 0; i < objT.NumField(); i++ {
fieldV := objV.Field(i)
if !fieldV.CanSet() {
continue
}
fieldT := objT.Field(i)
if fieldT.Anonymous && fieldT.Type.Kind() == reflect.Struct {
err := parseFormToStruct(form, fieldT.Type, fieldV)
if err != nil {
return err
}
continue
}
tag, ok := formTagName(fieldT)
if !ok {
continue
}
value, ok := formValue(tag, form, fieldT)
if !ok {
continue
}
switch fieldT.Type.Kind() {
case reflect.Bool:
b, err := parseFormBoolValue(value)
if err != nil {
return err
}
fieldV.SetBool(b)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
fieldV.SetInt(x)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
x, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return err
}
fieldV.SetUint(x)
case reflect.Float32, reflect.Float64:
x, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
fieldV.SetFloat(x)
case reflect.Interface:
fieldV.Set(reflect.ValueOf(value))
case reflect.String:
fieldV.SetString(value)
case reflect.Struct:
if fieldT.Type.String() == "time.Time" {
t, err := parseFormTime(value)
if err != nil {
return err
}
fieldV.Set(reflect.ValueOf(t))
}
case reflect.Slice:
if fieldT.Type == sliceOfInts {
formVals := form[tag]
fieldV.Set(reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(int(1))), len(formVals), len(formVals)))
for i := 0; i < len(formVals); i++ {
val, err := strconv.Atoi(formVals[i])
if err != nil {
return err
}
fieldV.Index(i).SetInt(int64(val))
}
} else if fieldT.Type == sliceOfStrings {
formVals := form[tag]
fieldV.Set(reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf("")), len(formVals), len(formVals)))
for i := 0; i < len(formVals); i++ {
fieldV.Index(i).SetString(formVals[i])
}
}
}
}
return nil
}
// nolint
func parseFormTime(value string) (time.Time, error) {
var pattern string
if len(value) >= 25 {
value = value[:25]
pattern = time.RFC3339
} else if strings.HasSuffix(strings.ToUpper(value), "Z") {
pattern = time.RFC3339
} else if len(value) >= 19 {
if strings.Contains(value, "T") {
pattern = formatDateTimeT
} else {
pattern = formatDateTime
}
value = value[:19]
} else if len(value) >= 10 {
if len(value) > 10 {
value = value[:10]
}
pattern = formatDate
} else if len(value) >= 8 {
if len(value) > 8 {
value = value[:8]
}
pattern = formatTime
}
return time.ParseInLocation(pattern, value, time.Local)
}
func parseFormBoolValue(value string) (bool, error) {
if strings.ToLower(value) == "on" || strings.ToLower(value) == "1" || strings.ToLower(value) == "yes" {
return true, nil
}
if strings.ToLower(value) == "off" || strings.ToLower(value) == "0" || strings.ToLower(value) == "no" {
return false, nil
}
return strconv.ParseBool(value)
}
// nolint
func formTagName(fieldT reflect.StructField) (string, bool) {
tags := strings.Split(fieldT.Tag.Get("form"), ",")
var tag string
if len(tags) == 0 || tags[0] == "" {
tag = fieldT.Name
} else if tags[0] == "-" {
return "", false
} else {
tag = tags[0]
}
return tag, true
}
func formValue(tag string, form url.Values, fieldT reflect.StructField) (string, bool) {
formValues := form[tag]
var value string
if len(formValues) == 0 {
defaultValue := fieldT.Tag.Get("default")
if defaultValue != "" {
value = defaultValue
} else {
return "", false
}
}
if len(formValues) == 1 {
value = formValues[0]
if value == "" {
return "", false
}
}
return value, true
}

View File

@ -31,7 +31,8 @@ import (
"strings"
"time"
yaml "gopkg.in/yaml.v2"
"google.golang.org/protobuf/proto"
"gopkg.in/yaml.v2"
)
// BeegoOutput does work for sending response header.
@ -154,7 +155,7 @@ func (output *BeegoOutput) Cookie(name string, value string, others ...interface
fmt.Fprintf(&b, "; HttpOnly")
}
}
// default empty
if len(others) > 5 {
if v, ok := others[5].(string); ok && len(v) > 0 {
@ -224,6 +225,19 @@ func (output *BeegoOutput) YAML(data interface{}) error {
return output.Body(content)
}
// Proto writes protobuf to the response body.
func (output *BeegoOutput) Proto(data proto.Message) error {
output.Header("Content-Type", "application/x-protobuf; charset=utf-8")
var content []byte
var err error
content, err = proto.Marshal(data)
if err != nil {
http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError)
return err
}
return output.Body(content)
}
// JSONP writes jsonp to the response body.
func (output *BeegoOutput) JSONP(data interface{}, hasIndent bool) error {
output.Header("Content-Type", "application/javascript; charset=utf-8")

View File

@ -17,8 +17,6 @@ package web
import (
"bytes"
context2 "context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"html/template"
@ -32,8 +30,7 @@ import (
"strings"
"sync"
"github.com/gogo/protobuf/proto"
"gopkg.in/yaml.v2"
"google.golang.org/protobuf/proto"
"github.com/beego/beego/v2/server/web/context"
"github.com/beego/beego/v2/server/web/context/param"
@ -244,49 +241,35 @@ func (c *Controller) HandlerFunc(fnname string) bool {
// URLMapping register the internal Controller router.
func (c *Controller) URLMapping() {}
// Bind if the content type is form, we read data from form
// otherwise, read data from request body
func (c *Controller) Bind(obj interface{}) error {
ct, exist := c.Ctx.Request.Header["Content-Type"]
if !exist || len(ct) == 0 {
return c.BindJSON(obj)
}
i, l := 0, len(ct[0])
for i < l && ct[0][i] != ';' {
i++
}
switch ct[0][0:i] {
case "application/json":
return c.BindJSON(obj)
case "application/xml", "text/xml":
return c.BindXML(obj)
case "application/x-www-form-urlencoded":
return c.BindForm(obj)
case "application/x-protobuf":
return c.BindProtobuf(obj)
case "application/x-yaml":
return c.BindYAML(obj)
default:
return errors.New("Unsupported Content-Type:" + ct[0])
}
return c.Ctx.Bind(obj)
}
// BindYAML only read data from http request body
func (c *Controller) BindYAML(obj interface{}) error {
return yaml.Unmarshal(c.Ctx.Input.RequestBody, obj)
return c.Ctx.BindYAML(obj)
}
// BindForm read data from form
func (c *Controller) BindForm(obj interface{}) error {
return c.ParseForm(obj)
return c.Ctx.BindForm(obj)
}
// BindJSON only read data from http request body
func (c *Controller) BindJSON(obj interface{}) error {
return json.Unmarshal(c.Ctx.Input.RequestBody, obj)
return c.Ctx.BindJSON(obj)
}
func (c *Controller) BindProtobuf(obj interface{}) error {
return proto.Unmarshal(c.Ctx.Input.RequestBody, obj.(proto.Message))
// BindProtobuf only read data from http request body
func (c *Controller) BindProtobuf(obj proto.Message) error {
return c.Ctx.BindProtobuf(obj)
}
// BindXML only read data from http request body
func (c *Controller) BindXML(obj interface{}) error {
return xml.Unmarshal(c.Ctx.Input.RequestBody, obj)
return c.Ctx.BindXML(obj)
}
// Mapping the method to function
@ -440,35 +423,23 @@ func (c *Controller) URLFor(endpoint string, values ...interface{}) string {
}
func (c *Controller) JSONResp(data interface{}) error {
c.Data["json"] = data
return c.ServeJSON()
return c.Ctx.JSONResp(data)
}
func (c *Controller) XMLResp(data interface{}) error {
c.Data["xml"] = data
return c.ServeXML()
return c.Ctx.XMLResp(data)
}
func (c *Controller) YamlResp(data interface{}) error {
c.Data["yaml"] = data
return c.ServeYAML()
return c.Ctx.YamlResp(data)
}
// Resp sends response based on the Accept Header
// By default response will be in JSON
// it's different from ServeXXX methods
// because we don't store the data to Data field
func (c *Controller) Resp(data interface{}) error {
accept := c.Ctx.Input.Header("Accept")
switch accept {
case context.ApplicationYAML:
c.Data["yaml"] = data
return c.ServeYAML()
case context.ApplicationXML, context.TextXML:
c.Data["xml"] = data
return c.ServeXML()
default:
c.Data["json"] = data
return c.ServeJSON()
}
return c.Ctx.Resp(data)
}
// ServeJSON sends a json response with encoding charset.
@ -518,11 +489,7 @@ func (c *Controller) Input() (url.Values, error) {
// ParseForm maps input data map to obj struct.
func (c *Controller) ParseForm(obj interface{}) error {
form, err := c.Input()
if err != nil {
return err
}
return ParseForm(form, obj)
return c.Ctx.BindForm(obj)
}
// GetString returns the input value by key string or the default value while it's present and input is blank

View File

@ -269,10 +269,10 @@ type respTestCase struct {
func TestControllerResp(t *testing.T) {
// test cases
tcs := []respTestCase{
{Accept: context.ApplicationJSON, ExpectedContentLength: 18, ExpectedResponse: "{\n \"foo\": \"bar\"\n}"},
{Accept: context.ApplicationXML, ExpectedContentLength: 25, ExpectedResponse: "<S>\n <foo>bar</foo>\n</S>"},
{Accept: context.ApplicationJSON, ExpectedContentLength: 13, ExpectedResponse: `{"foo":"bar"}`},
{Accept: context.ApplicationXML, ExpectedContentLength: 21, ExpectedResponse: `<S><foo>bar</foo></S>`},
{Accept: context.ApplicationYAML, ExpectedContentLength: 9, ExpectedResponse: "foo: bar\n"},
{Accept: "OTHER", ExpectedContentLength: 18, ExpectedResponse: "{\n \"foo\": \"bar\"\n}"},
{Accept: "OTHER", ExpectedContentLength: 13, ExpectedResponse: `{"foo":"bar"}`},
}
for _, tc := range tcs {

View File

@ -25,13 +25,8 @@ import (
"strconv"
"strings"
"time"
)
const (
formatTime = "15:04:05"
formatDate = "2006-01-02"
formatDateTime = "2006-01-02 15:04:05"
formatDateTimeT = "2006-01-02T15:04:05"
"github.com/beego/beego/v2/server/web/context"
)
// Substr returns the substr from start to length.
@ -266,165 +261,11 @@ func AssetsCSS(text string) template.HTML {
return template.HTML(text)
}
// ParseForm will parse form values to struct via tag.
// Support for anonymous struct.
func parseFormToStruct(form url.Values, objT reflect.Type, objV reflect.Value) error {
for i := 0; i < objT.NumField(); i++ {
fieldV := objV.Field(i)
if !fieldV.CanSet() {
continue
}
fieldT := objT.Field(i)
if fieldT.Anonymous && fieldT.Type.Kind() == reflect.Struct {
err := parseFormToStruct(form, fieldT.Type, fieldV)
if err != nil {
return err
}
continue
}
tags := strings.Split(fieldT.Tag.Get("form"), ",")
var tag string
if len(tags) == 0 || len(tags[0]) == 0 {
tag = fieldT.Name
} else if tags[0] == "-" {
continue
} else {
tag = tags[0]
}
formValues := form[tag]
var value string
if len(formValues) == 0 {
defaultValue := fieldT.Tag.Get("default")
if defaultValue != "" {
value = defaultValue
} else {
continue
}
}
if len(formValues) == 1 {
value = formValues[0]
if value == "" {
continue
}
}
switch fieldT.Type.Kind() {
case reflect.Bool:
if strings.ToLower(value) == "on" || strings.ToLower(value) == "1" || strings.ToLower(value) == "yes" {
fieldV.SetBool(true)
continue
}
if strings.ToLower(value) == "off" || strings.ToLower(value) == "0" || strings.ToLower(value) == "no" {
fieldV.SetBool(false)
continue
}
b, err := strconv.ParseBool(value)
if err != nil {
return err
}
fieldV.SetBool(b)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
fieldV.SetInt(x)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
x, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return err
}
fieldV.SetUint(x)
case reflect.Float32, reflect.Float64:
x, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
fieldV.SetFloat(x)
case reflect.Interface:
fieldV.Set(reflect.ValueOf(value))
case reflect.String:
fieldV.SetString(value)
case reflect.Struct:
switch fieldT.Type.String() {
case "time.Time":
var (
t time.Time
err error
)
if len(value) >= 25 {
value = value[:25]
t, err = time.ParseInLocation(time.RFC3339, value, time.Local)
} else if strings.HasSuffix(strings.ToUpper(value), "Z") {
t, err = time.ParseInLocation(time.RFC3339, value, time.Local)
} else if len(value) >= 19 {
if strings.Contains(value, "T") {
value = value[:19]
t, err = time.ParseInLocation(formatDateTimeT, value, time.Local)
} else {
value = value[:19]
t, err = time.ParseInLocation(formatDateTime, value, time.Local)
}
} else if len(value) >= 10 {
if len(value) > 10 {
value = value[:10]
}
t, err = time.ParseInLocation(formatDate, value, time.Local)
} else if len(value) >= 8 {
if len(value) > 8 {
value = value[:8]
}
t, err = time.ParseInLocation(formatTime, value, time.Local)
}
if err != nil {
return err
}
fieldV.Set(reflect.ValueOf(t))
}
case reflect.Slice:
if fieldT.Type == sliceOfInts {
formVals := form[tag]
fieldV.Set(reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(int(1))), len(formVals), len(formVals)))
for i := 0; i < len(formVals); i++ {
val, err := strconv.Atoi(formVals[i])
if err != nil {
return err
}
fieldV.Index(i).SetInt(int64(val))
}
} else if fieldT.Type == sliceOfStrings {
formVals := form[tag]
fieldV.Set(reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf("")), len(formVals), len(formVals)))
for i := 0; i < len(formVals); i++ {
fieldV.Index(i).SetString(formVals[i])
}
}
}
}
return nil
}
// ParseForm will parse form values to struct via tag.
func ParseForm(form url.Values, obj interface{}) error {
objT := reflect.TypeOf(obj)
objV := reflect.ValueOf(obj)
if !isStructPtr(objT) {
return fmt.Errorf("%v must be a struct pointer", obj)
}
objT = objT.Elem()
objV = objV.Elem()
return parseFormToStruct(form, objT, objV)
return context.ParseForm(form, obj)
}
var (
sliceOfInts = reflect.TypeOf([]int(nil))
sliceOfStrings = reflect.TypeOf([]string(nil))
)
var unKind = map[reflect.Kind]bool{
reflect.Uintptr: true,
reflect.Complex64: true,
@ -441,10 +282,11 @@ var unKind = map[reflect.Kind]bool{
// RenderForm will render object to form html.
// obj must be a struct pointer.
// nolint
func RenderForm(obj interface{}) template.HTML {
objT := reflect.TypeOf(obj)
objV := reflect.ValueOf(obj)
if !isStructPtr(objT) {
if objT.Kind() != reflect.Ptr || objT.Elem().Kind() != reflect.Struct {
return template.HTML("")
}
objT = objT.Elem()
@ -549,10 +391,6 @@ func parseFormTag(fieldT reflect.StructField) (label, name, fType string, id str
return
}
func isStructPtr(t reflect.Type) bool {
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct
}
// go1.2 added template funcs. begin
var (
errBadComparisonType = errors.New("invalid type for comparison")