面试题:

package main

func a() []int {
a1 := []int{3}
a2 := a1[1:]
return a2
} func main() {
a()
}

看到这个题, 你的第一反应是啥?

(A) 编译失败
(B) panic: runtime error: index out of range [1] with length 1
(C) []
(D) 其他

第一感觉: 肯定能编译过, 但是运行时一定会panic的. 但事与愿违竟然能够正常运行, 结果是:[]

疑问

a1 := []int{3}
a2 := a1[1:]
fmt.Println("a[1:]", a2)

a1 和 a2 共享同样的底层数组, len(a1) = 1, a1[1]绝对会panic, 但是a[1:]却能正常输出, 这是为何?

从表面入手

整体上看下整体的情况

a1 := []int{3}
fmt.Printf("len:%d, cap:%d", len(a1), cap(a1))
fmt.Println("a[0:]", a1[0:])
fmt.Println("a[1:]", a1[1:])
fmt.Println("a[2:]", a1[2:])

结果:

len:1, cap:1
a[0:]: [1]
a[1:] []
panic: runtime error: slice bounds out of range [2:1]

从表面来看, 从a[2:]才开始panic, 到底是谁一手造成这样的结果呢?

汇编上看一目了然

"".a STEXT size=87 args=0x18 locals=0x18
// 省略...
0x0028 00040 (main.go:6) CALL runtime.newobject(SB)
0x002d 00045 (main.go:6) MOVQ 8(SP), AX // 将slice的数据首地址加载到AX寄存器
0x0032 00050 (main.go:6) MOVQ $3, (AX) // 把3放入到AX寄存器中, 也就是a1[0]
0x0039 00057 (main.go:8) MOVQ AX, "".~r0+32(SP)
0x003e 00062 (main.go:8) XORPS X0, X0 // 初始化 X0 寄存器
0x0041 00065 (main.go:8) MOVUPS X0, "".~r0+40(SP) // 将X0放入返回值
0x0046 00070 (main.go:8) MOVQ 16(SP), BP
0x004b 00075 (main.go:8) ADDQ $24, SP
0x004f 00079 (main.go:8) RET
// 省略....

其实主要关心这两行即可.

0x003e 00062 (main.go:8)	XORPS	X0, X0     // 初始化 X0 寄存器
0x0041 00065 (main.go:8) MOVUPS X0, "".~r0+40(SP) // 将X0放入返回值

是不是很神奇, a[1:] 没有调用runtime.panicSliceB(SB), 而是返回的是一个空的slice. 这是为何呢?

持着怀疑态度, 去 github 提上一枚 issue. https://github.com/golang/go/issues/42069

结论: 这是故意的, 单纯为了保持reslice对称而已. 这也就解释了返回一个空的slice的原因.

reslice 原理

上面的问题已经解释清楚了, 回过头来看正常 reslice 的例子

func a() []int {
a1 := []int{3, 4, 5, 6, 7, 8}
a2 := a1[2:]
return a2
}

用简单的图来描述这段代码里, a1 和 a2 之间的reslice 关系. 可以看到 a1 和 a2 是共享底层数组的.

如果你知道这些, 那么 slice 的使用基本上不会出现问题.

下面这些问题你考虑过吗 ?

  1. a1, a2 是如何共享底层数组的?
  2. a1[low:high]是如何实现的?

继续来看这段代码的汇编:

