在Go语言中实现REST API的策略

huangapple go评论85阅读模式
英文:

strategy for REST API in go

问题

在我的数据库中,每一行对应一个结构体。

type datum struct{
    Id *string `json:"task_id"`
    Status *string `json:"status"`
    AccountId *string `json:"account_id"`
    .... // 还有很多其他字段,都是指针类型
}

在网页上,用户可以根据account_idstatus等字段进行查询。服务器将返回满足查询条件的所有数据,并投影出Idaccount_idstatus等字段。

目前,我编写了一个HTTP处理程序来完成以下任务:

从请求中提取查询条件作为一个datum对象:

body, err := ioutil.ReadAll(r.Body)
condition := datum{} 
err = json.Unmarshal(body, &condition)

使用部分填充的datum对象来查询数据库,只有非空字段会转换为SELECT ... WHERE ..=..语句。查询结果保存在query_result []datum中。

query_result写入JSON对象以便回复:

reply := map[string]interface{}{
    "reply": query_result,
}
data, err := json.Marshal(reply)

问题是,在回复中,许多字段的值为nil,但我仍然发送它们,这是浪费的。另一方面,我不想更改datum结构体以包含omitempty标签,因为在数据库中,值条目的所有字段都是非空的。

  • 在这种情况下,我应该为回复定义一个新的结构体吗?有没有办法使用datum结构体来定义这个新的结构体,而不是硬编码一个?
  • 是否有更好的设计来实现这个查询功能?
英文:

In my database, each row corresponds to a struct

type datum struct{
    Id *string `json:"task_id"`
    Status *string `json:"status"`
    AccountId *string `json:"account_id"`
    .... // many more fields, all of pointer types
}

On the webpage, the user can query on several fields of datum (say account_id and status). The server will return all data that satisfy the query with a projection of the fields (say Id, account_id and status).

Right now, I wrote a HTTP handler to

Extract the query as a datum object from the request:

body, err := ioutil.ReadAll(r.Body)
condition := datum{} 
err = json.Unmarshal(body, &condition)

Use the partially filled datum object to query the database, only the non-nil fields translate to SELECT ... WHERE ..=... The query result is saved in query_result []datum

Write the query_result into json object for reply:

reply := map[string]interface{}{
			"reply": query_result,
		}
data, err := json.Marshal(reply)

The problem is that in the reply many of the fields are nil, but I still send them, which is wasteful. On the other hand, I don't want to change the datum struct to include omitempty tag because in the database a value entry has all fields non-nil.

  • In this case, shall I define a new struct just for the reply? Is there a way to define this new struct using datum struct, instead of hard code one?
  • Is there a better design for this query feature?

答案1

得分: 1

你有几个选择,选择取决于在你的特定情况下什么更浪费/更昂贵:

  1. 只需在原始结构中使用指针+omitempty。
  2. 准备一个自定义的响应对象。但是你需要将原始结构中的值复制/转换到其导出版本中。
  3. 编写一个自定义的编组器,它将探索你的结构并创建一个导出就绪的变体,这样比#1更具动态性/自动性。

虽然#1不需要注释,而#2在上面由Gepser部分涵盖,但是下面是如何使用自定义编组器来解决这个问题(思路是重新组装输出,跳过空字段):

package main

import (
	"fmt"

	"encoding/json"
	"reflect"
)

type datum struct {
	Id        *string `json:"task_id"`
	Status    *string `json:"status"`
	AccountId *string `json:"account_id"`
}

type Response struct {
	Reply []datum `json:"reply"`
}

func main() {

	var query_result []datum

	// 模拟带有空字段的查询结果
	val_id_a := "id-a"
	val_status := "status-b"
	d1 := datum{
		Id:     &val_id_a,
		Status: &val_status,
	}

	query_result = append(query_result, d1)

	val_id_b := "id-b"
	val_account_id := "account-id-b"
	d2 := datum{
		Id:        &val_id_b,
		AccountId: &val_account_id,
	}

	query_result = append(query_result, d2)

	reply := &Response{
		Reply: query_result,
	}

	data, err := json.Marshal(reply)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", string(data))
}

