前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Go常见错误集锦之令人困惑的nil切片和空切片

Go常见错误集锦之令人困惑的nil切片和空切片

作者头像
Go学堂
发布2023-01-31 15:37:39
发布2023-01-31 15:37:39
1.3K00
代码可运行
举报
文章被收录于专栏:Go工具箱Go工具箱
运行总次数:0
代码可运行

在使用Go编程的时候,切片是常用的数据结构之一。而在实际项目中,大家都会遇到nil切片和空切片。那什么是nil切片,什么又是空切片呢?通过本文,我们主要解决以下几个问题:

  • 什么是空切片
  • 什么是nil切片
  • nil切片的使用场景
  • 如何正确判断切片是否为空

什么是空切片

在Go中对空切片的定义是这样的:如果切片的长度是0,那么称该切片是空切片

我们通过一个例子来看下定义切片的不同方式。同时我们判断切片是否是nil以及它的长度和容量。

代码语言:javascript
代码运行次数:0
运行
复制
//定义变量
var s []string
fmt.Printf("1:nil=%t, len=%d, cap=%d\n", s == nil, len(s), cap(s))

//组合字面量方式
s = []string{}
fmt.Printf("2:nil=%t, len=%d, cap=%d\n", s == nil, len(s), cap(s))

//make方式
s = make([]string, 0)
fmt.Printf("3: nil=%t, len=%d, cap=%d\n", s == nil, len(s), cap(s))

运行上面的代码,将会有如下的输出:

代码语言:javascript
代码运行次数:0
运行
复制
1: nil=true, len=0, cap=0
2: nil=false, len=0, cap=0
3: nil=false, len=0, cap=0

根据空切片的定义以及输出结果,我们发现3个切片的长度和容量都为0,即都是空切片。同时我们也发现只有第一个切片为nil。那nil切片又是什么呢?

什么是nil切片?

我们看下在Go源码中的builtin中的定义:

nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type.

翻译成中文的大致含义是:nil是为pointer、channel、func、interface、map或slice类型预定义的标识符,代表这些类型的零值。

可见,在Go中,nil代表的是上述类型的零值。切片类型的默认零值是nil,所以在上述的代码中 s 是nil切片。同时s的长度是0,可见nil切片也是空切片。既然都是空切片,那么nil切片和非nil的空切片的区别是什么呢?

我们知道,slice的底层结构体中是由3个字段构成的:长度、容量和指向底层数组的指针字段。如下图:

而nil切片除了长度和容量都是0之外,还有就是ptr指针不指向任何底层数组,这也是和空切片的本质区别

如下图表示一个nil切片:

我们将nil切片和空切片做个小结:

  • nil切片的长度和容量都是0,空切片的长度为0,容量由指向的底层数组决定
  • 空切片 != nil切片
  • nil切片的ptr指针是nil,而空切片的ptr指针指向底层数组的地址
  • nil切片也切片,具有和普通切片相同的行为,所以nil切片具有切片同样的行为操作,可以放心使用。

那为什么要区分nil切片和非nil的空切片呢?接下来我们看下nil切片的主要使用场景。

nil切片使用场景

场景一:append可以直接往nil切片中添加元素

可以对nil切片进行append操作。在切片容量未知的前提下,建议优先声明为nil切片,而不用担心容量问题。因为它的每次重分配容量都是倍增的。即nil切片的第一次append,会重分配一个容量为1的切片。而后会分配容量为2/4/8/16这样倍增的数值。如下代码:

代码语言:javascript
代码运行次数:0
运行
复制
var s []int
s = append(s, []int{1, 2, 3}...)

场景二:encoding/json包对nil和非nil-空切片编码结果不同

在对切片进行json.Marshal编码的时候,nil切片会被编码成null,而空切片会被编码成空数组:[]。如下代码所示:

代码语言:javascript
代码运行次数:0
运行
复制
type Person {
 Friends []string
}

var f1 []string //nil切片
json1, _ := json.Marshal(Person{Friends: f1})
fmt.Printf("%s\n", json1) //output:{"Friends": null}


f2 := make([]string, 0) //non-nil空切片
json2, _ := json.Marshal(Person{Friends: f2})
fmt.Printf("%s\n", json2) //output: {"Friends": []}

如何判断切片是否为空切片

在实际编程中,还有一种常见的场景,就是需要判断切片是否为空切片。下面的例子首先调用一个返回float32类型的切片的getOperations函数,然后依据该函数的返回切片中是否有元素来调用handle函数,代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
func handleOperations(id string) {
  operations := getOperations(id)
  if operations != nil { ①
    handle(operations)
  }
}

func getOperations(id string) []float32 {
  operations := make([]float32, 0) ②
  if id == "" {
    return operations ③
  }
  // Add elements to operations
  return operations 
}

① 检查operations是否是nil切片

② 初始化operations切片为空切片

③ 如果id为空,直接返回operations空切片

在该示例中,我们通过检查operations是否为nil切片来判断该切片中是否包含元素。但这样存在一个问题是,在getOperations函数中,初始化的是一个operations的空切片,而非nil切片,所以该函数永远不会返回nil。因此,operations != nil 的条件永远都是true。那该如何解决这个问题呢?

方法1:返回nil

代码语言:javascript
代码运行次数:0
运行
复制
func getOperations(id string) []float32 {
  operations := make([]float32, 0) 
  if id == "" {
    return nil ①
  }
  // Add elements to operations
  return operations 
}

① 当id为空时直接返回nil

这样该表被调用的的函数返回值,可以解决该问题。但有时候我们使用的是第三方函数库,对被调用的函数是没有控制权的。所以我们还应该考虑第2种方法。

方法2:在调用函数中判断返回的切片长度是否为0

代码语言:javascript
代码运行次数:0
运行
复制
func handleOperations(id string) {
  operations := getOperations(id)
  if len(operations) != 0 { ①
    handle(operations)
  }
}

① 根据切片的长度来判断是否为空切片,而非是否是nil

这样,无论返回值是nil切片还是空切片,都可以正常工作。即如果要判断切片是否为空,要根据切片长度是否为0而不是根据值是否为nil来判断

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go学堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在使用Go编程的时候,切片是常用的数据结构之一。而在实际项目中,大家都会遇到nil切片和空切片。那什么是nil切片,什么又是空切片呢?通过本文,我们主要解决以下几个问题:
    • 什么是空切片
    • 什么是nil切片?
    • nil切片使用场景
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档