golang内置函数len的小技巧

 更新时间:2021年07月25日 08:32:18   作者:@apocelipes  
len是很常用的内置函数,可以测量字符串、slice、array、channel以及map的长度/元素个数。本文就来介绍一下其他小技巧,感兴趣的可以了解一下

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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解Go语言中如何创建Cron定时任务

    详解Go语言中如何创建Cron定时任务

    Cron是一个强大的定时任务调度库,它允许开发者在Go应用中方便地设置和管理定时任务,本文将结合具体案例,详细介绍Cron在Go语言中的用法,需要的可以参考下
    2024-10-10
  • Go语言项目中使用Viper获取配置信息详解

    Go语言项目中使用Viper获取配置信息详解

    Viper是Go应用的完整配置解决方案,它能处理所有类型的配置需求和配置格式,这篇文章主要介绍了Go项目中使用Viper获取配置信息,需要的可以参考下
    2024-04-04
  • golang中tar压缩和解压文件详情

    golang中tar压缩和解压文件详情

    这篇文章主要给大家介绍golang中tar压缩和解压文件,文章以查看官方文档自带的给大家演习一下golang的archive/tar压缩和解压功能,需要的朋友可以参考一下
    2021-11-11
  • 详解go-zero如何使用validator进行参数校验

    详解go-zero如何使用validator进行参数校验

    这篇文章主要介绍了如何使用validator库做参数校验的一些十分实用的使用技巧,包括翻译校验错误提示信息、自定义提示信息的字段名称、自定义校验方法等,感兴趣的可以了解下
    2024-01-01
  • Go并发编程实现数据竞争

    Go并发编程实现数据竞争

    本文主要介绍了Go并发编程实现数据竞争,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09
  • Golang Printf,Sprintf,Fprintf 格式化详解

    Golang Printf,Sprintf,Fprintf 格式化详解

    这篇文章主要介绍了Golang Printf,Sprintf,Fprintf 格式化详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • golang中使用proto3协议导致的空值字段不显示的问题处理方案

    golang中使用proto3协议导致的空值字段不显示的问题处理方案

    这篇文章主要介绍了golang中使用proto3协议导致的空值字段不显示的问题处理方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • Golang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由字母数字和_(下划线)组成,并且只能以字母和_开头,更详细内容请看下面文章吧
    2021-10-10
  • 使用Go语言创建静态文件服务器问题

    使用Go语言创建静态文件服务器问题

    这篇文章主要介绍了使用Go语言创建静态文件服务器,本文通过试了代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • golang时间/时间戳的获取与转换实例代码

    golang时间/时间戳的获取与转换实例代码

    说实话,golang的时间转化还是很麻烦的,最起码比php麻烦很多,下面这篇文章主要给大家介绍了关于golang时间/时间戳的获取与转换的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-11-11

最新评论