Golang: 什么时候nil != nil

本文深入探讨了Go语言中nil的比较规则,解释了为何nil!=nil及如何避免相关问题。通过实例展示了不同类型的nil变量在比较时的特殊行为,揭示了指针和空接口在类型和值上的差异。

最近在做Go语言开发工作中遇到不少坑,今天来理一理在哪些情况下我们使用 == 操作符号对比时候 nil != nil,以及我们如何避免在代码中遇到这些问题。

我们先定义两个不同类型的变量,每个变量都赋值为nil。

var a *int = nil

var b interface{} = nil

想象一下下面的代码会输出什么结果:

fmt.Println("a == nil", a == nil)

fmt.Println("b == nil", b == nil)

fmt.Println("a == b:", a == b)

运行结果:

a == nil: true

b == nil: true

a == b: false

我们再来看一个类似的例子,我们将b初始化为a

var a *int = nil

var b interface{} = a

fmt.Println("a == nil:", a == nil)

fmt.Println("b == nil:", b == nil)

fmt.Println("a == b:", a == b)

我们再次运行上面代码后,输出的结果如下

a == nil: true

b == nil: false

a == b: true

这是怎么回事?

这个问题不是Go语言bug,这只是一个特定的规则,我们理解这个规则后就能明白经常看到开源代码中写的下面这种代码是为什么了。

if nil == a {

b = nil

}

这里对b赋值之前对a进行了检测并且直接赋值了 nil。

我们需要搞清楚的第一件事,Go中的所有指针都有两个值,一个是类型(表明指针的类型),一个是值(指向具体值).也就是说每个指针都需要有一个类型来表明指针属于什么类型,所以我们不能给一个指针类型赋值为nil。例如下面这个代码就不能被编译

a := nil

为了让这行代码被编译,必须要给这个指针赋值一个类型,然后对它的值赋值为nil,就像这样:

var a *int = nil

var b interface{} = nil

现在这些变量都有了类型,就可以使用 fmt.Printf 函数打印出它们的类型了。

var a *int = nil

var b interface{} = nil

fmt.Printf("a=(%T, ...)\n", a)

fmt.Printf("b=(%T, ...)\n", b)

注: %T 格式化符仅仅打印出变量的类型

运行结果:

a=(*int, ...)

b=(<nil>, ...)

上面的结果显示,当a被赋值为nil时候,它的类型被赋值为 *int;

b是一个interface{}(空接口),但是它被赋值nil时候它的类型却被指定为 <nil>。

这是怎么回事?

我们使用空接口主要是为了符合任何类型的实现。<nil>其实也是一个类型,它符合一个空接口的标准。

好了,我们知道所有的指针都是由 (type, value)这样的组合构成,我们看到了给变量硬编码 nil时候会出现的情况。下面我们来给这些类型赋值,看会有什么变化。

var a *int = nil

var b interface{} = a

fmt.Printf("a=(%T, ...)\n", a)

fmt.Printf("b=(%T, ...)\n", b)

运行结果:

a=(*int, ...)

b=(*int, ...)

运行程序之后你会发现你会得到一个新的结果,b有了一个新的类型。

在我们之前硬编码赋值时候,b是一个<nil>类型的指针。我们来看看使用a给b赋值时候,发生了什么。

比较a和b的值

既然我们了解这些类型被确定,让我们看看会发生什么,当我们在代码中检查a和b相等情况。我们开始对a和b都被分配给硬编码nil。之后,我们将会看到类似的片段,a赋值给b。

var a *int = nil

var b interface{} = nil

fmt.Printf("a=(%T, %v)\n", a, a)

fmt.Printf("b=(%T, %v)\n", b, b)

fmt.Println()

fmt.Println("a == nil:", a == nil)

fmt.Println("b == nil:", b == nil)

fmt.Println("a == b:", a == b)

运行结果:

a=(*int, <nil>)

b=(<nil>, <nil>)

a == nil: true

b == nil: true

a == b: false

这里很奇怪的是 a和b是不相等的,这是一个很奇怪的状况,看起来像是 a == nil 且 b == nil,所以应该是 a == b,但是实际输出结果并不是这样。

实际上我们写的 a == nil 并不是真正的在比较值,我们实际上是在比较(type,value) 这个结构。我们并不是仅仅比较存储在a中的值.

下面模拟显示一下实际比较的值:

a == nil: (*int, <nil>) == (*int*, <nil>)

b == nil: (<nil>, <nil>) == (<nil>, <nil>)

a == b: (*int, <nil>) == (<nil>, <nil>)

当我们看到上面这个就明白了,a和b实际上是不等的,因为它们的类型并不一样,但是在代码中这里并不会显示展示出来,所以我们会误解它们应该是相等的。

但是如果你这样写代码

if nil == a && nil == b {

//表达式为true

}

下面我们看一下将a赋值给b,会发生什么

var a *int = nil

var b interface{} = a // <- the change

fmt.Printf("a=(%T, %v)\n", a, a)