"".a STEXT size=117 args=0x18 locals=0x18
// 省略...
0x0028 00040 (main.go:4) CALL runtime.newobject(SB)
0x002d 00045 (main.go:4) MOVQ 8(SP), AX
0x0032 00050 (main.go:4) MOVQ $3, (AX)
0x0039 00057 (main.go:4) MOVQ $4, 8(AX)
0x0041 00065 (main.go:4) MOVQ $5, 16(AX)
0x0049 00073 (main.go:4) MOVQ $6, 24(AX)
0x0051 00081 (main.go:4) MOVQ $7, 32(AX)
0x0059 00089 (main.go:4) MOVQ $8, 40(AX)
0x0061 00097 (main.go:5) ADDQ $16, AX
0x0065 00101 (main.go:6) MOVQ AX, "".~r0+32(SP)
0x006a 00106 (main.go:6) MOVQ $4, "".~r0+40(SP)
0x0073 00115 (main.go:6) MOVQ $4, "".~r0+48(SP)
0x007c 00124 (main.go:6) MOVQ 16(SP), BP
0x0081 00129 (main.go:6) ADDQ $24, SP
0x0085 00133 (main.go:6) RET
// 省略....
  • 第4行: 将 AX 栈顶指针下移 8 字节, 指向了 a1 的 data 指向的地址空间里
  • 第5-10行: 将 [3,4,5,6,7,8] 放入到 a1 的 data 指向的地址空间里
  • 第11行: AX 指针后移 16 个字节. 也就是指向元素 5 的位置
  • 第12行: 将 SP 指针下移 32 字节指向即将返回的 slice (其实就是 a2 ), 同时将 AX 放入到 SP. 注意 AX 放入 SP 里的是一个指针, 也就造成了a1, a2是共享同一块内存空间的
  • 第13行: 将 SP 指针下移 40 字节指向了 a2 的 len, 同时 把 4 放入到 SP, 也就是 len(a2) = 4
  • 第14行: 将 SP 指针下移 48 字节指向了 a2 的 cap, 同时 把 4 放入到 SP, 也就是 cap(a2) = 4

下图是 slice 的 栈图, 可以配合着上面的汇编一块看.

看到这里是不是一目了然了. 于是有了下面的这些结论:

  1. reslice 完全是利用汇编实现的
  2. reslice 时, slice 的 data 通过指针的移动完成, 造成了共享相同的底层数据, 同时将新的 len, cap 放入对应的位置

至此, golang reslice的原理基本已经阐述清楚了.

参考资料

  1. 深入Go的底层,带你走近一群有追求的人
  2. 汇编角度看 Slice,一个新的世界
  3. Why slice not painc
  4. Slice expressions
  5. A Quick Guide to Go's Assembler
  6. plan9 assembly 完全解析

