Go encoding/json库
JSON在网络上广泛使用,是一种基于文本的数据传输方式。在本集中,我们将与 Daniel Marti 一起探索 Go 的 encoding/json 包
和其他包。
本篇内容是根据2020年7月份[#141 {“encoding”:“json”}](https://changelog.com/gotime/141 “#141 {“encoding”:“json”}”)音频录制内容的整理与翻译
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer: 大家好,欢迎来到 Go Time!我是 Mat Ryer,今天我们要聊聊 JSON。你可能会觉得这是最无聊的一期节目,但我向你保证,绝对不会!今天和我一起的是 Johnny Boursiquot。你好,Johnny!
Johnny Boursiquot: 你好,Mat。
Mat Ryer: 这一周过得还好吗?
Johnny Boursiquot: 嗯……我们即将讨论 JSON,所以我不知道……我们拭目以待吧。
Mat Ryer: (笑) 我们拭目以待。不过别担心,今天我们有一位非常棒的嘉宾……他是 Go 语言的高产贡献者,你可能见过他的名字---
Daniel Martí。你好,Daniel。
Daniel Martí: 嗨,很高兴来到这里。
Mat Ryer: 欢迎来到节目。很高兴你能来。你这一周过得怎么样?
Daniel Martí: 还不错。我们在英国的气温差点达到了 20 度,这很不错……不过几周前夏天就结束了。它只持续了一个周末。
Mat Ryer: 是的。这周晚些时候还会有一些热浪,所以请大家拭目以待……我也不知道为什么我说得像个新闻主播……(笑声) 我只是试图表现得像个正常人,但有时我觉得这很难。
好吧,那我们开始吧……先对初学者快速介绍一下,万一有人真的完全不熟悉---
JSON 是什么?你是这么念的吗?你说 JSON,还是有别的发音?
Daniel Martí: 我就说 JSON,如果我要向一个小孩子解释,我会说这是一种表示数据的方式……不过一开始我就失败了,这计划显然不适合小孩。
Mat Ryer: 不,小孩能理解。我们假设这是个非常聪明的小孩。
Daniel Martí: 好吧。这是一种非常通用的数据表示方式。不管接收方是谁,他们大概率都能读懂这些数据。
Mat Ryer: 对。它的全称是 JavaScript Object Notation,起源于 JavaScript……但后来它在很多领域非常有用---
几乎每种语言现在都支持 JSON。
Daniel Martí: 它几乎无处不在。几乎所有现代编程语言都必须支持 JSON,因为你无法避免。而且你的电脑---
你可能看不到,但它肯定在某个层面上运行着 JSON。
Mat Ryer: 对。它像一个对象,有字段,这些字段有一些类型……这些类型也是我们在 Go 中常见的类型,比如字符串、数字、布尔值。还有其他对象、数组之类的东西。我想这差不多就是全部了。为什么它在网络上如此流行?我觉得它对网络技术而言非常完美。
Daniel Martí: 我会说这源于浏览器和现代网络的成功……突然间,HTTP、HTML、CSS、JavaScript 和 JSON 这些技术让所有人都感到意外。最初大家觉得它们只是玩具,但现在人们用它们创建了真正的公司……而 JSON 已经有了太大的势头,我认为它不太可能被其他东西取代。
Mat Ryer: 这很有意思。
Johnny Boursiquot: Mat,等等……我想补充一下。我很喜欢你刚才说的,Daniel……我觉得还有另一个原因---
至少个人认为---
为什么 JSON 能迅速流行起来。对我来说,主要是因为它不是 XML。在 JSON 之前,如果你想要一种可以替换的格式,与其他系统交互,XML 是默认的选择。然后我们围绕 XML 解析、XSLT、模板、样式表等创建了一个完整的生态系统。回想起来,这些技术在当时非常出色,但也真的很难用。你真的需要依赖机器生成 XML,手动编辑 XML,尤其是很大、很复杂的文档,那真是令人头疼。
于是 JSON 出现了,它非常简单,人类可读性很强……这就像一股清新的空气。所以,确实如你所说,它与 HTML、JavaScript、CSS 的兴起密不可分,从系统和数据交换的角度来看,它也是一场革命。
Mat Ryer: 对,而且它比 XML 简单得多……因为在 XML 中你可以对结构做一些奇怪的操作,比如让元素并列在一起,处理起来非常复杂。而在 JSON 中,这种情况不会发生,它的结构更紧凑,我觉得这也帮助了它的流行。
在使用 JSON 时,有没有什么需要注意的陷阱?有没有初学者需要特别留意的地方?我想到的一点是,在 Go 中如果你有一个 time.Time
类型,要把时间表示为 JSON,它会把时间转换成字符串,对不对?
Daniel Martí: 是的。其实我不太确定默认行为是什么,因为我通常会写自定义代码来处理 JSON 中的时间……
Mat Ryer: 真的吗?为什么?
Daniel Martí: 大多数情况下,人们希望时间戳是特定格式的……所以他们会写代码来处理。我其实不太记得默认行为是什么,但对,JSON 没有时间戳类型,所以它最终会变成字符串。
Mat Ryer: 是的。只要解析方也能理解这个格式,并能够处理它,那就没问题……但这确实是一个有趣的点---
JSON 只有一些基础类型,有时你需要做一些魔法处理,把你特定的数据转化为能在这种文本格式中工作的东西。
另一个有点奇怪的地方是---
默认情况下,所有的数字都是 float64
类型。如果你用 Go 中的 map[string]interface{}
来解析 JSON,它会工作;它会像对象一样填充这个 map。但如果其中有数字,它不确定是浮点数还是整数……所以它会使用 float64
,因为这是最通用的类型。我第一次在 Go 中处理 JSON 时觉得这很奇怪。
Daniel Martí: 其实,数字这点非常有趣。我认为 JSON 本来可以有两种不同的处理方式。一种是像 Go 这样,区分整数和浮点数,并定义它们的位数。比如,在 Go 中,你可以有 int64
和 float64
。这样做的好处是更严格,如果你想用其中一种,它会一直保持不变,不会有精度丢失。
但另一方面,如果你只是说它是一个数字,那么它可以支持任意精度的数字,也就是大数,Go 也通过一个不同的包支持这种大数。
Mat Ryer: 说到 encoding/json
包,顺便提一句,Daniel,你其实是 encoding/json
包的共同维护者,对吧?
Daniel Martí: 是的,没错。但在我们继续之前,我得提前说一下,我注意到我的笔记本内存使用率在过去 15 分钟内逐渐上升……我不确定这是 Zoom 的问题,还是我录音软件的问题,但我觉得我的笔记本大概会在 10 分钟内崩溃……所以如果发生这种情况,请大家知道这是为什么。
Mat Ryer: 这挺刺激的……就像有个炸弹要爆炸,我们正在等待。
Daniel Martí: 是啊。它一开始是 30%,现在是 92%……大概还有五分钟的时间。我也不知道发生了什么。
Mat Ryer: 好吧。如果你突然消失,我们就知道是怎么回事了。我只希望不要在 Johnny 说完话后你立刻消失,那样他会觉得很受伤的。
Daniel Martí: 抱歉。你刚才问我是不是共同维护 encoding/json
包,答案是是的。我已经帮忙大概 3-4 年了……JSON 确实有活跃的维护者,包括 Russ、Joe 和 Brad。我最初只是帮忙修一些小 bug 和做一些小优化;但随着时间的推移,这些人都很忙,所以现在我做的工作几乎和他们一样多……一方面这很有成就感,因为这是一个非常有用的包,有很多人在用。但另一方面,压力也很大。
Mat Ryer: 是吗?为什么?
Daniel Martí: 一方面是因为几乎有成百上千的人使用这个包……
Mat Ryer: 哦,他的内存用光了。
Johnny Boursiquot: 就这样……(笑)
Mat Ryer: 是的。
Johnny Boursiquot: 真的是这样。
Mat Ryer: Linux。
Mat Ryer: Daniel,维护 encoding/json
包的挑战是什么?为什么会有压力?
Daniel Martí: 我觉得这非常有成就感,因为你一修复一个 bug,马上就会有很多人感到高兴……显然,很多人非常关心 JSON 包的运行速度。但另一方面,因为它有如此多的用户,如果你搞砸了,人们会非常生气。而且还有一个叫做 Go 1 兼容性保证的东西,这意味着如果你的程序能在 Go 1.0 上运行,它也应该能在 Go 1.2、Go 1.3 等版本上运行。
Mat Ryer: 有意思。如果最初的 JSON 版本里有个错误,这个错误也要继续支持吗?
Daniel Martí: 这是一个很好的问题。我觉得有多种理解方式。最严格的理解是只有那些文档上明确写明的行为需要保持不变。所以如果你写的代码依赖于某些实现细节,那这些细节未来是允许发生变化的……这通常是我对它的理解。但更保守的理解是“几乎所有你做的事情,只要是合理的,即使没有写在文档上,也应该继续工作,因为我们不想破坏用户的程序。”在这两者之间,团队需要找到一个中间点。
Mat Ryer: 哇,这确实是个微妙的平衡。但这个承诺非常重要,因为正是它让我们能够依赖 Go 的未来版本,确保我们的系统依然可以正常运行。对我来说,这也是 Go 吸引力之一,所以我非常感谢你们的努力,因为我知道这并不容易。
我本以为 JSON 包在最初编写完成后就差不多了,它已经在工作了。那么它还需要维护什么呢?
Daniel Martí: 这是个好问题,我觉得这和 JSON 的灵活性有关……因为 JSON 没有模式,它只是某种结构中的数据。你可以用它做很多事情,人们确实也用它做了很多奇怪的事情……于是他们来到标准库中的 encoding/json
包,希望它能适应他们的工作流。所以他们可能会希望“哦,根据某个字段的值解码某些字段”,或者他们会说“我希望能够流式处理一个非常大的对象,即使它无法全部放入内存”,诸如此类的用例。
所以总是有源源不断的功能请求,还有优化和修复以前更改带来的 bug。
Mat Ryer: 明白了……我猜这就像其他软件一样……你可以改进它,但在改进的过程中也会引入一些问题……不过它是经过良好测试的,对吧?encoding/json
包的测试还是不错的。
Daniel Martí: 是的,大多数情况下我认为它的测试是不错的。
Mat Ryer: 对,这很重要。这让你可以有信心去做更改。你提到你不想打破向后兼容的承诺……单元测试是确保这一点的关键,对吧?
Daniel Martí: 是的。检查你的包是否经过良好测试其实是一门艺术。你可以查看 Go 工具生成的代码覆盖率,但那并不足够……因为你可能覆盖了一行代码,但并没有覆盖其中的所有逻辑……或者你可能没有触发某个会导致 panic 的边界情况。
Mat Ryer: 是的。我总是告诉大家不要在应用代码中追求 100% 的代码覆盖率,因为这可能会让你的测试与实现紧密耦合。这个包是个例外吗?在这个包中追求 100% 的代码覆盖率是否合理?
Daniel Martí: 我会说大多数情况下确实应该尽可能提升覆盖率,因为这个包大部分就是一些带有逻辑的 if-else 语句。但也有一些地方会有 panic,比如一些不应该发生的情况,或者一些边界情况……比如指向 nil 的指针。
Mat Ryer: 有趣的是,JSON 在 Go 的 API 中有些奇怪的地方。你需要传入一个指针,当你想要解码 JSON 数据时,你传入的实际上是指向目标对象的指针,也就是你希望 JSON 解码后存放数据的地方。而且在传入这些指针时有一些有趣的、复杂的规则,对吧?
Daniel Martí: 没错。你可以传入指向任何有效数据的指针……但不能是指向 nil 或 0 的指针,因为那样就无法存储任何数据。所以,它期望的是指向一个结构体的指针,能够存储和解码传入的 JSON 数据。这里有一些规则,比如你传入一个空接口,它会根据数据类型做出猜测。如果看到一个数字,它会假设是 float64
;如果看到一个对象,它会使用 map
。但如果你传入的是一个有具体字段类型的结构体,它会按照你的定义解码,如果类型不匹配,它就会返回错误。
Johnny Boursiquot: 这个包其实内置了一些智能功能,我通常挺喜欢的。最近我在做代码审查时,发现有位开发者创建了一个结构体,并给字段添加了 JSON 标签,但并没有要解码的数据。在这种情况下,除非你真的预料到需要将不同的字段名映射到结构体中,否则其实不需要给字段加标签。JSON 包会按照你定义的字段名输出 JSON。所以你不需要特别去加这些标签。这个包里确实有很多智能功能,我个人挺欣赏的,后面我们会深入探讨……
我喜欢使用标准库,可能是因为工作的性质吧,我倾向于不去寻找第三方包来解决问题,如果标准库中有相应的功能,即使有点难用,或者性能稍差。我想如果你在 Go 社区待过一段时间,你可能会遇到一些社区构建的第三方包,它们在 JSON 解析、编码和解码方面做了不同的权衡……很多第三方包似乎都专注于速度和性能。
Dave Cheney 也做过相关实验,并发布了结果。希望他今天也在场能讨论这个话题。大家都想要速度快的工具,对吧?“哦,这个更快,那我就该用它。”但实际上,这里面也有权衡。你不能仅仅因为它快就选它。我很想知道你的看法,为什么会选择一种实现而不是另一种?在这方面你做了哪些权衡?
Daniel Martí: 我觉得这个话题是讨论的核心,因为对于很多人来说,标准库中的 JSON 解码器已经是最快的解码器之一,但有些人可能没有意识到这个速度背后的权衡……对于所有这些第三方 JSON 实现,我的感觉是复杂的。它们中的一些确实有意义,比如某些场景下你确实需要最高性能,可能这是系统的瓶颈,你不介意 Go 为你生成代码来自动编写一个 JSON 解码器。在这种情况下,你可以使用像 easyjson
这样的包,它挺受欢迎……但权衡在于你需要运行 go generate
,而且你的二进制文件会变大很多,因为它包含了很多额外的代码。但这些额外的代码会直接编码所有的逻辑,不使用反射,也没有额外的解引用操作。所以我认为在某些使用场景下,这种做法是合理的。
Mat Ryer: 我喜欢你这样分析这个问题。你说“也许这是你系统的瓶颈……” 这就是关键所在---
一旦你发现这确实是需要改进的地方,才值得去承受额外的复杂性,比如学习新的 API 或者处理更多的复杂代码。我喜欢这种方式,因为……其实我们应该一直这样做。正如 Johnny 提到的,我们可能有点过于执着于“为什么不选择最快的东西?”而答案可能是“标准库已经足够好了。”
有哪些包,它们又有什么不同呢?
Daniel Martí: 最近我看到一个有趣的包,但我忘记了它的名字,好像是以某个公司命名的……他们的做法是保留了和标准库相同的 API,所以自称是一个可以直接替换的包。但它的底层实现非常有趣,它没有使用 reflect
包(反射是 encoding/json
性能不佳的主要原因之一),而是直接使用了 unsafe
。权衡在于,使用 unsafe
可以做很多“魔法”,速度很快,但同时也不安全。所以我对它是有些矛盾的。如果告诉人们这是一个可以直接替换的包,他们可能会觉得“哦,我只需要更改一下导入路径,速度就能提高一倍”,但他们没意识到这可能会带来巨大的安全漏洞……
Mat Ryer: 哦,这确实是个问题。
Daniel Martí: ……因为虽然标准库的反射机制本身也使用了 unsafe
,但反射经过了严格的审查和审计,它遵循了 Go 的规则,确保只能设置合法的字段。但如果你直接使用 unsafe
,你就绕过了所有这些限制,完全依赖自己了。
Mat Ryer: 标准库使用反射的原因是,它在某种程度上是动态的,对吧?你并不知道你要解码的 JSON 的结构,尤其是当你解码到 map[string]interface{}
时,你无法提前知道 JSON 的结构……这种灵活性非常强大,但也容易被滥用。你提到的那个使用 unsafe
的方法确实有它的道理,但风险也很大。
我之前在一个命令行工具中用过 JSON,可能有点奇怪,也可能不奇怪。它通过标准输入接收一行行的 JSON,然后输出也是一行行的 JSON。我们有一系列工具可以按不同的方式组合使用,这些工具会接收不同的 JSON 对象,每个对象都在一行上……在这种情况下,使用 JSON 的编码器和解码器非常合适,因为它们可以处理 io.Reader
,不断解码对象,遇到换行符时分隔开每个对象。
所以对于这种设计来说,这种方式非常完美。这些工具在接收到一行 JSON 数据之前不会执行任何操作,然后处理数据,最后输出一行 JSON。当然,你也可以直接使用 marshal
和 unmarshal
函数。它们之间的关键区别是什么呢?
Daniel Martí: 我认为大多数人会说关键区别在于流处理。如果你使用 marshal
或 unmarshal
,你可以从函数签名中看到,它们接收和返回的是字节切片……所以很容易看出,如果你想要解码一块 JSON 数据,你必须将整个 JSON 数据加载到内存中。而如果你使用 decoder
,它接收的是 reader
,你可能会以为“哦,它会流式处理 JSON,这样我就不用把所有数据都加载到内存里了”,但其实并不是这样。我认为这是当前 API 的一个问题,我不会说它是错误的,但它确实有些误导……因为它会将整个 JSON 对象缓冲到内存中,然后再进行解码。这样做的原因是 encoding/json
包优先保证正确性……
举个例子,当你解码到一个 map 时,如果 map 中本来有键 foo,然后你解码了一个新键 bar,最终你会得到包含两个键 foo 和 bar 的 map。它不会直接用一个新的 map 替换原来的 map。这在某些场景下是有用的。
但大多数人只是在解码到一个空值,他们不关心之前有没有数据。所以对于大多数人来说,这个行为是出乎意料的,因为他们不在意这种特性。而 encoding/json
包为了实现这种特性,会对输入做两次扫描,第一遍是为了确保 JSON 语法正确,不会有任何错误。
Mat Ryer: 这确实有道理。我看到过另一个 JSON 实现,它不尝试将 JSON 转换成结构化数据,而是让你可以直接查找特定的键路径。比如你可以说“给我这个 JSON 流或 JSON 字符串中的 author.firstname。”它只是简单地扫描数据,而不去解析所有的字段和数据类型……这是另一种方式。如果你只关心某个特定字段,这种方式会非常快。
Johnny Boursiquot: 我突然回想起 XPath 了。(笑声)
Mat Ryer: 哈哈,对。
Daniel Martí: 你提到的这个点非常好,我差点忘了这个额外的用例……我记得有个库好像叫 json-iterator
,或者类似的名字,挺有名的……它有两个主要的用例。一个是你提到的,只获取某个字段或某个值。如果 JSON 数据很大,你可以跳过不必要的部分,直接查找你需要的内容。另一个用例是,当你不知道数据的具体结构时,它会很有用。因为 encoding/json
包要求你提前知道数据的结构。你可以使用 json.RawMessage
来延迟解码部分 JSON 数据,但这相当于让你自己去做多次解码。
如果你只是想快速查看某个字段,然后根据它的值决定做什么,像这个包那样的实现可能会让你的工作更轻松。但我认为大多数情况下,人们是知道 JSON 数据的结构的。
Mat Ryer: 是的,按照我的经验,最好是你能知道 JSON 数据的结构。不要被那种“我的应用程序可以支持任何数据结构”的想法所诱惑,因为最终你会因此而头疼。RawMessage
实际上是做什么的呢?它只是一个字符串类型,还是字节切片之类的东西?
Daniel Martí: 它实际上就是一个命名的字节切片,实现了 UnmarshalJSON
接口。它的作用就是接受 JSON 数据并存储下来,仅此而已。这个功能非常强大,因为它让你可以自由决定如何处理数据。
不过在我们继续讨论之前,我的内存快满了,我需要暂停录音并重新开始……给我两分钟时间。
Mat Ryer: 好的,没问题。休息一下吧。停一下世界的转动。
Daniel Martí: 我用的这个程序有个有趣的 bug……它似乎随着录音时间的增加,内存占用越来越大。每次保存文件并完全关闭它后,重新打开才会恢复正常……
Mat Ryer: 是的,感觉它好像是在把音频都存到 RAM 里了,是吧?
Daniel Martí: 我猜是的,但我现在显然没有时间去修复它……
Mat Ryer: 那它使用的内存是不是和你保存的文件大小一样,都是那么多?
Daniel Martí: 我保存的 WAV 文件大概有 160 兆,但它用了我 15 GB 的内存,所以我也不知道它在干嘛……抱歉,我又在抱怨了。
Johnny Boursiquot: 嗯,有个问题是来自我们频道的 Jon Calhoun,他提到 Go 1.0 的兼容性承诺。我想所有在生产环境中使用 Go 的开发者都非常看重这个承诺……关于标准库中的 JSON 包,有没有什么你希望能加入其中的,但因为兼容性承诺的限制而无法实现的功能?这些功能有可能会出现在 Go 的下一个版本中,允许打破向后兼容的约束吗?
Daniel Martí: 这是个好问题。我觉得有两类问题我会想修复。第一类是高级 API 改动。我们之前提到过,使用 reader
和 writer
让人误以为它是流式处理,但实际并不是。如果我们修改这些部分,几乎每个使用 JSON 的程序都会被打破,所以这在 v1 版本中是完全不可能的。
另一类是一些微妙的 bug 和历史遗留问题,它们已经成为事实上的行为,很多人依赖于此。举个例子,有一个类型叫 json.Number
,它可以方便地支持大数,实际上是一个字符串类型。所以当你解码一个像 50 位长的数字时,它不会关心是否超出了 int32
或 int64
的范围,因为它会保持原样,作为字符串处理。这是处理大数的最简单方法。
但 json.Number
的实现中,如果输入的 JSON 是一个包含数字的字符串,它也会接受,尽管这不是一个 JSON 数字。这并不是文档中提到的行为,文档只说它解码数字,没有提到字符串。所以我们尝试修复这个问题---
我记得是别人提出来的修复,我负责审查---
结果很多人说“这破坏了我的代码!”我告诉他们“只需三行代码就能修复,这非常简单,我还给你们提供了 Playground 链接。”但他们还是说“不,不,不,这打破了生产环境,违反了兼容性承诺。”
Mat Ryer: 哦,这确实是个灰色地带。你不应该这样使用它,但既然它有效,那该怎么办呢?确实棘手。
Daniel Martí: 是的,这很难处理。你必须评估“我是不是打破了太多用户的代码?多少用户算是‘太多’呢?”我不知道大家是如何使用 JSON 包的。我也许可以查看一些开源项目,看看他们的代码是什么样的,但这只能触及表面。我猜用 JSON 最多的 Go 代码很可能不是开源的……所以很难判断某些改动是否可行。
Mat Ryer: 对,所以你现在有一个 encoding/json
的 v2 版本草案,对吗?
Daniel Martí: 嗯。
Mat Ryer: 这个文档是为了什么?是你心目中的完美设计,如果可以的话你会想要的东西吗?
Daniel Martí: 目前来说,这只是我用来整理自己想法的一个文档……因为我维护 JSON 已经几年了,积累了很多类似“我没法修复这个”的小问题。如果我试图修复某个问题,人们可能会不高兴。我也不能碰这个功能,因为它被 API 限制住了……所以我整理了我的所有想法---
至少是我能记住的那些---
但我还没到设计新 API 的程度,因为在某种程度上,我觉得这有点徒劳……因为即使我设计了一个新的 JSON API,它也不会取代现有的 API。据我所知,目前也没有计划推出标准库包的第二版本。我可能会在外部写点东西,但我并不想再增加 Go 中已有的 50 个 JSON 包的复杂性。
Mat Ryer: 嗯……我想知道有什么合理的办法,是否可以在 JSON 包中添加一些新的方法……
Daniel Martí: 是的,这确实是个好点子。有一些 bug……比如,有一个我会说影响了大多数代码库的 bug,就是标准的---
你有一个 HTTP 端点,body 是 JSON,所以你想要解码它……你会做的事情是获取 r.body
,然后用 json.NewDecoder().Decode()
来解码到某个结构体中。如果你这么做了,那是有问题的。
Mat Ryer: 我刚开始学习 Go……你说有问题?![笑声] 请告诉我为什么。
Daniel Martí: 大约一年前,Joe(其中一位维护者)发现了这个问题……bug 是解码器本应适用于 JSON 值的流。比如当你用 JSON 标志运行 go test
时,它会给你一个换行分隔的 JSON 值流、JSON 对象流。
Mat Ryer: 是的,我在那些工具中使用它时,正是这样的。
Daniel Martí: 是的,正是如此。
Mat Ryer: 某种程度上,它确实是流式的……它读取 reader
。对于每个对象,它会缓冲它,我猜,但它会丢弃之前的那个对象,对吧?
Daniel Martí: 是的。
Mat Ryer: ……下一次时。对。所以从某种意义上来说,它是流式的。
Johnny Boursiquot: 它看起来像是在流式处理,但实际上它并不是这么做的。
Mat Ryer: 但它每次只处理一个对象,你可以说这是一个流;只是如果这是一个非常大的对象,那么---
Daniel Martí: 确实如此。
Johnny Boursiquot: 你就麻烦了。
Mat Ryer: 你可能会。
Daniel Martí: 所以我会说,假设你的值是小的;它并没有考虑到你可能会有一个 200 MB 的 JSON 对象。如果你有,它就会说,“哎呀!我只能缓冲这个。”
Mat Ryer: 嗯。比如说,你现在的机器就做不了这个,因为内存不够。
Johnny Boursiquot: [笑声]
Daniel Martí: 如果你想让我离开,你可以直接说。[笑声]
Mat Ryer: 拜托别走。你八分钟后就得走了。
Daniel Martí: 目前我还剩 30% 的内存,还能撑 7-8 分钟。
Mat Ryer: 我在想,这是不是和你说的多少话有关。当你说话时,它肯定会消耗更多的 RAM……[笑声]
Daniel Martí: 好吧,让我对着麦克风大喊一声,看 RAM 是不是会增加。
Mat Ryer: 是的,我知道它在做什么---
它把音频存储成 JSON 了,不是吗?
Daniel Martí: 也许吧……也许每一波音频都是一个 JSON 对象,正在某个地方被流式传输。
Mat Ryer: 对。它并不是每种数据的完美格式,对吧?有时候二进制数据会更好。
Johnny Boursiquot: 其实,这引出了一个很好的话题,因为是的,JSON 很棒,它是人类可读的……但大多数时候,我们是让机器相互通信。所以在某些情况下,为了传输和存储的效率,选择二进制格式而不是基于文本的 JSON 可能更有意义……尤其是当它是一种数据流,或者你正在接受大量的信息时……除非你作为开发人员在本地调试,否则你不可能通过大量的 JSON 数据,试图阅读并利用其可读性。那么,什么时候可以给自己一个理由,不必为了使用 JSON 而使用 JSON,仅仅因为大家都在用 JSON?有哪些好的标准可以帮助你做出不使用 JSON 的决定?
Daniel Martí: 这是个好问题。在回答这个问题之前,我想简要提一下刚才提到的那个 bug。
Johnny Boursiquot: 对,我们还没猜出来。
Mat Ryer: 抱歉,这都是我蠢的错。别担心,这只是编辑们的更多工作。拍一下!好吧……[笑声] 你只要拍拍手就能修复它。
促销广告: “你遇到过这种情况吗?现在介绍---
拍手器。跟着音乐拍手!很简单……拍手开,拍手关……!拍手器。”
Mat Ryer: 所以 Daniel,告诉我们 r.body 解码时的那个 bug 是什么?
Daniel Martí: 这个 bug 是你只解码了一个对象,但如果 body 中包含了多个值,用多个换行符分隔呢?你不会注意到,你只会在解码第一个对象后立即关闭字节流。所以即使你不支持它,如果客户端想要发送三个对象,用换行符分隔,你会使用第一个对象,忽略后两个,这很可能不是你想要的。你要么应该抛出一个错误,要么应该使用所有数据。
Mat Ryer: 是啊,这很有趣。如果你到达流的末尾,当你尝试使用解码器解码时会发生什么?
Daniel Martí: 嗯,我想它会返回 EOF 错误,或者类似的东西……
Mat Ryer: 是的,你会得到 EOF……嗯。所以你可以通过一个循环来支持它,一直循环并解码,但我不知道,这确实有点奇怪……当你想到 JSON 中的数组时,数组可以包含许多对象,这往往是有效载荷的一部分,而这实际上仍然是可行的,不会触发这个 bug。这只是针对换行符分隔的 JSON 对象。
Daniel Martí: 是的。在这种情况下,你可以很容易地修复代码。你只需在解码结束时添加一个检查,看看解码器是否还有更多的令牌需要解码,如果有,就抛出错误。你可以这么做。但问题是,人们必须记住去做这个检查。最初,没有人知道需要这么做……所以我会说这是一个复杂的 API 设计,因为它很容易被误用。
Mat Ryer: 是的,但坦白说,我不知道有哪个 API 会让你这样发送多行 JSON。我可能错了,但我好像没见过。
Daniel Martí: 是的。如果有这样的 API,你可能会正确地实现它。我同意,这在现实生活中可能不是个大问题,但它仍然是一个存在的边缘用例,很少有人考虑到,技术上来说这也是个 bug。
Mat Ryer: 是的。我喜欢那些为我们维护这些包的人……这真的很难,你必须关心所有细节……但这很好,因为这意味着我们其他人不用去操心这些问题了。
Daniel Martí: 回到 Johnny 的问题,他问“什么时候应该选择 JSON 或其他纯文本格式,什么时候应该选择二进制格式?”我认为在这个问题上有多种观点,但我认为大多数程序员的共识是,如果这是人类要处理的东西,比如调试、查看或使用,或者是人类要写的东西,你很可能会选择纯文本格式,比如 JSON 或 YAML 等。但如果你需要高效处理数据,可能是因为数据量非常大,或者仅仅是机器之间的通信,那么你可能会考虑使用更高效的二进制格式,它占用更少的空间,等等。
Mat Ryer: 是的,我认为这个论点也适用于 gRPC 和 JSON API 的争论……这是同样的问题,你可能有很好的理由需要一个低级的二进制格式;你希望它尽可能高效。但这样你就损害了开发者的友好程度。它在构建时很不错,但即使在使用时,如果你想探索发生了什么,你有时可以在浏览器的网络选项卡中查看 HTTP 请求,看看 JSON 的内容……我发现这非常有用,尤其是在开发时……所以,如果你使用 gRPC 之类的东西,你可能需要额外的工具来做到这一点,我想……
Daniel Martí: 我完全同意。我会说默认情况下使用纯文本格式,只有在仔细考虑后才选择二进制格式。或者更好的是,支持两者。很多构建 gRPC 服务的人会在其上添加一个 REST 网关,这样客户端可以选择使用哪一个。也许机器使用 gRPC,而调试时的人类会使用带有 JSON 的 REST。
Mat Ryer: 是的,绝对如此。我认为这是一个合理的做法。但我同意,首先使用 JSON,因为一开始它是最容易使用的。也许这就是你所需要的一切。
Johnny Boursiquot: 你是在说这是一个 YAGNI(你不会需要它的)情况吗?
(译者注: YAGNI 是 “You Aren’t Gonna Need It” 的缩写,翻译为“你不会需要它”。这是软件开发中的一种设计原则,强调在编写代码时,应该避免过度设计或实现尚未需要的功能。)
Mat Ryer: YAGNI……!
Daniel Martí: 什么是 YAGNI?
Johnny Boursiquot: 哦,你还没有被 Ruby 生态系统洗礼。YAGNI 是 Ruby 生态系统中一个非常受欢迎的框架作者推广的概念。YAGNI 代表“你不会需要它”(You Ain’t Gonna Need It)。[笑声]
Daniel Martí: 我记下了。
Mat Ryer: 很好,不是吗?
Johnny Boursiquot: 是的,确实如此。
Mat Ryer: 我们需要这个。
Johnny Boursiquot: 我偶尔还会拿出来用。
Daniel Martí: 但我确实认为我们可能漏掉了一个点,那就是定义你的数据模型……我认为这是 JSON 最短板的地方,也是它最让人头疼的地方。这也是 JSON Schema 之类的东西出现的原因……但我不会说它们是非常好的解决方案。它们大多试图将 20 年前的 XML 解决方案移植到 JSON。我认为这不是一个很好的方法。我认为像 protobuf 和 gRPC 这样的模式语言更好……所以你必须在“我要使用简单的 JSON 并快速开始”与“我要使用一个能够让我正确定义类型的模式语言”之间做出权衡。
Mat Ryer: 是的,这可能也取决于用例。在某些情况下,如果你处理的是通用数据,而你不知道这些数据的结构……这种情况确实会发生。我确实参与过一些项目,它们是某种平台,你无法提前知道数据的形状……那么这确实会影响你的选择。
不过 JSON 的好处在于,你可以随时向其中添加字段,不是吗?你可以随时添加字段,之前的代码也会继续工作……因为在 Go 的结构体中,如果结构体缺少字段,而 JSON 中存在该字段,默认情况下它会被忽略,对吧?
Daniel Martí: 是的,这是个很好的点。JSON 确实很容易实现向后兼容性,只要你愿意维护之前的字段,等等。我认为大多数格式都是这样的。例如,protobuf 也是如此,只要你在最后添加带有新 ID 的字段就可以了,但它不如 JSON 直观。我承认这确实增加了一些复杂性。
Johnny Boursiquot: 但它能保持我的旧代码继续工作,所以……我觉得这是一个我愿意接受的权衡。
Mat Ryer: 现有的实现中还有什么其他提升效率的可能?比如在解码 JSON 的过程中减少内存分配?
Daniel Martí: 是的,这确实是我的大部分工作所在……因为如我之前所说,我不想仅仅写一个新包,增加 Go 新开发者在 20 个 JSON 包之间做选择的困扰。所以我确实对内部做了一些改动,比如避免重复工作,或者缓存一些东西,或者去掉某些平衡检查之类的东西……我记得是在 Go 1.10 到 Go 1.13 之间,如果你主要使用结构体,而不是 map,JSON 解码速度提高了大约 30% 到 50%,这蛮不错的……
Mat Ryer: 哇……
Daniel Martí: 但你得明白,基点其实很低……
Johnny Boursiquot: [笑声]
Mat Ryer: 你不用这么说……只关注改进部分就好了。[笑声]
Johnny Boursiquot: 是的,没错。
Mat Ryer: 提速了 30%!
Daniel Martí: 但我也想说,那些声称比 encoding/json
快十倍的包---
他们的基准测试可能做得很早,现在可能只快四倍左右。
Mat Ryer: 有趣……嗯。
Daniel Martí: 我确实认为还有更多工作可以做,但所有的低垂果实已经被摘走了,主要是我和其他几个人……不过还有一些事情可以做,而不会改变 API 或破坏用户。我认为最大的一个---
这与 Dave 的工作有关---
就是重写标记器。标记器负责读取字节并识别“哦,这是一个字符串,这是一个左大括号,这是一个逗号”,等等。
Mat Ryer: 嗯。那么,这个过程是边解析边构建数据结构,还是它以某种方式在某个中间数据结构中描述解析的结构,如果你能明白我的意思?
Daniel Martí: 一种方法确实是构建某种树结构,比如当你解析 Go 文件时,你会得到 Go 代码的语法树……
Mat Ryer: 是的。
Daniel Martí: 它并不是这样做的。它的工作方式是先对一个值(例如一个 JSON 对象)进行标记化处理;它开始遍历字节,逐个识别 “标记,标记,标记”,但它会忘记这些标记,因为这是第一次遍历。它只是想检查 JSON 是否有效。当它到达末尾时,例如遇到第一个大括号的闭合括号,它会回到缓冲区的开头,然后再次进行标记化处理……但这次当它遇到“打开对象”时,它会真正开始在目标值中创建一个对象。如果它看到一个字符串,它会尝试将该字符串解码为当前的目标值,依此类推。
Mat Ryer: 这很有趣。我很惊讶它会这么做……因为你会认为它只需要做一次,不是吗?为什么它要这样做?
Daniel Martí: 它这样做两次的原因是为了防止部分解码。比如说我给你一个包含 9,000 个元素的数组,但没有闭合标记,这就不是有效的 JSON。那么你会怎么办?你要花时间把这 9,000 个元素全部解码到目标值中吗?如果目标值之前有数据,那你可能会破坏它。对于数组来说,这可能没什么意义,但想象一下,如果是一个 map 呢……
Mat Ryer: 对。
Daniel Martí: 所以你不想这么做,至少在 JSON 包中不会这么做。它更注重正确性,所以它会先确保 JSON 是有效的,然后再进行解码。
Mat Ryer: 哦,真有趣。嗯。
Daniel Martí: 我觉得你可以说它应该保持一棵树而不是字节,这样可能会更高效一些,因为不会重复工作,但我会说这样可能会增加分配对象的成本。
Mat Ryer: 我的想法是,我会只遍历一次,不用太担心正确性,做完所有的工作,然后如果最后发现有错误,那就得到一个错误。但你可能要等到最后。我感觉这更像是一种乐观的设计。你觉得这是个糟糕的设计吗?
Daniel Martí: 我不确定。我大概是 50/50。我觉得两种情况都是合理的。我认为当前的 API 尽量保持简单。它基本上只有一个入口点,就是 decoder.Decode()
,而 Unmarshal()
只是它的一个包装器……因为如果你看 Unmarshal()
,它只是为你处理了底层的事情。
Mat Ryer: 哦,不是反过来的吗?我以为解码器会用---
Johnny Boursiquot: 你以为解码器用了 Marshal()
?
Mat Ryer: 是啊,或者 Unmarshal()
。
Daniel Martí: 解码器的好处在于它保留了一些东西以便后续重用。如果解码器使用 Marshal()
,那么 Marshal()
就不会有解码器对象来重用这些东西了。
Mat Ryer: 对,明白了。嗯……很酷。很酷。当然,这些代码都是开源的,所以如果我们真的想了解它是如何工作的,我们可以去阅读代码。
Daniel Martí: 是的,但我会说不要把这些代码和 API 当作典型的 Go 代码来看,因为很多东西都是十多年前写的。而且不仅仅是我,很多其他人也动过手,所以它现在有点像个僵尸了。
Johnny Boursiquot: 你刚提到的这个点非常好,因为我们 Go 社区的很多老成员经常告诉新手:“去读标准库吧,这是写 Go 代码的一个很好的例子”,但这并不总是对的。[笑] 我们从那时起学到了很多东西,有些做法是该避免的,有些是最佳实践,还有一些是更符合 Go 语言惯例的做法……而 encoding/json
包可能并不是我们今天所取得进步的最佳代表。
Mat Ryer: 是的,另一个问题是它包含了很多优化,而它确实应该这样做……但这也可能带来代码复杂性和“丑陋性”的代价……不过你不会介意,因为这是一个非常重要的地方。但确实,一个初级开发者可能会去看这些代码,看到一些东西后觉得“哦,这是该怎么做的”,但实际上你可能不想这么做。
Daniel Martí: 我完全同意。
Johnny Boursiquot: 我们一定要确保在时间用完前讨论到一些不受欢迎的观点……
Daniel Martí: 我不受欢迎的观点是,encoding/json
已经足够快了……
Johnny Boursiquot: [笑] 哦,拜托……!
Mat Ryer: 哇!你可是负责让它变快的人…… [笑声]
Daniel Martí: 嗯,我是说总体上,当然大多数情况是这样的……但这可能不适用于那 1% 的人,比如处理 20GB 的 JSON 数据的人……但大多数人不会遇到这种情况。我的观点回到权衡上。是的,如果你选择另一个包,你可能会得到 2 倍、3 倍,甚至可能 4 倍的提升,但在那个时候还坚持使用 JSON 真的值得吗?那些不得不使用 JSON 的人和那些需要处理大量数据的人之间的重叠非常小……因为那些需要处理大量数据的人通常会选择更好的格式,这些格式的解码速度更快。
Mat Ryer: 我觉得这个论点非常合理……嗯,这个观点对我来说并不“不得人心”。我觉得你说得很好。
Daniel Martí: 你会认为那些抱怨 encoding/json
速度太慢的人会不同意……
Johnny Boursiquot: [笑]
Mat Ryer: 当然了,那是因为我们给了他们基准测试工具。我不明白你还能期待什么。当然了。
Daniel Martí: 我收回我的话。[笑声]
Mat Ryer: 好吧,Daniel,感谢你来参加我们的节目并花时间和我们交流。非常棒。你一定要再来。
Daniel Martí: 很荣幸。
Mat Ryer: 是的,非常感谢。感谢大家的收听,我们下次再见。
Daniel Martí: 顺便说一下,我找到了那个 bug。
Mat Ryer: 哦,你找到了?
Daniel Martí: 是的。如果我看我的录音程序,它会持续占用更多的内存……但如果我切换到另一个窗口,它就不会了。
Mat Ryer: 就像量子现象一样。
Daniel Martí: 它停止增长了。所以我想是 UI 的问题。UI 不断显示我的声音波形,但可能将整个 UI 保存在内存中……而当我切换视图时,它停止渲染,然后就不再占用更多内存了。
Mat Ryer: 所以它在你看着它的时候才会这样。别看它!
Daniel Martí: 我现在正在看,它增长到了 31%、32%……然后我停止看它,它就不再增长了。
Mat Ryer: 施罗丁格的猫文件。
Johnny Boursiquot: [笑] 哦,天啊……
Mat Ryer: 是的,一旦它的行为就改变了。哦,这太奇怪了……你绝对不会想到要去检查这个,对吧?这就是那种经典的计算机 bug。这正是发生的事情……!
Daniel Martí: 显然,当我做五秒的录音时,时间不够长,没注意到内存的异常。
Mat Ryer: 所以实际上,如果你最小化窗口或者把它切换到另一个屏幕,它就不再这样了吗?内存会跳回原来的水平吗?
Daniel Martí: 不,它只是保持在那里。所以在第二段中,它攀升到了 30%,然后我最小化了窗口,它就停在那里了。
Mat Ryer: 对。你只是想“我不想再为这个烦心了。我不想看它”,然后它就正常了……你找到了原因。
Daniel Martí: 如果你看到我在抬头,那是我在检查内存使用情况,并祈祷它不会崩溃……[笑声] 但再次抱歉。
Mat Ryer: 好吧,你找到问题了。
Johnny Boursiquot: 太棒了。
Mat Ryer: Daniel,你一定要再回来,帮我们调试更多的技术小鬼。
Daniel Martí: 哦,我的天啊……哦,我的天啊……不,拜托。这次真的太有压力了。[笑声]
相关文章:
Go encoding/json库
JSON在网络上广泛使用,是一种基于文本的数据传输方式。在本集中,我们将与 Daniel Marti 一起探索 Go 的 encoding/json 包和其他包。 本篇内容是根据2020年7月份[#141 {“encoding”:“json”}](https://changelog.com/gotime/141 “#141 {“encoding”…...
「实战应用」如何用图表控件LightningChart可视化天气数据?(二)
LightningChart.NET完全由GPU加速,并且性能经过优化,可用于实时显示海量数据-超过10亿个数据点。 LightningChart包括广泛的2D,高级3D,Polar,Smith,3D饼/甜甜圈,地理地图和GIS图表以及适用于科学…...
苹果瑕疵数据集苹果质量数据集YOLO格式VOC格式 深度学习 目标检测 数据集
一、数据集概述 数据集名称:2类苹果图像数据集 数据集包含两类样本:正常苹果和有瑕疵的苹果。正常苹果样本代表完好的苹果,而有瑕疵的苹果样本代表苹果表面可能存在的损伤、瑕疵或病害。每个样本都经过详细标记和描述,以便训练模…...
旧电脑安装Win11提示“这台电脑当前不满足windows11系统要求”,安装中断。怎么办?
前言 最近有很多小伙伴也获取了LTSC版本的Win11镜像,很大一部分小伙伴安装这个系统也是比较顺利的。 有顺利安装完成的,肯定也有安装不顺利的。这都是很正常的事情,毕竟这个镜像对电脑硬件要求还是挺高的。 有一部分小伙伴在安装Windows11 …...
深入理解QT多线程编程
文章目录 多线程用法QThread类QtConcurrent类QFutureSynchronizer类获取线程信息线程优先级获取线程状态线程局部存储使用线程池监听线程事件Qt是一个跨平台的应用程序开发框架,广泛应用于图形用户界面(GUI)开发。它提供了强大的多线程支持,允许开发者在应用程序中创建和管理…...
React四官方文档总结一UI与交互
代码下载 React官网已经都是函数式组件文档,没有类组件文档,但是还是支持类组件这种写法。 UI 描述 组件 组件 是 React 的核心概念之一,它们是构建用户界面(UI)的基础。React 允许你将标签、CSS 和 JavaScript 组…...
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
文章目录 一、什么是 HTTP?无状态的含义 二、为什么 HTTP 是无状态的?三、Cookie 和 Session 的引入1. Cookie特点:示例: 2. Session特点:示例(Java Servlet): 四、HTTP、Cookie 和 …...
OpenCV视觉分析之运动分析(2)背景减除类:BackgroundSubtractorKNN的使用
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 K-最近邻(K-nearest neighbours, KNN)基于的背景/前景分割算法。 该类实现了如 319中所述的 K-最近邻背景减除。如果前景…...
android黑屏问题记录
近期出现了一个黑屏问题: 仪表显示,主副屏黑的 :原因背光开启太晚,导致拍照时候是黑的,太晚的原因是绘制进程出现异常导致重启延后了时间,绘制进程crash原因是hwc调用底层库卡住,需更新hwc对应的…...
SIP 业务举例之 Call Forwarding - No Answer(无应答呼叫转移)
目录 1. Call Forwarding - No Answer 简介 2. RFC5359 的 Call Forwarding - No Answer 信令流程 呼转开始 呼转完成 3. Call Forwording - No Answer 过程总结 博主wx:yuanlai45_csdn 博主qq:2777137742 想要 深入学习 5GC IMS 等通信知识(加入 51学通信),或者想要 …...
EFCore pgsql Join 查询
安装包 > Microsoft.EntityFrameworkCore 6.0.35 6.0.35> Microsoft.EntityFrameworkCore.Tools 6.0.35 6.0.35> Npgsql.EntityFrameworkCore.PostgreSQL 6.0.29 6.0.29定义实体等 using Microsoft.EntityFrameworkCore; using Micros…...
力扣80:删除有序数组中重复项
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 示例 1&a…...
等保测评:安全计算环境的详细讲解
安全计算环境是信息安全领域中的一个重要概念,旨在确保在计算过程中数据的机密性、完整性和可用性。随着信息技术的迅猛发展和网络攻击的日益频繁,构建安全计算环境显得尤为重要。本文将详细探讨安全计算环境的主要组成部分、特性及其在信息安全中的作用…...
[Java基础] Lambda 表达式
往期回顾 [Java基础] 基本数据类型 [Java基础] 运算符 [Java基础] 流程控制 [Java基础] 面向对象编程 [Java基础] 集合框架 [Java基础] 输入输出流 [Java基础] 异常处理机制 [Java基础] Lambda 表达式 目录 概述 Lambda 表达式的基本语法 应用场景 并发编程 集合…...
《深入掌握高德地图 API:全面调用指南与最佳实践》
本文 高德地图 API 调用指南引言高德 API 的基础设置注册和获取 API Key 基本 API 调用结构地理编码与逆地理编码地理编码(Geocoding)逆地理编码(Reverse Geocoding)注意事项 路径规划(Direction API)驾车路…...
【功能安全】系统架构设计
目录 01 系统架构介绍 02 投票逻辑架构介绍 03 SIS架构 04 ADS域控制器架构设计 01 系统架构介绍 法规GBT 34590 Part4 part10定义的软件要求、设计和测试子阶段之间的关系(其中的3-7个人建议翻译为初始架构设计更合理 ) 系统架构的作用…...
FPGA实现PCIE视频采集转USB3.0输出,基于XDMA+FT601架构,提供3套工程源码和技术支持
目录 1、前言工程概述免责声明 2、相关方案推荐本博已有的PCIE方案本博已有的USB通信方案 3、PCIE基础知识扫描4、工程详细设计方案工程设计原理框图电脑端视频PCIE视频采集QT上位机XDMA配置及使用XDMA中断模块FDMA图像缓存FT601功能和硬件电路FT601读时序解读FT601写时序解读U…...
基于docker-compose编排部署微服务快速开发框架
1. 规划节点 节点规划,见表1。 表1 节点规划 IP主机名节点10.24.2.10masterdocker-compose节点 2. 基础准备 Docker和Docker Compose已安装完成,将提供的软件包Pig.tar.gz上传至master节点/root目录下并解压。 案例实施 1. 基础环境准备 &#x…...
【Java面试——并发编程——相关类和关键字——Day6】
1. Future 1.1 Future类 Future 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任…...
Android 两种方式实现类似水波扩散效果
两种方式实现类似水波扩散效果,(相比较而言,自定义view的效果更好点,动画实现起来更方便点。) 自定义view实现动画实现 自定义view实现 思路分析:通过canvas画圆,每次改变圆半径和透明度&…...
基于SSM+小程序的垃圾分类管理系统(垃圾2)
👉文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于SSM小程序的垃圾分类管理系统实现了管理员及用户。 1、管理员功能结构图,管理员功能有个人中心,管理员管理,基础数据管理、论坛管理、垃圾信息管理…...
微服务网格Istio介绍
微服务网格Istio 介绍服务注册和发现服务度量灰度发布 Istio核心特性断路器互动1:举个生活中的例子解释断路器互动2:服务降级(提高用户体验效果) 超时重试多路由规则 Istio架构istio组件详解PilotEnvoyCitadelGalleyIngressgatewa…...
【MySQL】视图与用户管理——MySQL
W...Y的主页 😊 代码仓库分享 💕 目录 视图 基本使用 视图规则和限制 用户管理 用户 用户信息 创建用户 删除用户 修改用户密码 数据库的权限 给用户授权 回收权限 视图 视图是一个虚拟表,其内容由查询定义。同真实的表一样&am…...
Go语言中三个输入函数(scanf,scan,scanln)的区别
Go语言中三个输入函数(scanf,scan,scanln)的区别 在 Go 语言中,fmt 包提供了三种输入函数:Scanf、Scan 和 Scanln。这三个函数都是用于从标准输入读取数据并存储到变量中,但是它们在处理输入的方式上有所不同。下面详细解读每个函数的特点和…...
uniapp使用html2canvas时,页面内的image元素模糊
不废话很简单只需要将image改成img就行 改之前 改之后 原因可能是因为uniapp里面的image标签做了某种处理...
华为交换机堆叠
堆叠方式 堆叠卡堆叠: 堆叠卡堆叠又可以分为两种情况: 交换机之间通过专用的堆叠插卡ES5D21VST000及专用的堆叠线缆连接。堆叠卡集成到交换机后面板上,交换机通过集成的堆叠端口及专用的堆叠线缆连接。 业务口堆叠: 业务口堆…...
Spring Boot框架下中小企业设备管理系统开发
1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及,互联网成为人们查找信息的重要场所,二十一世纪是信息的时代,所以信息的管理显得特别重要。因此,使用计算机来管理中小企业设备管理系统的相关信息成为必然。…...
鸿蒙开发融云demo消息未读数
鸿蒙开发融云demo消息未读数 跟着我一步步搭建带界面的融云demo,这次是要显示未读数,未读数有两个,一个是消息列表的未读数,一个是主页消息tab上的未读数。 一、消息列表的未读数 先看下效果图: 关键代码如下&#…...
非对称加密算法(RSA):原理、应用与代码实现
一、引言 在当今数字化时代,信息安全成为了至关重要的议题。非对称加密算法作为保障信息安全的核心技术之一,在数据加密、数字签名、身份验证等领域发挥着不可或缺的作用。其中,RSA 算法以其可靠性、安全性和广泛的适用性,成为了…...
docker部署SQL审核平台Archery
1、概述 Archery 是一个开源的 SQL 审核平台,专为数据库的 SQL 运维和管理而设计,广泛应用于企业的数据库运维工作中。其主要功能是帮助数据库管理员和开发人员实现 SQL 审核、SQL 执行、在线执行、查询、工单管理、权限控制等数据库管理相关的操作。 Archery 的主要功能包括…...
wordpress 2011/app推广方式有哪些
Install Air Conditioning HDU - 4756 题意是要让n-1间宿舍和发电站相连 也就是连通嘛 最小生成树板子一套 但是还有个限制条件 就是其中有两个宿舍是不能连着的 要求所有情况中最大的那个 这是稠密图 用kruskal的时间会大大增加 所以先跑一遍prim 跑完之后对最小生成树里面的边…...
网站安全风险评估报告/百度推广代理
学习软件: 用法: 1、 搭建 宝塔面板创建多个站点。 2、 不用购买域名和服务器,适用于本地 步骤: 1.下载好软件之后,安装。 2.找到目录文件:C:\Windows\System32\drivers\etc\hosts,用文本文档打…...
崇明区建设镇网站/seo站外推广有哪些
目录 1请设计一个类,不能被拷贝 2请设计一个类,只能在堆上创建对象 3请设计一个类,只能在栈上创建对象 4请设计一个类,不能被继承 5请设计一个类,只能创建一个对象(单例模式) 1.请设计一个类,不能被拷…...
网站建设定制设计/品牌推广策划方案案例
iwconfig命令用于系统配置无线网络设备或显示无线网络设备信息。iwconfig命令类似于ifconfig命令,但是他配置对象是无线网卡,它对网络设备进行无线操作,如设置无线通信频段。auto 自动模式essid 设置ESSIDnwid 设置网络IDfreq 设置无线网络通…...
嘉兴网站制作案例/百度一下就知道官方
前面我们所讲的所有指令,代码执行顺序都是一条接着一条顺序的执行。但是实际上在编码过程中,会有某些结构,比如条件语句(if-else),循环语句(for,do-while)和分支语句(swi…...
scf900色带/南宁百度seo排名公司
在百度控制台发布自定义样式并复制样式ID 添加 setMapStyleV2 功能 map.setMapStyleV2({styleId:76c03cdd35fa1e24f39edeb18849f04e}) 完整代码如下: <!DOCTYPE html> <html lang"en"> <head><script type"text/javascript&…...