fmt.Printf("b=(%T, %v)\n", b, b)

fmt.Println()

fmt.Println("a == nil:", a == nil)

fmt.Println("b == nil:", b == nil)

fmt.Println("a == b:", a == b)

运行结果:

a=(*int, <nil>)

b=(*int, <nil>)

a == nil: true

b == nil: false

a == b: true

现在的结果又有问题了,b == nil 返回 false

当我们运行 b == nil的时候,编译器需要确定给nil一个什么类型,实际上编译器给出了<nil,nil>。但是实际上这个时候b被赋值为 <*int, nil>。明显编译器就不会认为他们是相等的了。

这个地方实际上会造成一些混淆,我们以为编译器会处理这个问题,但是实际上编译器并不能处理这个问题,因为interface{}的类型在运行的过程中随时在发生变化。

比如下面的程序

var a *int = nil

var b interface{} = a

var c *string = nil

fmt.Printf("b=(%T, %v)\n", b, b)

fmt.Println("b == nil:", b == nil)

b = c

fmt.Printf("b=(%T, %v)\n", b, b)

fmt.Println("b == nil:", b == nil)

b = nil

fmt.Printf("b=(%T, %v)\n", b, b)

fmt.Println("b == nil:", b == nil)

运行结果:

b=(*int, <nil>)

b == nil: false

b=(*string, <nil>)

b == nil: false

b=(<nil>, <nil>)

b == nil: true

从这个结果我们就可以看出来,编译器在编译的时候并不能确定b的类型,只能在运行期的时候确定b的具体类型,所以编译器并不能处理这个问题。

我们来看看如何强制让编译器将 nil放进正确类型里,实际上这并不是唯一的情况下让编译器使用这样的类型决定。比如,当你给变量分配一个硬编码的数字,编译器应该使用哪种类型将基于程序的上下文来决定。当声明的变量(如var int = 12),但这也会发生在当我们经过一个硬编码值函数或当我们给变量分配一个数字。所有这些情况如下所示。

程序如下:

package main

import "fmt"

func main() {

var a int = 12

var b float64 = 12

var c interface{} = a

d := 12 // will be an int

fmt.Printf("a=(%T,%v)\n", a, a)

fmt.Printf("b=(%T,%v)\n", b, b)

fmt.Printf("c=(%T,%v)\n", c, c)

fmt.Printf("d=(%T,%v)\n", d, d)

useInt(12)

useFloat(12)

}

func useInt(n int) {

fmt.Printf("useInt=(%T,%v)\n", n, n)

}

func useFloat(n float64) {

fmt.Printf("useFloat=(%T,%v)\n", n, n)

}

运行结果:

var a int = 12

var b float64 = 12

var c interface{} = a

fmt.Println("a==12:", a == 12) // true

fmt.Println("b==12:", b == 12) // true

fmt.Println("c==12:", c == 12) // true

fmt.Println("a==c:", a == c) // true

fmt.Println("b==c:", b == c) // false

有没有一种抓狂的感觉。a等于12,b等于12,c等于12,a等于c,但是c不等于a。怎么回事?

我们看看之前我们得出的结论:

a=(int,12)

b=(float64,12)

c=(int,12)

看到这个是不是有点明白怎么回事了?类型不匹配啊!!!

另外有一个有意思的事情就是,当你拿一个数字和一个接口相比较时候,这个接口的类型永远是int;类似的当你硬编码的nil相比较时候,这个时候类型永远是<nil, nil>.

var b float64 = 12

var c interface{} = b

fmt.Println("c==12:", c == 12)

fmt.Printf("c=(%T,%v)\n", c, c)

fmt.Printf("hard-coded=(%T,%v)\n", 12, 12)

运行结果:

c==12: false

c=(float64,12)

hard-coded=(int,12)

总结

当我们硬编码的值和变量编译器做==操作时候,假定他们有一些特定类型和遵循一些规则来实现,指针的数据结构是<type,value>,比较时候会比较这两个参数。有时候这个情况会比较困惑,但是你只要去适应它就好了。如果你发现各种类型都可以分配给nil,避免问题的一种常用技术是显式地指定为nil。,而不是 a = b,例如:

var a *int = nil

var b interface{}

if a == nil {

  b = nil

}

这样我们对b进行硬编码赋值nil时候才可以得到我们想要的效果。

最后举一个常出现的例子

type AAer interface {

That() int

}

type BB struct {

}

type BBMgr struct {

bbMap map[int]AAer

}

func (this *BB) That() int {

return 0

}

func (this *BBMgr) QueryAA(id int32) AAer {

aa, _ :=this.bbMap[id]

//这里如果没有找到数据 返回的接口去做 nil比较的话

//发现 AAer永远不为nil,但是执行AAer的函数就报错

// 实际上这里应该写

// aa, ok :=this.bbMap[id]

// if !ok {

// return nil

// }

return aa

}