面试题:让你捉摸不透的 Go reslice的更多相关文章

  1. mysql的又一个让人捉摸不透的bug?

    这次就不说很多没有写博客了,因为前几天已经写过了.\^o^/ 昨天我们刚讨论了关于自动化运维工作的实现方式,如果批量执行,中间出错怎么办?突然有人提出mysql支持--force,可以跳过出错继续执行 ...

  2. setTimeout 的黑魔法

    setTimeout,前端工程师必定会打交道的一个函数.它看上去非常的简单,朴实.有着一个很不平凡的名字--定时器.让年少的我天真的以为自己可以操纵未来.却不知朴实之中隐含着惊天大密.我还记得我第一次 ...

  3. B 最熟悉的陌生人 (纪念当年就读的梅州市江南高级中学)

    最熟悉的陌生人 作者:张慧桥 枪与玫瑰 我看了一下聊天室的名单,哈哈哈,我不禁喜出望外:蝶恋花那丫头片子挂在线上呢,真是天助我也.初时的担心一扫而光,我精神抖擞地喝下一大口咖啡,猛抽了三口烟,现在的我 ...

  4. 【Networking】容器网络大观 && SDN 资料汇总

    SDNLAB技术分享(十五):容器网络大观   SDNLAB君• 16-06-17 •2957 人围观 编者按:本文系SDNLAB技术分享系列,本次分享来自SDN撕X群(群主:大猫猫)群直播,我们希望 ...

  5. CSS z-index 属性的使用方法和层级树的概念

    之前有一篇文章提到过z-index,我们知道只有在元素设置了position部位static时才生效,而且z-index也跟父元素有关系,今天就在ie7遇到类似问题,在网上查了一些资料,发现一篇好文章 ...

  6. float属性

    float属性介绍 float给人一种捉摸不透的感觉,不过可以依照浏览器的解析机制(根据HTML文档,从上往下解析),对float属性了解一二.float有四种值:none/left/right/in ...

  7. js高级程序设计(七)BOM

    window 对象 BOM 的核心对象是window,它表示浏览器的一个实例.在浏览器中,window 对象有双重角色,它既是通过JavaScript 访问浏览器窗口的一个接口,又是ECMAScrip ...

  8. 【转】CSS z-index 属性的使用方法和层级树的概念

    文章转自:CSS z-index 属性的使用方法和层级树的概念,另外加了一点自己的注释 CSS 中的 z-index 属性用于设置节点的堆叠顺序, 拥有更高堆叠顺序的节点将显示在堆叠顺序较低的节点前面 ...

  9. html里文本编辑器如何制作呢?

    初入it职场,文本编辑器真的让人捉摸不透.最终在前端姐姐帮助下弄好了↓ 先在头部写好编辑器的各种功能的总体模型 <script>var editor; KindEditor.ready(f ...

随机推荐

  1. 使用 Castle 实现 AOP,以及 Autofac 集成 Castle

    Castle 是 2003 年诞生于 Apache Avalon 项目,目的是为了创建一个IOC 框架.发展到现在已经有四个组件: ORM组件:ActiveRecord IOC组件:Windsor 动 ...

  2. 自关联映射:一个表自己关联自己,此时从同一个表中查询,通过起别名将一张表变成两张表,使用join语句。

    实例1:id自关联. 隐式内连接: 实例二:编写一个 SQL 查询,来查找与之前(昨天的)日期相比温度更高的所有日期的 id .返回结果 不要求顺序 . 查询结果格式如下例: Weather +--- ...

  3. js如何判断一假则假,全真则真

    思路:初始化flag参数为true,一旦有一个为假,则将flag赋值为false,最后返回. 代码如下: checkSupplyWt(list){ var flag = true; list.forE ...

  4. JSON的stringify和parse方法

    一.JSON.parse() 方法用于将一个 JSON 字符串转换为对象. 以下代码为将字符串解析为对象,然后再赋值给对象 //页面初始化完成加载,option是传递的参数 onLoad: funct ...

  5. Vim的基本命令

    Vi vi的两种模式 ①commad命令模式:无法输入任何东西,需要按下i进入编辑模式 ②edit编辑模式:按下esc退出到命令模式,在命令模式下按下wq [文件名] 可以退出并且成功的保存 //一些 ...

  6. Hive实现自增序列及常见的Hive元数据问题处理

    Hive实现自增序列 在利用数据仓库进行数据处理时,通常有这样一个业务场景,为一个Hive表新增一列自增字段(比如事实表和维度表之间的"代理主键").虽然Hive不像RDBMS如m ...

  7. python模块win32com中的early-bind与lazy-bind(以Autocad为例)

    1.什么是Lazy-bind模式,Early-bind模式? win32com中,Lazy-bind 模式指的是程序事先不知道对象的任何方法和属性,当对象属性,方法被调用时,程序才向对象发出一个询问( ...

  8. 心脏滴血(CVE-2014-0160)检测与防御

    用Nmap检测 nmap -sV --script=ssl-heartbleed [your ip] -p 443 有心脏滴血漏洞的报告: ➜ ~ nmap -sV --script=ssl-hear ...

  9. 381. O(1) 时间插入、删除和获取随机元素 - 允许重复

    381. O(1) 时间插入.删除和获取随机元素 - 允许重复 LeetCode_381 题目详情 题解分析 代码实现 package com.walegarrett.interview; impor ...

  10. HDOJ-1114(完全背包模板题)

    Piggy-Bank HDOJ-1114 本题就是完全背包的模板题,注意复习一下关于背包九讲中的问什么这里使用的是顺序遍历. 还需要注意的一个问题就是初始化的问题,dp[0]初始化为0,其他的初始化为 ...