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 26 Golang
go 原生http web 服务跨域restful api的写法介绍
Apr 27 Golang
golang通过递归遍历生成树状结构的操作
Apr 28 Golang
golang gopm get -g -v 无法获取第三方库的解决方案
May 05 Golang
浅谈golang package中init方法的多处定义及运行顺序问题
May 06 Golang
Golang实现AES对称加密的过程详解
May 20 Golang
Go语言实现Snowflake雪花算法
Jun 08 Golang
golang中的struct操作
Nov 11 Golang
Go语言基础map用法及示例详解
Nov 17 Golang
简单聊聊Golang中defer预计算参数
Mar 25 Golang
golang为什么要统一错误处理
Apr 03 Golang
Golang 结构体数据集合
Apr 22 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
PHP5.3.1 不再支持ISAPI
2010/01/08 PHP
PHP CURL获取cookies模拟登录的方法
2013/11/04 PHP
PHP 正则判断中文UTF-8或GBK的思路及具体实现
2013/11/26 PHP
php实现微信公众平台账号自定义菜单类
2015/10/11 PHP
PHP微信开发之查询微信精选文章
2016/06/23 PHP
PHP利用pdo_odbc实现连接数据库示例【基于ThinkPHP5.1搭建的项目】
2019/05/13 PHP
如何解决PHP获取不到SESSION信息之一般情况
2019/10/10 PHP
使用Git实现Laravel项目的自动化部署
2019/11/24 PHP
js实现点击注册按钮开始读秒倒计时的小例子
2013/05/11 Javascript
Javascript学习笔记之相等符号与严格相等符号
2014/11/23 Javascript
Bootstrap进度条学习使用
2017/02/09 Javascript
Swiper实现轮播图效果
2017/07/03 Javascript
javascript trie前缀树的示例
2018/01/29 Javascript
JavaScript捕捉事件和阻止冒泡事件实例分析
2018/08/03 Javascript
vue2.0 element-ui中el-select选择器无法显示选中的内容(解决方法)
2018/08/24 Javascript
vue完成项目后,打包成静态文件的方法
2018/09/03 Javascript
vue 中使用 watch 出现了如下的报错的原因分析
2019/05/21 Javascript
JS开发自己的类库实例分析
2019/08/28 Javascript
vue 框架下自定义滚动条(easyscroll)实现方法
2019/08/29 Javascript
用python + openpyxl处理excel2007文档思路以及心得
2014/07/14 Python
python操作gmail实例
2015/01/14 Python
Django使用AJAX调用自己写的API接口的方法
2019/03/06 Python
Python图片的横坐标汉字实例
2019/12/04 Python
Python 3.8 新功能大揭秘【新手必学】
2020/02/05 Python
python 安装库几种方法之cmd,anaconda,pycharm详解
2020/04/08 Python
使用CSS3实现多列布局与多背景的技巧
2016/02/29 HTML / CSS
浅谈CSS3特性查询(Feature Query: @supports)功能简介
2017/07/31 HTML / CSS
Ralph Lauren拉夫·劳伦美国官网:带有浓郁美国气息的高品味时装品牌
2017/11/01 全球购物
捷克母婴用品购物网站:Feedo.cz
2020/12/28 全球购物
行政文秘岗位职责范本
2014/02/10 职场文书
房产分割协议书范文
2014/11/21 职场文书
刘胡兰观后感
2015/06/16 职场文书
灵魂歌王观后感
2015/06/17 职场文书
新手入门Jvm-- JVM对象创建与内存分配机制
2021/06/18 Java/Android
在容器中使用nginx搭建上传下载服务器
2022/05/11 Servers
Redis 报错 error:NOAUTH Authentication required
2022/05/15 Redis