<think>嗯,用户问的是如何在Golang中调整if-else逻辑以便支持多个分支同时执行。根据提供的引用内容,我需要先理解现有的流程控制结构,特别是if-else和switch的用法。 首先,用户提供的代码示例里有多个条件判断,比如processIntersection、filterKey等函数调用,这些可能是在处理不同的条件分支。而用户希望这些分支能够同时执行,而不是像传统的if-else那样只执行其中一个。传统的if-else结构是互斥的,一旦某个条件满足,后面的else if就不会执行了。所以用户的需求应该是让每个条件都独立判断,符合条件的都执行,而不是互斥。 接下来,我需要参考提供的引用资料。引用[5]中提到可以使用switch代替多个if-else,但默认情况下switch的case也是互斥的。不过,在Go中,switch语句如果没有表达式,会按顺序评估每个case的条件,执行第一个为true的case,而不会继续检查后面的。所以直接使用switch可能无法满足同时执行多个分支的需求。 那怎么办呢?可能需要把每个条件判断独立出来,不使用else if,而是用多个独立的if语句。这样每个条件都会被单独检查,不管前面的条件是否满足。例如,去掉else,只保留if,这样每个条件都会依次判断,满足的话就执行对应的代码块。引用[3]和[4]提到if语句的使用,不需要括号,但必须有大括号,而且可以独立使用多个if。 所以,修改的思路应该是将原来的if-else结构拆分成多个独立的if语句,每个条件单独判断。例如,原来的代码可能是这样的: if condition1 { // code1 } else if condition2 { // code2 } else { // default } 现在要改成: if condition1 { // code1 } if condition2 { // code2 } if condition3 { // code3 } 这样每个条件都会被检查,即使前面的条件已经满足。不过,用户可能还希望保留一个默认的else分支,但这样的话,如果所有条件都不满足,才会执行else。但这样无法同时执行多个分支,所以可能不需要else,或者将else改为最后一个独立的if,检查前面所有条件都不满足的情况。 另外,用户提到的示例代码中有processIntersection、filterKey等函数,可能这些函数需要被依次执行,只要满足条件就执行,而不是只执行其中一个。比如,一个数据可能需要经过多个过滤处理,每个条件独立判断是否需要处理。 因此,正确的做法应该是将每个条件分支写成独立的if语句,而不是通过else if连接。这样,每个条件都会被评估,可能多个条件同时满足,从而执行多个对应的代码块。 此外,引用[5]中的switch示例虽然用于多条件判断,但默认只会执行第一个匹配的case。不过,如果用户确实想用switch,可以考虑在每个case后面加上fallthrough,但这通常用于无条件执行下一个case,可能不符合需求。所以更合适的方式还是拆分成多个if语句。 总结下来,解决方法是去除else if,改用多个独立的if语句,每个条件独立判断和执行。这样多个分支的条件可能同时满足,各自执行对应的代码块,达到同时执行多个分支的效果。</think>在Golang中,if-else结构默认采用互斥执行逻辑(仅执行首个满足条件的分支),但可以通过以下两种方式实现多分支同时执行: ### 方法一:独立if语句法 通过将多个条件拆分为独立的if语句,使每个条件独立判断: ```go if processIntersection() { // 执行分支1逻辑 } if filterKey() { // 执行分支2逻辑 } if filterFutureVersions() { // 执行分支3逻辑 } if removeCurrentVersions() { // 执行分支4逻辑 } ``` ▲ 特点:每个条件独立判断,可能触发多个分支执行[^3][^4] ### 方法二:switch-case组合法 利用switch的case表达式支持组合判断特性: ```go switch { case processIntersection(): // 执行分支1 fallthrough // 可选继续执行后续case case filterKey(): // 执行分支2 case filterFutureVersions(), removeCurrentVersions(): // 执行分支3或4(逗号表示OR条件) default: // 默认逻辑 } ``` ▲ 注意:需配合`fallthrough`关键字控制执行流程[^5] ### 对比选择建议 | 方法 | 执行特性 | 适用场景 | 注意事项 | |-----------|---------------|--------------------|--------------------| | 独立if语句 | 完全独立判断 | 需要严格并行处理的场景 | 注意条件间的耦合关系 | | switch-case | 可控顺序执行 | 需要条件组合判断的场景 | fallthrough需谨慎使用 | ### 示例改造 原始互斥逻辑: ```go if processIntersection() { // ... } else if filterKey() { // ... } else if filterFutureVersions() { // ... } else { removeCurrentVersions() } ``` 并行执行改造: ```go // 独立if实现并行执行 if processIntersection() { // 执行A逻辑 } if filterKey() { // 执行B逻辑 } if filterFutureVersions() || removeCurrentVersions() { // 执行组合逻辑 } ``` ### 最佳实践 1. 使用`defer`处理公共逻辑: ```go func main() { defer cleanup() // 最终执行 if cond1 { /*...*/ } if cond2 { /*...*/ } } ``` 2. 通过通道同步执行结果: ```go results := make(chan string) go func() { if cond1 { results <- "A" } }() go func() { if cond2 { results <- "B" } }() ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值