// MarshalJSON 是 Response 对象的自定义 JSON 编组器实现。
func (r *Response) MarshalJSON() ([]byte, error) {
	a := struct {
		Reply []map[string]interface{} `json:"reply"`
	}{}

	for _, v := range r.Reply {
		a.Reply = append(a.Reply, converter(v))
	}

	return json.Marshal(a)
}

// converter 将结构转换为映射,跳过具有空值的字段。
func converter(in interface{}) map[string]interface{} {
	out := make(map[string]interface{})
	v := reflect.ValueOf(in)

	for i := 0; i < v.NumField(); i++ {
		f := v.Type().Field(i)
		tag := f.Tag.Get("json")
		if tag != "" && !v.Field(i).IsNil() {
			out[tag] = v.Field(i).Interface()
		}
	}
	return out
}

希望对你有所帮助!

英文:

You have several options, with choice depending what is more wasteful/expensive in your particular case:

  1. Just use pointers+omitempty in the original struct.
  2. Prepare a custom response object. But you'll need to copy/convert the values from the original struct into its export version.
  3. Write a custom marshaller, that will be exploring your struct and creating an export-ready variant, this way being more dynamic/automatic that #1.

While #1 needs no comments, and #2 to some extend covered by Gepser above, here's how you can address this with a custom marshaller (the idea is to re-assemble your output skipping nil fields):

package main
import (
&quot;fmt&quot;
&quot;encoding/json&quot;
&quot;reflect&quot;
)
type datum struct {
Id        *string `json:&quot;task_id&quot;`
Status    *string `json:&quot;status&quot;`
AccountId *string `json:&quot;account_id&quot;`
}
type Response struct {
Reply []datum `json:&quot;reply&quot;`
}
func main() {
var query_result []datum
// mocking a query result with records with nil fields
val_id_a := &quot;id-a&quot;
val_status := &quot;status-b&quot;
d1 := datum{
Id:     &amp;val_id_a,
Status: &amp;val_status,
}
query_result = append(query_result, d1)
val_id_b := &quot;id-b&quot;
val_account_id := &quot;account-id-b&quot;
d2 := datum{
Id:        &amp;val_id_b,
AccountId: &amp;val_account_id,
}
query_result = append(query_result, d2)
reply := &amp;Response{
Reply: query_result,
}
data, err := json.Marshal(reply)
if err != nil {
panic(err)
}
fmt.Printf(&quot;%+v\n&quot;, string(data))
}
// MarshalJSON is a custom JSON marshaller implementation for Response object.
func (r *Response) MarshalJSON() ([]byte, error) {
a := struct {
Reply []map[string]interface{} `json:&quot;reply&quot;`
}{}
for _, v := range r.Reply {
a.Reply = append(a.Reply, converter(v))
}
return json.Marshal(a)
}
// converter converts a struct into a map, skipping fields with nil values.
func converter(in interface{}) map[string]interface{} {
out := make(map[string]interface{})
v := reflect.ValueOf(in)
for i := 0; i &lt; v.NumField(); i++ {
f := v.Type().Field(i)
tag := f.Tag.Get(&quot;json&quot;)
if tag != &quot;&quot; &amp;&amp; !v.Field(i).IsNil() {
out[tag] = v.Field(i).Interface()
}
}
return out
}

答案2

得分: 0

我建议的方法(也是我使用的方法)是使用带有omitempty标签的new struct,例如:

type datumResponse struct{
    Id *string `json:"task_id,omitempty"`
    Status *string `json:"status,omitempty"`
    AccountId *string `json:"account_id,omitempty"`
    .... // 更多字段
}

如果没有子结构或者你不写一个结构数组,那么就没有办法使用旧结构的字段来编写新的struct

英文:

The approach I suggest (is the one I use) is the new struct with omitempty tag, for example:

type datumResponse struct{
Id *string `json:&quot;task_id,omitempty&quot;`
Status *string `json:&quot;status,omitempty&quot;`
AccountId *string `json:&quot;account_id,omitempty&quot;`
.... // many more fields
}

and there is no option to write your new struct using the fields of the old one if there is not substructs or you don't write an array of structs.

huangapple
  • 本文由 发表于 2016年12月9日 20:57:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/41061042.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定