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语言中切片与内存复制 memcpy 的实现操作
Apr 27 Golang
浅谈Golang 嵌套 interface 的赋值问题
Apr 29 Golang
golang slice元素去重操作
Apr 30 Golang
go语言中GOPATH GOROOT的作用和设置方式
May 05 Golang
Go 自定义package包设置与导入操作
May 06 Golang
解决golang 关于全局变量的坑
May 06 Golang
Go语言空白表示符_的实例用法
Jul 04 Golang
一文搞懂Golang 时间和日期相关函数
Dec 06 Golang
golang生成并解析JSON
Apr 14 Golang
GO语言字符串处理函数之处理Strings包
Apr 14 Golang
Go语言编译原理之源码调试
Aug 05 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的错误信息
2006/10/09 PHP
利用PHPExcel实现Excel文件的写入和读取
2017/04/26 PHP
Laravel 5.5 的自定义验证对象/类示例代码详解
2017/08/29 PHP
对于Laravel 5.5核心架构的深入理解
2018/02/22 PHP
php 读写json文件及修改json的方法
2018/03/07 PHP
laravel 判断查询数据库返回值的例子
2019/10/11 PHP
TP5框架实现签到功能的方法分析
2020/04/05 PHP
统一接口:为FireFox添加IE的方法和属性的js代码
2007/03/25 Javascript
jQuery Ajax使用 全解析
2010/12/15 Javascript
用Js实现的动态增加表格示例自己写的
2013/10/21 Javascript
实现51Map地图接口(示例代码)
2013/11/22 Javascript
JS高级调试技巧:捕获和分析 JavaScript Error详解
2014/03/16 Javascript
JavaScript中String.match()方法的使用详解
2015/06/06 Javascript
基于jQuery实现动态搜索显示功能
2016/05/05 Javascript
javascript时间差插件分享
2016/07/18 Javascript
用JavaScript实现让浏览器停止载入页面的方法
2017/01/19 Javascript
BootStrap中jQuery插件Carousel实现轮播广告效果
2017/03/27 jQuery
javascript获取指定区间范围随机数的方法
2017/09/08 Javascript
js如何找出字符串中的最长回文串
2018/06/04 Javascript
vue axios 简单封装以及思考
2018/10/09 Javascript
使用Sonarqube扫描Javascript代码的示例
2018/12/26 Javascript
ant-design-vue 快速避坑指南(推荐)
2020/01/21 Javascript
JS获取表格视图所选行号的ids过程解析
2020/02/21 Javascript
vue 路由守卫(导航守卫)及其具体使用
2020/02/25 Javascript
[42:36]DOTA2上海特级锦标赛B组败者赛 VG VS Spirit第二局
2016/02/26 DOTA
python版本单链表实现代码
2018/09/28 Python
centos6.5安装python3.7.1之后无法使用pip的解决方案
2019/02/14 Python
python/Matplotlib绘制复变函数图像教程
2019/11/21 Python
基于virtualenv创建python虚拟环境过程图解
2020/03/30 Python
CSS3制作皮卡丘动画壁纸的示例
2020/11/02 HTML / CSS
法国设计制造的扫帚和刷子:Andrée Jardin
2018/12/06 全球购物
挂科检讨书范文
2014/02/20 职场文书
揭牌仪式主持词
2014/03/19 职场文书
营销计划书
2015/01/17 职场文书
给校长的建议书范文
2015/09/14 职场文书
MySQL数据库超时设置配置的方法实例
2021/10/15 MySQL