golang内置函数len的小技巧


Posted in Golang onJuly 25, 2021

len是很常用的内置函数,可以测量字符串、slice、array、channel以及map的长度/元素个数。

不过你真的了解len吗?也许还有一些你不知道的小知识。

我们来看一道GO101的题目,这题也被GO语言爱好者周刊转载:

package main

import "fmt"

func main() {
    var x *struct {
        s [][32]byte
    }
    
    fmt.Println(len(x.s[99]))
}

题目问你这段代码的运行结果,选项有编译错误、panic、32和0。

我们分析一下,别看x的声明定义一大长串,实际上就是定义了一个有个[][32]byte的结构体,然后x是这个结构体的指针。

然后我们没有初始化x,所以x是一个值为nil的指针。看到这里你也许以及有答案了,对nil指针解引用访问它的成员s,那不就是panic嘛。即使引用x的成员合法,我们的s也没有初始化,访问没有初始化的slice也会panic。

然而这么想你就错了,代码的实际运行结果是32!

为什么呢?我们看看len的帮助文档:

For some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification's "Length and capacity" section for details.

这句话很重要,对于结果是数组的表达式,len可能会是一个编译期常量,而且数组类型的长度在编译期是可知的,所以熟悉c++的朋友大概会立刻想到这样的常量是不需要进行实际求值的,简单类型推导即可获得。不过口说无凭,我们看看spec里的描述:

The expression len(s) is constant if s is a string constant. The expressions len(s) and cap(s) are constants if the type of s is an array or pointer to an array and the expression s does not contain channel receives or (non-constant) function calls; in this case s is not evaluated. Otherwise, invocations of len and cap are not constant and s is evaluated.

如果表达式是字符串常量那么len(s)也是常量。如果表达式s的类型是array或者array的指针,且表达式不是channel的接收操作或是函数调用,那么len(s)是常量,且表达式s不会被求值;否则len和cap会对s进行求值,其计算结果也不是一个常量。

其实说的很清楚了,但还有三点需要说明。

第一个是视为常量的表达式里为什么不能含有chan的接收操作和函数调用?

这个答案很简单,因为这两个操作都是使用这明确希望发生“副作用”的。特别是从chan里接收数据,还会导致goroutine阻塞,而我们的常量len表达式不会进行求值,这些你期望会发生的副作用便不会产生,会引发一些隐蔽的bug。

第二个是我们注意到了函数调用前用non-constant修饰了,这是什么意思?

按字面意思,一部分函数调用其实是可以在编译期完成计算被当成常量处理的,而另一些不可以。

在进一步深入之前我们先要看看golang里哪些东西是常量/常量表达式。

  • 首先是各种字面量以及对字面量的类型转换产生的值了,无需多说。
  • 一部分内置函数:len、cap、imag、real、complex,它们在参数是常量的时候本身也是常量表达式。
  • unsafe.Sizeof,因为类型的大小也是编译期就能确定的,所以它是常量表达式也很好理解。
  • 所有的常量之间的运算(加减乘除位运算等,除了非常量表达式函数的调用)都是常量表达式。

从上面的描述里可以看出两点,内置函数和unsafe.Sizeof的调用我们可以看成是constant function calls,所有常量表达式除了浮点数和复数表达式都可以在编译期完成计算。而其他函数比如用户自定义函数的调用,虽然仍然有可能在编译期被求值优化,但本身不属于常量表达式。所以语言标准会加以区分。比如下面这个:

func add(x, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(512, 513)) // 1025
}

如果我们看生成的汇编,会发现求值已经完成,不需要调用add:

MOVQ    $1025, (SP)
PCDATA  $1, $0
CALL    runtime.convT64(SB)
MOVQ    8(SP), AX
XORPS   X0, X0
MOVUPS  X0, ""..autotmp_16+64(SP)
LEAQ    type.int(SB), CX
MOVQ    CX, ""..autotmp_16+64(SP)
MOVQ    AX, ""..autotmp_16+72(SP)
NOP
MOVQ    os.Stdout(SB), AX
LEAQ    go.itab.*os.File,io.Writer(SB), CX
MOVQ    CX, (SP)
MOVQ    AX, 8(SP)
LEAQ    ""..autotmp_16+64(SP), AX
MOVQ    AX, 16(SP)
MOVQ    $1, 24(SP)
MOVQ    $1, 32(SP)
NOP
CALL    fmt.Fprintln(SB)
MOVQ    80(SP), BP
ADDQ    $88, SP
RET

很明显的,1025已经在编译期求值了,然而add的调用不是常量表达式,所以下面的代码会报错:

const number = add(512, 513) // error!!!

// example.go:11:7: const initializer add(512, 513) is not a constant

spec给出的实例是调用的内置函数,内置函数也只有在参数是常量的情况下被调用才算做常量表达式:

const (
 c4 = len([10]float64{imag(2i)})  // imag(2i) is a constant and no function call is issued
 c5 = len([10]float64{imag(z)})   // invalid: imag(z) is a (non-constant) function call
)
var z complex128

所以len的表达式里如果用了non-constant的函数调用,那么就len本身不能算是常量表达式了。

这就有了最后一个疑问,题目中的x不是常量,为什么len的结果是常量呢?

