惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
D
Docker
GbyAI
GbyAI
B
Blog RSS Feed
博客园 - 司徒正美
博客园 - Franky
美团技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
C
Check Point Blog
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
AI
AI
O
OpenAI News
Attack and Defense Labs
Attack and Defense Labs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
S
Secure Thoughts
博客园 - 聂微东
L
LINUX DO - 最新话题
U
Unit 42
SecWiki News
SecWiki News
A
Arctic Wolf
Schneier on Security
Schneier on Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Visual Studio Blog
量子位
The Cloudflare Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
G
Google Developers Blog
T
Threat Research - Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
B
Blog
博客园 - 【当耐特】
C
CERT Recently Published Vulnerability Notes
Scott Helme
Scott Helme
Last Week in AI
Last Week in AI
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
A
About on SuperTechFans
博客园 - 三生石上(FineUI控件)
Latest news
Latest news

文章列表 on polarisxu

通过一个例子让你彻底掌握 Go 工作区模式 这道题正确率竟然只有 22% Go 开源项目推荐:一个简单的 Go 练手项目 go test 如何禁用缓存? Go1.18 快讯:constraints 包被移除标准库 跟着 Go 作者学泛型 「2022 版」轻松搞定 Go 开发环境 鹅厂小哥用行动反对加班:程序员加班能被终结吗? Go 为什么选择 Gopher 作为吉祥物? 本地如何配置多个 GitHub/Gitee 账号? 为 Java 开发者准备的 Go 教程 02:Java 有而 Go 无 GitHub 发现了 studygolang 项目依赖的漏洞 Typora 宣布收费后,这款开源 Markdown 编辑器火了 为 Java 开发者准备的 Go 教程 01:漫游了 我的 2021 年终总结 泛型版 singleflight:Go 中如何防止缓存击穿? 2021 年你写了多少代码?这个 Go 工具帮你统计 Go1.18 新特性:TryLock Go 泛型入门教程 程序员瑞士军刀:各种结构的转换工具 终于有 Go 版的 Elasticsearch 了 Go泛型系列:maps 包讲解 Go 1.18 中的 any 是什么? Go1.18 快讯:新的 IP 包 Go编程模式:详解函数式选项模式 Go泛型系列:slices 包讲解 Nginx 竟然也有 playground — Go 语言构建的 Go泛型系列:Go1.18 类型约束那些事 Go1.18 快讯:Module 工作区模式 Go1.18 快讯:新增的 Cut 函数太方便了 Go1.18 快讯:废弃了这个 API Go1.18 快讯:新增字符串 Clone API Go:如何获得项目根目录? Go 如何获取和设置环境变量 重磅变动:更快找到微信公众号里的Go文章 Go 这样设置版本号:我们的项目也可以 假期结束,推荐 2 本 manning 出的 Go 图书 Go Fiber 框架系列教程 04:测试应用 Go Fiber 框架系列教程 03:中间件 Go泛型系列:提前掌握Go泛型的基本使用 推荐一个 Go GUI 实战项目 Go Fiber 框架系列教程 02:详解相关 API 的使用 Go Fiber 框架系列教程 01: 和 Express 对比学习 Go Module:私有不合规库怎么解决引用问题 Go1.17 新特性:go get 变了 Go1.17 新特性:testing 包的相关变化 StackOverflow 上关于 Go select 死锁的问题 新书推荐:用 Gin 框架构建分布式应用 Go 官网要变天。。。 Gin 这是要成为 Go 官方框架? 扬眉吐气:刚刚,Go 已经默认支持泛型了 Go 1.17 新特性:Module 有哪些变化? Go 第三方库推荐:类型转换如此简单 担心密码提交到 GitHub?建议使用这个 Go 开源工具 Go 的时间格式化为什么是 2006-01-02 15:04:05? GitHub 为 Go 社区带来安全支持 Go开源项目推荐:500行代码确认请求时间花在哪 一道关于 len 函数的诡异 Go 面试题解析 厉害了我的 Go:推荐系统都有开源实现 Go1.17 新特性:新版构建约束 这个工具真好:看看你的Go项目依赖有无漏洞 网友很强大,发现了Go并发下载的Bug Rust 劝退系列 09:函数 Go项目实战:一步步构建一个并发文件下载器 这本 Go 新书挺期待的:100 个常见错误 Go 启用新的官方问答社区 这是要干嘛?!微软招 Go 编译器全职开发人员 Go1.17 快报之标准库越来越注重易用性 Go1.17 新特性之切片变数组 回顾 Go 官网的演变史 站长8年前的Go代码竟然进入大厂的项目里了 Go图书翻译:一个好消息,一个坏消息 不怕烂尾!决定组织翻译这本 Go 图书 Rust 劝退系列 08:模式匹配 一本花了2.5年写成的Go免费在线图书 被黑惨了:一句话,说明自己会 Go,咋整? Go1.16 中的新函数 signal.NotifyContext 怎么用? Go 1.17 新特性提前学之测试执行随机化 Rust 劝退系列 07:流程控制 官方的 Go 多版本管理:使用和原理 Rust新书:给你一个劝退的理由 周刊题解:常量表达式这个规则应该了解下 盘点那些使用 Go 语言的国外公司 编写了50万行Go代码是一种什么体验 Rust 劝退系列 06:常量 Uber 使用 Go 的规模这么大?!都自己定制的 Go 编译器了 Go Team Leader — rsc 大神新开源了一个库,增强模板功能 再一次看到了 Go 的节制:int128 类型要不要支持? Rust 劝退系列 05:复合数据类型 「卷」有理论依据:海勒姆定律—Go又是怎么卷的 Go 真的也可以进行 GUI 开发:还有这样的图书呢 Rust 劝退系列 04:基本数据类型 我的 Go 语言书单 Rust 劝退系列 03:变量 我又来推荐免费 Go 新书了:一本用 Go 讲架构的书 注释竟然还有特殊用途?一文解惑 //go:linkname 指令 这个功能,公众号、微信群会不会被玩坏? 一道 Go 闭包题,面试官说原来自己答错了:面别人也涨知识 Rust 劝退系列 02:第一个 Rust 程序 Rust 劝退系列 01:打造开发环境
一道关于 json 和 slice 的题难倒了 80% 的人
2020-10-18 · via 文章列表 on polarisxu

