我在 Go 中创建了一个 API,它在被调用时执行查询,创建一个结构的实例,然后将该结构编码为 JSON,然后再发送回调用者。我现在想让调用者能够通过传入“字段”GET 参数来选择他们想要返回的特定字段。
这意味着根据字段值,我的结构会改变。有没有办法从结构中删除字段?或者至少动态地将它们隐藏在 JSON 响应中? (注意:有时我有空值,所以 JSON omitEmpty 标记在这里不起作用)如果这些都不可能,有没有更好的处理方法的建议?
我正在使用的结构的较小版本如下:
type SearchResult struct {
Date string `json:"date"`
IdCompany int `json:"idCompany"`
Company string `json:"company"`
IdIndustry interface{} `json:"idIndustry"`
Industry string `json:"industry"`
IdContinent interface{} `json:"idContinent"`
Continent string `json:"continent"`
IdCountry interface{} `json:"idCountry"`
Country string `json:"country"`
IdState interface{} `json:"idState"`
State string `json:"state"`
IdCity interface{} `json:"idCity"`
City string `json:"city"`
} //SearchResult
type SearchResults struct {
NumberResults int `json:"numberResults"`
Results []SearchResult `json:"results"`
} //type SearchResults
然后我像这样编码并输出响应:
err := json.NewEncoder(c.ResponseWriter).Encode(&msg)
问题是要求根据调用者提供的字段列表动态选择字段。使用静态定义的 json 结构标记无法做到这一点。
如果您想要始终跳过一个字段以进行 json 编码,那么当然使用 json:"-"
忽略该字段。 (另请注意,如果您的字段未导出,则不需要;这些字段总是被 json 编码器忽略。)这不是问题所要问的。
要引用对 json:"-"
答案的评论:
这个 [json:"-" 答案] 是大多数人最终从这里搜索到的答案,但它不是问题的答案。
在这种情况下,我会使用 map[string]interface{}
而不是结构。您可以通过调用地图上的内置 delete
来轻松删除字段以删除字段。
也就是说,如果您一开始就不能只查询请求的字段。
使用`json:“-”`
// Field is ignored by this package.
Field int `json:"-"`
// Field appears in JSON as key "myName".
Field int `json:"myName"`
// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`
// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`
文档:http://golang.org/pkg/encoding/json/#Marshal
另一种方法是使用带有 ,omitempty
标记的 pointers 结构。如果指针为 nil,则不会编组字段。
此方法不需要额外的反射或低效使用地图。
与 jorelli 使用此方法的示例相同:http://play.golang.org/p/JJNa0m2_nw
您可以使用 reflect
包通过反映字段标记并选择 json
标记值来选择所需的字段。在您的 SearchResults 类型上定义一个方法,该方法选择您想要的字段并将它们作为 map[string]interface{}
返回,然后编组 that 而不是 SearchResults 结构本身。这是一个如何定义该方法的示例:
func fieldSet(fields ...string) map[string]bool {
set := make(map[string]bool, len(fields))
for _, s := range fields {
set[s] = true
}
return set
}
func (s *SearchResult) SelectFields(fields ...string) map[string]interface{} {
fs := fieldSet(fields...)
rt, rv := reflect.TypeOf(*s), reflect.ValueOf(*s)
out := make(map[string]interface{}, rt.NumField())
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
jsonKey := field.Tag.Get("json")
if fs[jsonKey] {
out[jsonKey] = rv.Field(i).Interface()
}
}
return out
}
这是一个可运行的解决方案,它显示了您将如何调用此方法并编组您的选择:http://play.golang.org/p/1K9xjQRnO8
我刚刚发布了 sheriff,它根据结构字段上注释的标签将结构转换为映射。然后,您可以编组(JSON 或其他)生成的地图。它可能不允许您仅序列化调用者请求的字段集,但我想使用一组组可以让您涵盖大多数情况。直接使用组而不是字段很可能还会增加缓存能力。
例子:
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/hashicorp/go-version"
"github.com/liip/sheriff"
)
type User struct {
Username string `json:"username" groups:"api"`
Email string `json:"email" groups:"personal"`
Name string `json:"name" groups:"api"`
Roles []string `json:"roles" groups:"api" since:"2"`
}
func main() {
user := User{
Username: "alice",
Email: "alice@example.org",
Name: "Alice",
Roles: []string{"user", "admin"},
}
v2, err := version.NewVersion("2.0.0")
if err != nil {
log.Panic(err)
}
o := &sheriff.Options{
Groups: []string{"api"},
ApiVersion: v2,
}
data, err := sheriff.Marshal(o, user)
if err != nil {
log.Panic(err)
}
output, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.Panic(err)
}
fmt.Printf("%s", output)
}
取三种材料:
用于循环结构的所有字段的反射包。一个 if 语句来选择你想要编组的字段,以及 encoding/json 包来编组你喜欢的字段。
准备:
以良好的比例混合它们。使用 reflect.TypeOf(your_struct).Field(i).Name() 获取 your_struct 的第 i 个字段的名称。使用 reflect.ValueOf(your_struct).Field(i) 获取 your_struct 的第 i 个字段的类型值表示。使用 fieldValue.Interface() 检索 Value 类型的 fieldValue 的实际值(向上转换为 interface{} 类型)(注意括号的使用 - Interface() 方法产生 interface{}
如果你幸运地设法在这个过程中没有烧毁任何晶体管或断路器,你应该得到这样的东西:
func MarshalOnlyFields(structa interface{},
includeFields map[string]bool) (jsona []byte, status error) {
value := reflect.ValueOf(structa)
typa := reflect.TypeOf(structa)
size := value.NumField()
jsona = append(jsona, '{')
for i := 0; i < size; i++ {
structValue := value.Field(i)
var fieldName string = typa.Field(i).Name
if marshalledField, marshalStatus := json.Marshal((structValue).Interface()); marshalStatus != nil {
return []byte{}, marshalStatus
} else {
if includeFields[fieldName] {
jsona = append(jsona, '"')
jsona = append(jsona, []byte(fieldName)...)
jsona = append(jsona, '"')
jsona = append(jsona, ':')
jsona = append(jsona, (marshalledField)...)
if i+1 != len(includeFields) {
jsona = append(jsona, ',')
}
}
}
}
jsona = append(jsona, '}')
return
}
服务:
使用任意结构和您想要包含的字段的 map[string]bool
提供服务,例如
type magic struct {
Magic1 int
Magic2 string
Magic3 [2]int
}
func main() {
var magic = magic{0, "tusia", [2]int{0, 1}}
if json, status := MarshalOnlyFields(magic, map[string]bool{"Magic1": true}); status != nil {
println("error")
} else {
fmt.Println(string(json))
}
}
胃口好!
我创建了这个函数,通过忽略一些字段将 struct 转换为 JSON 字符串。希望它会有所帮助。
func GetJSONString(obj interface{}, ignoreFields ...string) (string, error) {
toJson, err := json.Marshal(obj)
if err != nil {
return "", err
}
if len(ignoreFields) == 0 {
return string(toJson), nil
}
toMap := map[string]interface{}{}
json.Unmarshal([]byte(string(toJson)), &toMap)
for _, field := range ignoreFields {
delete(toMap, field)
}
toJson, err = json.Marshal(toMap)
if err != nil {
return "", err
}
return string(toJson), nil
}
示例:https://play.golang.org/p/nmq7MFF47Gp
您可以使用标记属性“omitifempty”或制作可选字段指针,并使您希望跳过的那些未初始化。
我没有同样的问题,但类似。当然,如果您不介意性能问题,下面的代码也可以解决您的问题。在对您的系统实施这种解决方案之前,我建议您尽可能重新设计您的结构。发送可变结构响应是过度设计的。我相信响应结构代表了请求和资源之间的合同,它不应该是依赖请求。(你可以让不需要的字段为空,我这样做)。在某些情况下,我们必须实施这种设计,如果您认为自己在这种情况下,这里是我使用的 play link 和代码。
type User2 struct {
ID int `groups:"id" json:"id,omitempty"`
Username string `groups:"username" json:"username,omitempty"`
Nickname string `groups:"nickname" json:"nickname,omitempty"`
}
type User struct {
ID int `groups:"private,public" json:"id,omitempty"`
Username string `groups:"private" json:"username,omitempty"`
Nickname string `groups:"public" json:"nickname,omitempty"`
}
var (
tagName = "groups"
)
//OmitFields sets fields nil by checking their tag group value and access control tags(acTags)
func OmitFields(obj interface{}, acTags []string) {
//nilV := reflect.Value{}
sv := reflect.ValueOf(obj).Elem()
st := sv.Type()
if sv.Kind() == reflect.Struct {
for i := 0; i < st.NumField(); i++ {
fieldVal := sv.Field(i)
if fieldVal.CanSet() {
tagStr := st.Field(i).Tag.Get(tagName)
if len(tagStr) == 0 {
continue
}
tagList := strings.Split(strings.Replace(tagStr, " ", "", -1), ",")
//fmt.Println(tagList)
// ContainsCommonItem checks whether there is at least one common item in arrays
if !ContainsCommonItem(tagList, acTags) {
fieldVal.Set(reflect.Zero(fieldVal.Type()))
}
}
}
}
}
//ContainsCommonItem checks if arrays have at least one equal item
func ContainsCommonItem(arr1 []string, arr2 []string) bool {
for i := 0; i < len(arr1); i++ {
for j := 0; j < len(arr2); j++ {
if arr1[i] == arr2[j] {
return true
}
}
}
return false
}
func main() {
u := User{ID: 1, Username: "very secret", Nickname: "hinzir"}
//assume authenticated user doesn't has permission to access private fields
OmitFields(&u, []string{"public"})
bytes, _ := json.Marshal(&u)
fmt.Println(string(bytes))
u2 := User2{ID: 1, Username: "very secret", Nickname: "hinzir"}
//you want to filter fields by field names
OmitFields(&u2, []string{"id", "nickname"})
bytes, _ = json.Marshal(&u2)
fmt.Println(string(bytes))
}
这是我定义我的结构的方式。
type User struct {
Username string `json:"username" bson:"username"`
Email string `json:"email" bson:"email"`
Password *string `json:"password,omitempty" bson:"password"`
FullName string `json:"fullname" bson:"fullname"`
}
并且在我的函数集 user.Password = nil
内不被编组。
我也遇到了这个问题,起初我只是想在我的 http 处理程序中专门处理响应。我的第一种方法是创建一个包,将结构的信息复制到另一个结构,然后编组第二个结构。我使用反射做了那个包,所以,从不喜欢这种方法,而且我也不是动态的。
所以我决定修改 encoding/json 包来做到这一点。函数 Marshal
、MarshalIndent
和 (Encoder) Encode
还接收一个
type F map[string]F
我想模拟编组所需字段的 JSON,因此它只编组地图中的字段。
https://github.com/jtorz/jsont
package main
import (
"fmt"
"log"
"net/http"
"github.com/jtorz/jsont/v2"
)
type SearchResult struct {
Date string `json:"date"`
IdCompany int `json:"idCompany"`
Company string `json:"company"`
IdIndustry interface{} `json:"idIndustry"`
Industry string `json:"industry"`
IdContinent interface{} `json:"idContinent"`
Continent string `json:"continent"`
IdCountry interface{} `json:"idCountry"`
Country string `json:"country"`
IdState interface{} `json:"idState"`
State string `json:"state"`
IdCity interface{} `json:"idCity"`
City string `json:"city"`
} //SearchResult
type SearchResults struct {
NumberResults int `json:"numberResults"`
Results []SearchResult `json:"results"`
} //type SearchResults
func main() {
msg := SearchResults{
NumberResults: 2,
Results: []SearchResult{
{
Date: "12-12-12",
IdCompany: 1,
Company: "alfa",
IdIndustry: 1,
Industry: "IT",
IdContinent: 1,
Continent: "america",
IdCountry: 1,
Country: "México",
IdState: 1,
State: "CDMX",
IdCity: 1,
City: "Atz",
},
{
Date: "12-12-12",
IdCompany: 2,
Company: "beta",
IdIndustry: 1,
Industry: "IT",
IdContinent: 1,
Continent: "america",
IdCountry: 2,
Country: "USA",
IdState: 2,
State: "TX",
IdCity: 2,
City: "XYZ",
},
},
}
fmt.Println(msg)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
//{"numberResults":2,"results":[{"date":"12-12-12","idCompany":1,"idIndustry":1,"country":"México"},{"date":"12-12-12","idCompany":2,"idIndustry":1,"country":"USA"}]}
err := jsont.NewEncoder(w).Encode(msg, jsont.F{
"numberResults": nil,
"results": jsont.F{
"date": nil,
"idCompany": nil,
"idIndustry": nil,
"country": nil,
},
})
if err != nil {
log.Fatal(err)
}
})
http.ListenAndServe(":3009", nil)
}
这个问题现在有点老了,但不久前我遇到了同样的问题,因为我发现没有简单的方法来做到这一点,所以我建立了一个库来实现这个目的。它允许从静态结构轻松生成 map[string]interface{}
。
https://github.com/tuvistavie/structomap
[]byte
的一个主要问题是它不是很可重用:例如,没有简单的方法可以在之后添加一个字段。所以我建议创建一个 map[string]interface{}
并将 JSON 序列化部分放入标准库。
map[string]interface{}
确实有意义,但它不需要您丢弃您的类型定义。Id
但不想返回整个 json 结构。谢谢你!