标准只说表达式里不能有chan的接收和非常量表达式的函数调用,没说其他的不可以。你也可以这么理解,表达式都有结果值,任何值除了无类型常量(这里显然不是)都是要有一个确定的类型的,只要这个类型是数组或者数组的指针,那len就能获得数组的长度,而这一切不需要s一定是常量表达式,编译器可以简单推导出表达式的值的类型。不能包含non-constant function calls和chan接收是我在第一点里解释的,杜绝所有可能的副作用发生从而保证即使不对表达式求值程序也是正确的,不包含这两个操作的表达式既可以是常量的也可以不是,所以这里我们能用x.s[99]作为len的参数。

说了这么多,只要len的参数类型为array或者array的指针并且符合要求,就不会进行求值,而题目里的表达式正好满足这点,所以虽然我们看起来是会导致panic的代码,但是本身并未进行实际求值,因此程序可以正常运行。另外cap也遵循同样的规则。

最后,还有个小测验,检验一下自己的学习吧:

// 以下哪些语句是正确的,哪些是错误的
var slice [][]*[10]int

const (
    a = len(slice[10000000000000][4]) // 1
    b = len(slice[1]) // 2
    c = len(slice) // 3
    d = len([1]int{1024}) // 4
    e = len([1]int{add(512, 512)}) // 5
    g = len([unsafe.Sizeof(slice)]int{}) // 6
    g = len([unsafe.Sizeof(slice)]int{int(unsafe.Sizeof(slice))}) // 7
)

参考
https://golang.org/ref/spec#Length_and_capacity

到此这篇关于golang内置函数len的小技巧的文章就介绍到这了,更多相关golang内置函数len内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Golang 相关文章推荐
go结构体嵌套的切片数组操作
Apr 28 Golang
使用Golang的channel交叉打印两个数组的操作
Apr 29 Golang
完美解决golang go get私有仓库的问题
May 05 Golang
浅谈golang package中init方法的多处定义及运行顺序问题
May 06 Golang
go web 预防跨站脚本的实现方式
Jun 11 Golang
使用GO语言实现Mysql数据库CURD的简单示例
Aug 07 Golang
Golang 并发下的问题定位及解决方案
Mar 16 Golang
Go语言实现一个简单的并发聊天室的项目实战
Mar 18 Golang
golang生成vcf通讯录格式文件详情
Mar 25 Golang
Golang 对es的操作实例
Apr 20 Golang
详解Go语言中Get/Post请求测试
Jun 01 Golang
Go gRPC进阶教程gRPC转换HTTP
Jun 16 Golang
修改并编译golang源码的操作步骤
Go语言应该什么情况使用指针
Jul 25 #Golang
基于Go语言构建RESTful API服务
Jul 25 #Golang
go goroutine 怎样进行错误处理
Jul 16 #Golang
go开发alertmanger实现钉钉报警
Jul 16 #Golang
K8s部署发布Golang应用程序的实现方法
Jul 16 #Golang
入门学习Go的基本语法
Jul 07 #Golang
You might like
解析:php调用MsSQL存储过程使用内置RETVAL获取过程中的return值
2013/07/03 PHP
在Yii框架中使用PHP模板引擎Twig的例子
2014/06/13 PHP
php第一次无法获取cookie问题处理
2014/12/15 PHP
谈谈PHP中substr和substring的正确用法及相关参数的介绍
2015/12/16 PHP
php异步:在php中使用fsockopen curl实现类似异步处理的功能方法
2016/12/10 PHP
JavaScript实际应用:innerHTMl和确认提示的使用
2006/06/22 Javascript
jQuery插件开发的两种方法及$.fn.extend的详解
2014/01/16 Javascript
JQuery弹出层示例可自定义
2014/05/19 Javascript
Javascript基础知识(三)BOM,DOM总结
2014/09/29 Javascript
javascript根据时间生成m位随机数最大13位
2014/10/30 Javascript
javascript递归回溯法解八皇后问题
2015/04/22 Javascript
jquery操作angularjs对象
2015/06/26 Javascript
Jquery幻灯片特效代码分享--打开页面随机选择切换方式(3)
2015/08/15 Javascript
JavaScript中rem布局在react中的应用
2015/12/09 Javascript
jQuery 获取多选框的值及多选框中文的函数
2016/05/16 Javascript
jQuery实现可展开折叠的导航效果示例
2016/09/12 Javascript
jquery无法为动态生成的元素添加点击事件的解决方法(推荐)
2016/12/26 Javascript
angularJS+requireJS实现controller及directive的按需加载示例
2017/02/20 Javascript
Javascript中八种遍历方法的执行速度深度对比
2017/04/25 Javascript
基于vue如何发布一个npm包的方法步骤
2019/05/15 Javascript
[02:04]2016国际邀请赛中国区预选赛VG.R晋级之路
2016/07/01 DOTA
Python中的命令行参数解析工具之docopt详解
2017/03/27 Python
python环形单链表的约瑟夫问题详解
2018/09/27 Python
django框架使用方法详解
2019/07/18 Python
Django如何实现网站注册用户邮箱验证功能
2019/08/14 Python
Python模块汇总(常用第三方库)
2019/10/07 Python
python3中利用filter函数输出小于某个数的所有回文数实例
2019/11/24 Python
python中remove函数的踩坑记录
2021/01/04 Python
为有想象力的人提供的生活方式商店:Firebox
2018/06/04 全球购物
高中体育教学反思
2014/01/24 职场文书
计算机毕业生求职信
2014/06/10 职场文书
4s店销售经理岗位职责
2014/07/19 职场文书
社区娱乐活动方案
2014/08/21 职场文书
企业百日安全活动总结
2015/05/07 职场文书
病假证明模板
2015/06/19 职场文书
2016年教师节感言
2015/12/09 职场文书