昨天在 Go语言爱好者周刊:第 65 期 刊首语发了一道题,以下代码输出什么?

package main

import (
	"encoding/json"
	"fmt"
)

type AutoGenerated struct {
	Age   int    `json:"age"`
	Name  string `json:"name"`
	Child []int  `json:"child"`
}

func main() {
	jsonStr1 := `{"age": 14,"name": "potter", "child":[1,2,3]}`
	a := AutoGenerated{}
	json.Unmarshal([]byte(jsonStr1), &a)
	aa := a.Child
	fmt.Println(aa)
	jsonStr2 := `{"age": 12,"name": "potter", "child":[3,4,5,7,8,9]}`
	json.Unmarshal([]byte(jsonStr2), &a)
	fmt.Println(aa)
}

结果 80% 的人都答错了。

结果为什么是 [1 2 3] [3 4 5] 呢?

这道题涉及到两个知识点:

  • json 解析;
  • slice;

关于 json.Unmarshal 的文档,不少人可能没认真看。借此机会正好一起看下。https://docs.studygolang.com/pkg/encoding/json/#Unmarshal

Unmarshal 解析 JSON 编码的数据,并将结果存入 v 指向的值。如果 v 为 nil 或不是指针,则 Unmarshal 返回 InvalidUnmarshalError。

Unmarshal 和 Marshal 做相反的操作,必要时申请 map、slice 或指针,有如下的附加规则:

  • 为了将 JSON 数据解码写入一个指针,Unmarshal 首先处理 JSON 数据为 JSON 字面值 null 的情况。此时,Unmarshal 会将指针设置为 nil。否则,Unmarshal 会将 JSON 数据解码为指针所指向的值。如果指针为 nil,则 Unmarshal 为其分配一个新值并使指针指向它。

  • 为了将 JSON 数据解码为实现 Unmarshaler 接口的值,Unmarshal 调用该值的 UnmarshalJSON 方法,包括当输入为 JSON null 时。否则,如果该值实现 encoding.TextUnmarshaler 且输入是带引号的 JSON 字符串,则 Unmarshal 会使用该字符串的未加引号形式来调用该值的 UnmarshalText 方法。

  • 要将 json 数据解码写入一个结构体,函数会匹配输入对象的键和 Marshal 使用的键(结构体字段名或者它的标签指定的键名),优先选择精确的匹配,但也接受大小写不敏感的匹配;

  • 为了将 JSON 数据解码到结构中,Unmarshal 将传入的对象键与 Marshal 使用的键(结构字段名称或其 Tag)进行匹配,希望使用精确匹配,但还接受不区分大小写的匹配。默认情况下,没有相应结构字段的对象键将被忽略(有关替代方法,请参见 Decoder.DisallowUnknownFields)。

  • 要将 JSON 数据解码写入一个接口类型值,Unmarshal 将其中之一存储在接口值中:

    Bool                   对应JSON布尔类型
    float64                对应JSON数字类型
    string                 对应JSON字符串类型
    []interface{}          对应JSON数组
    map[string]interface{} 对应JSON对象
    nil                    对应JSON的null
    
  • 要将一个 JSON 数组解码到切片(slice)中,Unmarshal 将切片长度重置为零,然后将每个元素 append 到切片中。特殊情况,如果将一个空的 JSON 数组解码到一个切片中,Unmarshal 会用一个新的空切片替换该切片。

  • 为了将 JSON 数组解码为 Go 数组,Unmarshal 将 JSON 数组元素解码为对应的 Go 数组元素。如果 Go 数组长度小于 JSON 数组,则其他 JSON 数组元素将被丢弃。如果 JSON 数组长度小于 Go 数组,则将其他 Go 数组元素会设置为零值。

  • 要将 JSON 对象解码到 map 中,Unmarshal 首先要建立将使用的 map。如果 map 为零,Unmarshal 会分配一个新 map。否则,Unmarshal 会重用现有 map,保留现有条目(item)。然后,Unmarshal 将来自 JSON 对象的键/值对存储到 map 中。map 的键类型必须是任意字符串类型、整数或实现了 json.Unmarshaler 或 encoding.TextUnmarshaler 接口的类型。

  • 如果 JSON 值不适用于给定的目标类型,或者 JSON 数字写入目标类型时溢出,则 Unmarshal 会跳过该字段并尽最大可能完成解析。如果没有遇到更多的严重错误,则 Unmarshal 返回一个 UnmarshalTypeError 来描述最早的此类错误。但无法确保有问题的字段之后的所有其余字段都将被解析到目标对象中。

  • JSON 的 null 值解码为 Go 的接口、指针、切片时会将它们设为 nil,因为 null 在 JSON 里一般表示“不存在”。 因此将 JSON null 解码到任何其他 Go 类型中不会影响该值,并且不会产生任何错误。

  • 解析带引号的字符串时,无效的 UTF-8 或无效的 UTF-16 不会被视为错误。而是将它们替换为 Unicode 字符 U+FFFD。

