go proposal: encoding/json: iter.Seq marshal support

xam8gpfp  于 22天前  发布在  Go
关注(0)|答案(9)|浏览(17)

提案详情

我建议 json.Marshal 应该像对待 []T 一样对待 iter.Seq[T],将其编码为数组。这样我们就可以例如写成:

json.Marshal(xiter.Map(time.Duration.String, []time.Duration{5*time.Second, 3*time.Hour}))
// -> ["5s","3h0m0s"]

并避免分配切片。

46scxncf

46scxncf1#

如何实现这个?

func Marshal(v any) ([]byte, error) {
	if seq, ok := v.(iter.Seq[T]); ok {
		// It's a sequence
	}
}

显然是不可能的。

9rbhqvlz

9rbhqvlz2#

好的,通过反射是可能的,参见#61897(评论)

pwuypxnk

pwuypxnk3#

我希望有一个通用的API,可以动态地表示与Go切片或Map在语义上等价的类型,因此像链表或B树这样的容器类型不需要自定义MarshalJSONUnmarshalJSON方法。为了实现这一点,我期望:

  • API支持MarshalUnmarshal。目前,提案仅适用于Marshal,但我认为我们也需要为Unmarshal找到一个解决方案。
  • 描述有序值的通用接口:
  • (用于编组)遍历值
  • (用于解组)重置序列并将值追加到序列中的能力
  • 描述无序键值对的通用接口:
  • (用于编组)遍历键值对
  • (用于解组)重置Map并获取或设置任意键的能力
zi8p0yeb

zi8p0yeb4#

@gophun,建议的实现using reflect.Value.Call would have poor performance。除了我对marshal和unmarshal之间不对称的担忧,我们还需要在"reflect"中提供一流的支持或优化,以使迭代快速进行。

lx0bsm1f

lx0bsm1f5#

To encode and decode iter.Seq using JSON, one way is to implement the json.Marshaler and json.Unmarshaler interfaces for iter.Seq .
Here's an example implementation:

func (s Seq[V]) MarshalJSON() ([]byte, error) {
	var buf bytes.Buffer
	buf.WriteByte('[')

	first := true
	for v := range s {
		if !first {
			buf.WriteByte(',')
		}
		first = false

		bytes, err := json.Marshal(v)
		if err != nil {
			return nil, err
		}
		buf.Write(bytes)
	}
	buf.WriteByte(']')

	return buf.Bytes(), nil
}

func (s *Seq[V]) UnmarshalJSON(bytes []byte) error {
	var vs []V
	if err := json.Unmarshal(bytes, &vs); err != nil {
		return err
	}
	*s = Iter(vs)
	return nil
}

This implementation allows iter.Seq to be marshaled into JSON format and unmarshaled from JSON format seamlessly.
https://go.dev/play/p/Jqg81JhEr9_f?v=gotip

nimxete2

nimxete26#

作为一个合适的解决方案,我期望以下属性:

  • 流式处理,不需要额外的内存,除了序列的底层存储。在这种情况下,我们分配一个新的 []V 并忽略任何支持迭代器的后备存储。
  • 不依赖于 "json" 包。也就是说,这个想法应该适用于 XML、CBOR、YAML 等,而不需要强制 iter.Seq 对这些都有特殊知识。
flvlnr44

flvlnr447#

迭代器,就像我们现在(实验性地)拥有的它们(即 iter.Seq ),是单向和只读的(C中称为“输入迭代器”),这就是为什么我只提到了 Marshal 而不是 Unmarshal。
要解组,我们需要类似于 C
的 Output Iterator 的东西,然后我们可以使用一个类似于 back_insert_iterator 的(我们可能会称之为“追加”而不是“后插入”)。一旦我们有了这个,我可以想象出类似于 json.Decoder.UseNumber() 的东西来定义自定义的解组数组的方式。值得思考,但超出了本票据的范围,我认为。

smtd7mpg

smtd7mpg8#

然后再说一次,使Marshall和Unmarshal不对称也不是很好。例如,让我们看看由protoc为grpc/protobuf生成的类型。当我发起请求时,我希望能够传递一个seq.Iter字段给repeated字段。但是当我接收一个请求时,切片更方便,因为我可以在遍历它之前检查它的长度。此外,改变这一点将是一个破坏性的改变。
这是否意味着protoc需要为发送和接收生成每个消息的两个不同版本?这似乎并不可取。如果我只想将(部分)传入的消息转发到另一个服务,我不想先转换它。

1cklez4t

1cklez4t9#

原则上,可以返回一个在消耗数组的流式方式中关闭的 iter.Seq : https://go.dev/play/p/6G4GTrHMNo4?v=gotip
但当然,第一轮迭代器只支持不可错的迭代器,因此任何尝试延迟进行 JSON 解析都可能导致无法处理的错误。
原则上,它可以先调用 json.Valid 来找到可能错误的子集,然后再开始迭代,但这会牺牲两次遍历令牌。即使这样还不够,因为语法上有效的值内部可能与类型 V 不兼容。据我所知,唯一解决这个问题的方法是首先进行完整的解析,然后将结果缓存到内存中以便在迭代过程中返回。这样做可能(但不一定)相对于在迭代过程中再次解析更有优势。🤷‍♂️
对我来说,这只是不支持可错迭代器的一个自然后果:对于可以动态失败的事情,不适合使用迭代器抽象,而 JSON 解析可以动态失败。
(我承认这种替代方法也是针对 JSON 的特定方法,因此无法在多种序列化格式上泛化,但我认为在考虑任何除 JSON 之外的格式之前,无法处理错误就使这种方法失去了资格。)

相关问题