跟此题相关的是下面这点:

要将一个 JSON 数组解码到切片(slice)中,Unmarshal 将切片长度重置为零,然后将每个元素 append 到切片中。特殊情况,如果将一个空的 JSON 数组解码到一个切片中,Unmarshal 会用一个新的空切片替换该切片。

因此第一次解析时,a.Child 是 [1 2 3],aa 自然也是 [1 2 3]。第二次解析时,a.Child 的长度会被重置为 0,也就说里面的值会被重置(比如 a.Child = a.Child[:0]),然后将 3,4,5,7,8,9 一个个 append 到 a.Child 中。

而 append 操作可能会涉及到底层数组的扩容:当原来的容量不足时,会进行扩容。怎么扩容的呢?目前的版本(Go1.15.x)按照如下规则扩容:(扩容规则依赖具体实现,不同版本可能不一样)

// Get element of array, growing if necessary.
if v.Kind() == reflect.Slice {
  // Grow slice if necessary
  if i >= v.Cap() {
    newcap := v.Cap() + v.Cap()/2
    if newcap < 4 {
      newcap = 4
    }
    newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
    reflect.Copy(newv, v)
    v.Set(newv)
  }
  if i >= v.Len() {
    v.SetLen(i + 1)
  }
}
  • 初始容量最小为 4;
  • 之后按照容量的一半扩容,所以容量是 4、6、9、13、19…

有人问上题为什么 aa 的容量是 4,这里正好解释了。

因此,第一次解析,aa.Child 是:[1 2 3],cap = 4。第二次解析,aa.Child 先被重置,之后将 3,4,5,7,8,9 一个个 append,最后 aa.Child 是:[3 4 5 6 7 8 9], cap = 6。

2、slice

以上就是能从 json Unmarshal 文档能学到的相关知识。接下来关键在于 slice。关于 slice 的知识,网上很多教程,这里只讲解和该题相关的内容。

1)aa := a.Child 意味着什么?

先看 a.Child 的内部结构。

赋值给 aa 后呢?aa 和 a.Child 共用底层数组。

这里有引入一个小知识点,aa := a.Child 后,以下代码输出的两个地址是一样的:

fmt.Printf("%p,%p\n", a.Child, aa)

它们输出的都是底层数组的地址,这里一定要注意。通过它们,你可以验证底层数组扩容了(地址变了,表明扩容了)。

如果要输出 slice 本身的地址,应该这样:

fmt.Printf("%p,%p\n", &a.Child, &aa)

2)执行第二次 json 解析后

根据上文的讲解,底层数组从索引 0 位置开始依次被 3、4、5、7 填充。因为 aa 的 len 是 3,所以即使底层数组变成了 3、4、5、7,aa 看到却是 3、4、5。

当再继续解析时,底层数组容量不够,因此进行扩容,cap 变成 6,将原底层数组的元素拷贝一份到新的数组中。所以最后 a.Child 的底层数组是这个新的底层数组:[3 4 5 7 8 9],cap = 6。而 aa 的底层数组还是原来的。最后的内部表示是这样的。

3、小结

总结起来两点:

  • json 解析的规则,文档上明确说明了;(有人评论说跟 json 没关系,但我觉得很多人根本不知道 json 对这块是怎么处理的,正因为有这样的处理,才引出了 slice 扩容的问题)
  • slice 内部表示和 append 导致扩容。

希望你已经彻底掌握了以上知识点。